Full Code of nicknlsn/MarioKart64NEAT for AI

master 7cdfcb012e2e cached
2 files
46.4 KB
15.1k tokens
1 requests
Download .txt
Repository: nicknlsn/MarioKart64NEAT
Branch: master
Commit: 7cdfcb012e2e
Files: 2
Total size: 46.4 KB

Directory structure:
gitextract_w0r8ezht/

├── README.md
└── neat.lua

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# Mario Kart 64 - NEAT Algorithm

This is an implementation of the NEAT algorithm in Lua for Mario Kart 64 and the BizHawk emulator. To use this code you need to create a save state at the beginning of a level. The code expects the name of the save state file to be BB150.state, however you can change this by changing line 54 of the code (state_file = "BB150.state"). Make sure the save state file is in the Lua folder (in the BizHawk folder) in order to use it.

[NEAT paper](http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf)

[More information on NEAT](https://www.cs.ucf.edu/~kstanley/neat.html)

### After about 2.5 days of training:
<a href="https://youtu.be/tmltm0ZHkHw" target="_blank"><img src="http://img.youtube.com/vi/tmltm0ZHkHw/0.jpg" 
alt="IMAGE ALT TEXT HERE" width="240" height="180" border="10" /></a>


================================================
FILE: neat.lua
================================================
--===========================================================================--
---------------------------NEAT algorithm for MK64-----------------------------
--===========================================================================--
--                                                                           --
-- Author: Nick Nelson                                                       --
-- November, 2016                                                            --
-- You may freely use this code, but please give credit to the original      --
--   author.                                                                 --
--                                                                           --
-- Setup: create a save state at the beginning of a level. Call it
--   LR150.state and save it in the lua folder. Note: So far I have only
--   tested this on Luigi Raceway with 150cc.
--===========================================================================--

--[[
Known bugs:
- replay best, then reload, in_cell is now nil. update: this only happens
  sometimes, just do it again

Possible bugs:
- in at least one case a single species took over the entire population, which
  could mean i have something wrong in the code that is supposed to prevent
  this...

Haven't tested:
- load a backup file instead of the saved file
- spawning code could use more testing

Areas for improvement:
- adjust mutation code to better suit the problem

Some unknown object types:
- 43 hot air balloon in luigi raceway?
- 42 blue shell I think
- 21 dead banana?
- 22 dead banana?
- 14
]]

function p(line)
	console.write(line)
end

function pn(line)
	console.writeline(line)
end

function initialize_things()
	console.clear()

	-- pn(game)
	pn('MK64 NEAT')

	state_file = "BB150.state"

	-- TODO add bumpers?
	button_input_names = {
	-- "Start",
	"P1 B",
	"P1 A",
	"P1 Z",
	"P1 A Down",
	"P1 A Left",
	"P1 A Right",
	"P1 A Up",
	"P1 L",
	"P1 R" }

	button_actual_names = {
	-- "Start",
	"B",
	"A",
	"Z",
	"Down",
	"Left",
	"Right",
	"Up",
	"L",
	"R" }
	num_buttons = #button_actual_names

	course = {}

	-- collision address
	course.col_addresses = {
	0x1D65A0,  -- Mario Raceway
	0x1D4280,  -- Choco Mountain
	"",
	0x1D8380,  -- Banshee Boardwalk
	0x1E5170,  -- Yoshi Valley
	0x1D4650,  -- Frappe Snowland
	0x1E6380,  -- Koopa Troopa Beach
	0x1DAF50,  -- Royal Raceway
	0x1DD1C0,  -- Luigi Raceway
	0x1E1500,  -- Moo Moo Farm
	0x1F0AC0,  -- Toad's Turnpike
	0x1F0120,  -- Kalamari Desert
	0x1D6A70,  -- Sherbet Land
	0x1E3080,  -- Rainbow Road
	0x1D9AE0,  -- Wario Stadium
	"",
	"",
	"",
	0x1E1300,
	""}  --

	-- the collision attribute for each track
	course.track_attribute = {
	"",  -- Mario Raceway
	"",  -- Choco Mountain
	"",
	6,
	2,  -- Yoshi Valley
	5,  -- Frappe Snowland
	3,  -- Koopa Troopa Beach
	1,  -- Royal Raceway
	1,  -- Luigi Raceway
	2,  -- Moo Moo Farm
	1,  -- Toad's Turnpike
	2,  -- Kalamari Desert
	"",  -- Sherbet Land
	1,  -- Rainbow Road
	"",  -- Wario Stadium
	"",
	"",
	"",
	2,
	""}

	course.names = {
	"Mario Raceway      ",
	"Choco Mountain     ",
	"Bowser's Castle    ",
	"Banshee Boardwalk  ",
	"Yoshi Valley       ",
	"Frappe Snowland    ",
	"Koopa Troopa Beach ",
	"Royal Raceway      ",
	"Luigi Raceway      ",
	"Moo Moo Farm       ",
	"Toad's Turnpike    ",
	"Kalimari Desert    ",
	"Sherbet Land       ",
	"Rainbow Road       ",
	"Wario Stadium      ",
	"Block Fort         ",
	"Skyscraper         ",
	"Double Deck        ",
	"DK's Jungle Parkway",
	"Big Donut          "}

	savestate.load(state_file)

	course.selected_addr = 0xDC5A0
	course.number = mainmemory.read_u16_be(course.selected_addr) + 1
	course.name = course.names[course.number]
	course.col_start = course.col_addresses[course.number]
	-- course.col_fin = course.col_addr_fins[course.number]
	course.col_step = 0x2C
	course.col_attr_offset = 0x02
	course.p1_offset = 0x11
	course.p2_offset = 0x15
	course.p3_offset = 0x19
	course.tr_attr = course.track_attribute[course.number]

	-- this loads the map, just the track sections though
	load_map()

	-- kart data
	kart = {}
	kart.x_addr = 0x0F69A4
	kart.xv_addr = 0x0F69C4
	kart.y_addr = 0xF69A8
	kart.yv_addr = 0x0F69C8
	kart.z_addr = 0x0F69AC
	kart.zv_addr = 0x0F69CC
	dist_addr = 0x16328A
	kart.sin = 0xF6B04
	kart.cos = 0xF6B0C

	character_addr = 0x0DC53B
	character = mainmemory.read_u8(character_addr)

	-- object addresses
	obj = {}
	obj.addr = 0x15F9B8
	obj.stop = 0x162578
	obj.step = 0x70
	obj.x_offset = 0x18
	obj.y_offset = 0x1C
	obj.z_offset = 0x20

	-- some colors
	black  = 0xFF000000
	white  = 0xFFFFFFFF
	red    = 0xFFFF0000
	bred   = 0x60FF0000
	green  = 0xFF00FF00
	sgreen = 0xFF009900
	blue   = 0xFF0000FF
	yellow = 0xFFFFFF00
	fbox   = 0xFF00007F
	bblue  = 0x200000FF
	l_off  = 0x500000FF
	bwhite = 0x60FFFFFF
	back   = 0x40808080
	none   = 0x00000000
	b_on   = 0xFF1d2dc1
	b_off  = 0x90000000
	player = {
	0x80FF0000, -- mario
	0x0, -- luigi
	0x0, --
	0x0, --
	0x0, --
	0x0, --
	0x0, --
	0x0} --

	math.randomseed(os.time())
	box_radius = 6
	num_inputs = box_radius*box_radius*4
	max_nodes = 1000000
	fitness = 0
	gain = 0
	highest_fitness = 0
	highest_distance = 0
	global_max_fitness = 0
	global_best_genome = new_genome()
	replay_best_genome = false
	not_advanced = 20
	global_innovation = 0
	current_species = 0
	population = 200
	compatibility_threshold = 1.0
	c1 = 1
	c2 = 1
	c3 = 1
	mutate_weights_chance   = 0.25
	weight_perturbation     = 2
	-- must be at least one so all genomes have at least one gene
	mutate_structure_chance = 1
	add_node_chance         = 0.66

	clear_controller()

	-- initialize population
	initialize_population()
end -- end initialize_things

function create_form()
	form = forms.newform(250, 400, "MK64 NEAT")
	hide_net = forms.checkbox(form, "Hide Network", 5, 5)
	hide_xyz_data = forms.checkbox(form, "Hide Info", 5, 25)
	load_save_label = forms.label(form, "Save/Load file:", 5, 50)
	load_save_backup = forms.textbox(
		form, state_file.."_frequent_backup.txt", 155, 30, nil, 5, 73
		)
	-- see next_genome() function
	save_button = forms.button(form, "Save", save_here, 5, 95)
	load_button = forms.button(form, "Load", load_generation, 85, 95)
	replay_best_button = forms.button(
		form, "Replay Best", replay_best, 5, 125
		)
	restart_button = forms.button(form, "Restart", initialize_things, 85, 125)

	l_generation = forms.label(form, "Generation: 0", 5, 160)
	l_species = forms.label(form, "Species: 0", 5, 185)
	l_genome = forms.label(form, "Genome: 0", 5, 210)
	l_fitness = forms.label(form, "Fitness: 0", 5, 235)
	l_max_fitness = forms.label(form, "Max Fitness: 0", 5, 260)
	l_member = forms.label(form, "Member: ", 5, 285)

end

function game_over()
	forms.destroy(form)
end

function round(num, idp)
	local mult = 10^(idp or 0)
	return math.floor(num * mult + 0.5) / mult
end

function display_info()
	gui.drawBox(-1, 214, 320, 240, none, bwhite)
	-- gui.drawText(-2, 212, course.name, black, none)
	gui.drawText(-2, 212, "Generation:"..pop.generation, black, none)
	gui.drawText(120, 212, "Species:"..pop.current_species, black, none)
	gui.drawText(240, 212, "Genome:"..pop.current_genome, black, none)

	gui.drawText(-1, 224, "Fitness:".. round(fitness), black, none)
	gui.drawText(
		90, 224, "Max Fitness:"..round(global_max_fitness), black, none
		)
	gui.drawText(215, 224, "Member:"..pop.member.."/"..population, black, none)

	if pop.frame_count % 10 == 0 then
		forms.settext(l_generation, "Generation: "..pop.generation)
		forms.settext(l_species, "Species: "..pop.current_species)
		forms.settext(l_genome, "Genome: "..pop.current_genome)
		forms.settext(l_fitness, "Fitness: "..round(fitness))
		forms.settext(l_max_fitness, "Max Fitness: "..round(global_max_fitness))
		forms.settext(l_member, "Member: "..pop.member.."/"..population)
	end

-------------------------------------------------------------------------------
	-- gui.text(100, 0, "Distance: " .. distance, "white", "black")
	-- gui.text(0, 0, course.name, "white", "topright")

	-- gui.text(0,17, "X ".. string.format("%.3f", kart_x),"white","bottomleft")
	-- gui.text(0,2, "Xv ".. string.format("%.3f", kart_xv),"white","bottomleft")

	-- gui.text(120,17, "Y ".. string.format("%.3f", kart_y),"white","bottomleft")
	-- gui.text(120,2, "Yv ".. string.format("%.3f", kart_yv),"white","bottomleft")

	-- gui.text(240,17, "Z ".. string.format("%.3f", kart_z),"white","bottomleft")
	-- gui.text(240,2, "Zv ".. string.format("%.3f", kart_zv),"white","bottomleft")

	-- gui.text(360,17, "XYv " .. string.format("%.3f", XYspeed) .. " km/h","white","bottomleft")
	-- gui.text(360,2, "XYZv " .. string.format("%.3f", round(XYZspeed)) .. " km/h","white","topleft")

	-- gui.text(530,17, "sin " .. string.format("%.9f", k_sin),"white","bottomleft")
	-- gui.text(530,2, "cos " .. string.format("%.9f", k_cos),"white","bottomleft")
end

function handle_form()
	-- handle form options
	if not forms.ischecked(hide_net) then
		show_network()
	end

	if not forms.ischecked(hide_xyz_data) then
		display_info()
	end
end

function load_map()
	the_course = {}
	-- TODO find where the collision addresses end for each track
	for addr = course.col_start, course.col_start + 0x9000, course.col_step do
		local section = {}
		section.p1 = {}
		section.p2 = {}
		section.p3 = {}
		local the_attribute = mainmemory.read_s16_be(
			addr + course.col_attr_offset
			)
		if the_attribute == course.tr_attr then
			section.attribute = the_attribute

			local p1_addr = mainmemory.read_s24_be(addr + course.p1_offset)
			section.p1.x = mainmemory.read_s16_be(p1_addr)
			section.p1.y = mainmemory.read_s16_be(p1_addr + 0x2)
			section.p1.z = mainmemory.read_s16_be(p1_addr + 0x4)

			local p2_addr = mainmemory.read_s24_be(addr + course.p2_offset)
			section.p2.x = mainmemory.read_s16_be(p2_addr)
			section.p2.y = mainmemory.read_s16_be(p2_addr + 0x2)
			section.p2.z = mainmemory.read_s16_be(p2_addr + 0x4)

			local p3_addr = mainmemory.read_s24_be(addr + course.p3_offset)
			section.p3.x = mainmemory.read_s16_be(p3_addr)
			section.p3.y = mainmemory.read_s16_be(p3_addr + 0x2)
			section.p3.z = mainmemory.read_s16_be(p3_addr + 0x4)

			the_course[#the_course + 1] = section
		end
	end
end

function initialize_population()
	pop = new_pop()

	for i = 1, pop.size do
		local genome = new_genome()
		mutate(genome)
		speciate(genome)
	end

	refresh()
	next_genome(false)
	initialize_run(false)
	create_backup("backup_"..state_file.."_gen_0.txt")
end

function new_pop()
	local pop = {}
	pop.size = population
	pop.species = {}
	pop.generation = 1
	pop.current_species = 1
	pop.current_genome = 0
	pop.member = 0
	pop.frame_count = -1
	return pop
end

function new_species()
	local species = {}
	species.genomes = {}
	species.fitness = 0
	species.last_fitness = 0
	species.improvement_age = 0
	return species
end

function new_genome()
	local genome = {}
	-- gonna try to do this with just one genes table,
	-- instead of nodes and connections
	genome.genes = {}
	-- keep track of how many nodes are in the network (not including outputs)
	genome.num_neurons = num_inputs
	genome.fitness = 0
	genome.shared_fitness = 0
	genome.network = {}
	genome.received_trial = false
	return genome
end

function new_gene(the_in, the_out, the_weight, the_enable, the_innovation)
	local gene = {}
	gene.in_node = the_in
	gene.out_node = the_out
	gene.weight = the_weight
	gene.enable = the_enable
	gene.innovation = the_innovation
	return gene
end

function copy_all_species()
	local species_copy = {}
	for s = 1, #pop.species do
		local species = new_species()
		for g = 1, #pop.species[s].genomes do
			table.insert(
				species.genomes, copy_genome(pop.species[s].genomes[g])
				)
		end
		table.insert(species_copy, species)
	end
	return species_copy
end

function copy_genome(genome)
	g2 = new_genome()
	for g = 1, #genome.genes do
		table.insert(g2.genes, copy_gene(genome.genes[g]))
	end
	g2.num_neurons = genome.num_neurons
	g2.fitness = genome.fitness
	g2.shared_fitness = genome.shared_fitness
	g2.received_trial = genome.received_trial
	return g2
end

function copy_gene(gene)
	local g2 = {}
	g2.in_node = gene.in_node
	g2.out_node = gene.out_node
	g2.weight = gene.weight
	g2.enable = gene.enable
	g2.innovation = gene.innovation
	return g2
end

function new_neuron(genome)
	genome.num_neurons = genome.num_neurons + 1
	return genome.num_neurons
end

function new_innovation(n1, n2)
	-- TODO check for existing innovation
	global_innovation = global_innovation + 1
	-- pn(global_innovation)
	return global_innovation
end

function mutate(genome)
	-- can mutate each weight
	if math.random() < mutate_weights_chance then
		mutate_weights(genome)
	end

	-- structure mutations
	-- can add connection
	-- if math.random() <= mutate_structure_chance then
	for i = 1, math.random(1, 2) do
		add_connection(genome)
	end
	-- end

	-- can add node
	if math.random() < add_node_chance then
		for i = 1, math.random(1,1) do
			add_node(genome)
		end
	end

	-- enable / disable
	if math.random() < 0.5 then
		mutate_enable(genome)
	end
end

function mutate_weights(genome)
	-- mutate the weights
	for i = 1, #genome.genes do
		local n = math.random(-1,1)
		while n == 0 do
			n = math.random(-1,1)
		end
		genome.genes[i].weight = (
			genome.genes[i].weight + n * math.random() * weight_perturbation
			)
	end
end

function add_connection(genome)
	-- add a new connection gene with a random weight

	-- find two random neurons to connect, only one can be an input,
	-- and they cannot be connected already
	local n1
	local n2
	n1, n2 = two_random_neurons(genome)

	-- generate a new gene with random weight, and in and out neurons
	-- TODO check for existing innovation
	local new_gene = new_gene(
		n1, n2, -- in_node, out_node
		math.random(), -- weight
		true, -- enable bit
		new_innovation(n1, n2)
		)

	for k, gene in pairs(genome.genes) do
		if (gene.in_node == new_gene.in_node and
			gene.out_node == new_gene.out_node) then
			return  -- TODO make this work better so it just tries again
		end
	end

	table.insert(genome.genes, new_gene)
end

function add_node(genome)
	if #genome.genes == 0 then
		return
	end

	-- pick a connection to split
	local old_connection = genome.genes[math.random(1, #genome.genes)]
	old_connection.enable = false
	local n1 = old_connection.in_node
	local n2 = old_connection.out_node
	local new_node_id = new_neuron(genome)

	-- create two new connections
	local new_connection_1 = new_gene(n1, new_node_id,
		1.0,
		true,
		new_innovation(n1, new_node_id)
		)
	local new_connection_2 = new_gene(new_node_id, n2,
		old_connection.weight,
		true,
		new_innovation(new_node_id, n2)
		)
	genome.num_neurons = genome.num_neurons + 1
	table.insert(genome.genes, new_connection_1)
	table.insert(genome.genes, new_connection_2)
end

function mutate_enable(genome)
	for g = 1, #genome.genes do
		if math.random() < 0.3 then
			if genome.genes[g].enable then
				genome.genes[g].enable = false
			else
				genome.genes[g].enable = true
			end
		end
	end
end

function two_random_neurons(genome)
	local n1
	local n2

	local neurons = {}
	for i = 1, num_inputs do
		neurons[i] = true
	end
	for o = 1, num_buttons do
		neurons[max_nodes + o] = true
	end
	for i = 1, #genome.genes do
		if genome.genes[i].in_node > num_inputs then
			neurons[genome.genes[i].in_node] = true
		end
		if genome.genes[i].out_node > num_inputs then
			neurons[genome.genes[i].out_node] = true
		end
	end
	local num_neurons = 0
	for _,_ in pairs(neurons) do
		num_neurons = num_neurons + 1
	end

	-- choose if n2 will be output neuron or some other neuron
	if num_neurons - 9 > num_inputs then
		local excess_neurons = num_neurons - num_inputs - 9
		local rando = math.random()
		if rando < 0.33 then
			n1 = math.random(1, 144)
			n2 = math.random(145, 145 + excess_neurons)
		elseif rando > 0.33 and rando < 0.66 then
			n1 = math.random(145, 145 + excess_neurons)
			n2 = math.random(1000001, 1000009)
		else
			n1 = math.random(1, num_neurons - 9)
			n2 = math.random(1000001, 1000009)
		end
	else -- we don't have any other neurons but outputs
		n1 = math.random(1, num_neurons - 9)
		n2 = math.random(1000001, 1000009)
	end

	return n1, n2
end

function get_neurons(genome)
	local neurons = {}
	for i = 1, num_inputs do
		neurons[i] = true
	end
	for o = 1, num_buttons do
		neurons[max_nodes + o] = true
	end
	for i = 1, #genome.genes do
		if genome.genes[i].in_node > num_inputs then
			neurons[genome.genes[i].in_node] = true
		end
		if genome.genes[i].out_node > num_inputs then
			neurons[genome.genes[i].out_node] = true
		end
	end
	return neurons
end

function speciate(baby_genome)
	local matched_a_species = false
	local s = 0
	for i = 1, #pop.species do
		local genes1 = pop.species[i].genomes[1].genes
		local genes2 = baby_genome.genes
		if #genes2 < #genes1 then -- make sure the shorter genome is first
			genes1, genes2 = genes2, genes1
		end
		cd = compatibility_distance(genes1, genes2)
		if cd < compatibility_threshold then
			table.insert(pop.species[i].genomes, baby_genome)
			matched_a_species = true
			s = i
			break
		end
	end

	if not matched_a_species then
		local unique_species = new_species()
		table.insert(unique_species.genomes, baby_genome)
		table.insert(pop.species, unique_species)
	end
	return s
end

function compatibility_distance(genes1, genes2)
	local E = excess_genes(genes1, genes2)
	local D = disjoint_genes(genes1, genes2)
	local N = math.max(#genes1, #genes2)
	if N < 20 then N = 1 end
	local W = sum_of_weight_differences(genes1, genes2) / (
		#genes1 + #genes2 - E - D
		)
	return c1*E/N + c2*D/N + c3*W
end

function excess_genes(genes1, genes2)
	local excess = 0

	local highest_1 = 0
	for i = 1, #genes1 do
		if genes1[i].innovation > highest_1 then
			highest_1 = genes1[i].innovation
		end
	end

	local highest_2 = 0
	for i = 1, #genes2 do
		if genes2[i].innovation > highest_2 then
			highest_2 = genes2[i].innovation
		end
	end

	if highest_1 > highest_2 then
		for i = 1, #genes1 do
			if genes1[i].innovation > highest_2 then
				excess = excess + 1

			end
		end
	else
		for i = 1, #genes2 do
			if genes2[i].innovation > highest_1 then
				excess = excess + 1
			end
		end
	end

	return excess
end

function disjoint_genes(genes1, genes2)
	local disjoint = 0

	for i = 1, #genes1 do
		local found = false
		for j = 1, #genes2 do
			if genes1[i].innovation == genes2[j].innovation then
				found = true
				break
			end
		end
		if not found then
			disjoint = disjoint + 1
		end
	end

	for i = 1, #genes2 do
		local found = false
		for j = 1, #genes1 do
			if genes2[i].innovation == genes2[j].innovation then
				found = true
				break
			end
		end
		if not found then
			disjoint = disjoint + 1
		end
	end

	return disjoint
end

function sum_of_weight_differences(genes1, genes2)
	local sum_of_differences = 0

	for i = 1, #genes1 do
		for j = 1, #genes1 do
			if genes1[i].innovation == genes2[j].innovation then
				sum_of_differences = sum_of_differences + math.abs(
					genes1[i].weight - genes2[j].weight
					)
			end
		end
	end

	return sum_of_differences
end

function get_objects()
	objects = {}
	for addr = obj.addr, obj.stop, obj.step do
		local object = {}
		object.t = mainmemory.read_s16_be(addr) * 0.1

		-- hmmm
		if object.t > 1.1 and object.t < 1.3 then
			object.t = 1.2
		end
		if object.t > 0.5 and object.t < 0.7 then
			object.t = 0.6
		end
		if object.t > 0.6 and object.t < 0.8 then
			object.t = 0.7
		end
		if object.t > 1.8 and object.t < 2 then
			object.t = 1.9
		end

		-- make bad objects a negative value
		if object.t == 2.6 or object.t == 1.3 or
			object.t == 0.8 or object.t == 0.7 or
			object.t == 0.6 or object.t == 3.1 or
			object.t == 3.2 or object.t == 3 or
			object.t == 1.9 then
			object.t = -object.t
		end

		object.x = mainmemory.readfloat(addr + obj.x_offset, true)
		object.y = mainmemory.readfloat(addr + obj.y_offset, true)
		object.z = mainmemory.readfloat(addr + obj.z_offset, true)
		if object.x == object.x and object.t ~= 0 and
			object.t ~= 4.3 and in_box(object) then
			objects[#objects + 1] = object
		end
	end
end

function get_box()
	box = {}
	box.tl    = {}
	box.tr    = {}
	box.bl    = {}
	box.br    = {}
	box.tl.x  = 359 * k_cos + 180 * k_sin + kart_x
	box.tl.z  = 359 * k_sin - 180 * k_cos + kart_z
	box.bl.x  =  -1 * k_cos + 180 * k_sin + kart_x
	box.bl.z  =  -1 * k_sin - 180 * k_cos + kart_z
	box.br.x  =  -1 * k_cos - 180 * k_sin + kart_x
	box.br.z  =  -1 * k_sin + 180 * k_cos + kart_z
	box.tr.x  = 359 * k_cos - 180 * k_sin + kart_x
	box.tr.z  = 359 * k_sin + 180 * k_cos + kart_z
	-- gui.text(
	-- 	220,130, string.format("%.3f", box.tl.x) .. ",Y," .. string.format(
	-- 		"%.3f", box.tl.z
	-- 		),"white","topleft")
	-- gui.text(
	-- 	480,130, string.format("%.3f", box.tr.x) .. ",Y," .. string.format(
	-- 		"%.3f", box.tr.z),
	-- 	"white","topleft")
	-- gui.text(
	-- 	220,430, string.format("%.3f", box.bl.x) .. ",Y," .. string.format(
	-- 		"%.3f", box.bl.z),
	-- 	"white","topleft")
	-- gui.text(
	-- 	480,430, string.format("%.3f", box.br.x) .. ",Y," .. string.format(
	-- 		"%.3f", box.br.z),
	-- 	"white","topleft")
end

function in_box(o)
	-- top left to bottom left
	local a = -(box.bl.z - box.tl.z)
	local b = box.bl.x - box.tl.x
	local c = -(a * box.tl.x + b * box.tl.z)
	local b1 = s_sign(o, a, b, c) < 0
	-- pn(b1)

	-- bottom left to bottom right
	a = -(box.br.z - box.bl.z)
	b = box.br.x - box.bl.x
	c = -(a * box.bl.x + b * box.bl.z)
	local b2 = s_sign(o, a, b, c) < 0

	-- bottom right to top right
	a = -(box.tr.z - box.br.z)
	b = box.tr.x - box.br.x
	c = -(a * box.br.x + b * box.br.z)
	local b3 = s_sign(o, a, b, c) < 0

	-- top right to top left
	a = -(box.tl.z - box.tr.z)
	b = box.tl.x - box.tr.x
	c = -(a * box.tr.x + b * box.tr.z)
	local b4 = s_sign(o, a, b, c) < 0

	return ((b1 == b2) and (b2 == b3) and (b3 == b4))
end

function s_sign(o, a, b, c)
	return a * o.x + b * o.z + c
end

function get_tiles()
	tiles = {}
	for z = -165, 165, 30 do
		for x = 344, 13, -30 do
			local tile = {}
			tile.n = #tiles + 1
			tile.x = x * k_cos - z * k_sin + kart_x
			tile.z = x * k_sin + z * k_cos + kart_z
			tile.t = get_tile_attribute(tile.x, tile.z)
			tiles[#tiles + 1] = tile
		end
	end
end

function get_tile_attribute(x, z)
	-- could be an object, could be track, could be something else

	-- objects
	for i, o in ipairs(objects) do
		local o_box = get_o_box(o)
		if in_o_box(x, z, o_box) then
			return o.t
		end
	end

	-- track
	for i, section in ipairs(the_course) do
		if in_section(x, z, section) then
			return section.attribute
		end
	end

	-- something else
	return -1
end

function get_o_box(o)
	local o_box = {}
	o_box.tl = {}
	o_box.bl = {}
	o_box.br = {}
	o_box.tr = {}
	o_box.tl.x =  15 * k_cos + 15 * k_sin + o.x
	o_box.tl.z =  15 * k_sin - 15 * k_cos + o.z
	o_box.bl.x = -15 * k_cos + 15 * k_sin + o.x
	o_box.bl.z = -15 * k_sin - 15 * k_cos + o.z
	o_box.br.x = -15 * k_cos - 15 * k_sin + o.x
	o_box.br.z = -15 * k_sin + 15 * k_cos + o.z
	o_box.tr.x =  15 * k_cos - 15 * k_sin + o.x
	o_box.tr.z =  15 * k_sin + 15 * k_cos + o.z
	o_box.y = o.y
	return o_box
end

function in_o_box(x, z, o_box)
	local o = {}
	o.x = x
	o.z = z

	-- top left to bottom left
	local a = -(o_box.bl.z - o_box.tl.z)
	local b = o_box.bl.x - o_box.tl.x
	local c = -(a * o_box.tl.x + b * o_box.tl.z)
	local b1 = s_sign(o, a, b, c) < 0

	-- bottom left to bottom right
	a = -(o_box.br.z - o_box.bl.z)
	b = o_box.br.x - o_box.bl.x
	c = -(a * o_box.bl.x + b * o_box.bl.z)
	local b2 = s_sign(o, a, b, c) < 0

	-- bottom right to top right
	a = -(o_box.tr.z - o_box.br.z)
	b = o_box.tr.x - o_box.br.x
	c = -(a * o_box.br.x + b * o_box.br.z)
	local b3 = s_sign(o, a, b, c) < 0

	-- top right to top left
	a = -(o_box.tl.z - o_box.tr.z)
	b = o_box.tl.x - o_box.tr.x
	c = -(a * o_box.tr.x + b * o_box.tr.z)
	local b4 = s_sign(o, a, b, c) < 0

	local b5 = o_box.y > kart_y - 74 and o_box.y < kart_y + 74

	return ((b1 == b2) and (b2 == b3) and (b3 == b4)) and b5
end

function in_section(x, z, s)
	local b1 = t_sign(x, z, s.p1,   s.p2)    < 0
	local b2 = t_sign(x, z, s.p2,   s.p3)  < 0
	local b3 = t_sign(x, z, s.p3, s.p1)    < 0
	local b4 = s.p1.y   > kart_y - 74 and s.p1.y   < kart_y + 74
	local b5 = s.p2.y   > kart_y - 74 and s.p2.y   < kart_y + 74
	local b6 = s.p3.y > kart_y - 74 and s.p3.y < kart_y + 74
	local b7 = ((b1 == b2) and (b2 == b3)) and b4 and b5 and b6
	return b7
end

function t_sign(x, z, p2, p3)
	return (x - p3.x) * (p2.z - p3.z) - (p2.x - p3.x) * (z - p3.z)
end

function get_inputs()
	get_box()
	get_objects()
	get_tiles()
end

function get_outputs()
	get_inputs()
	local outputs = evaluate_network()
	return outputs
end

function basic_ai()
	-- this demonstrates how simple a solution can be
	-- an interesting this about this is that it will play out the same every
	-- single time. the game may seem random, but if the input to the game is
	-- always the same, then the game will always play exactly the same.
	-- the opponents will always drive in the same place, the same items will
	-- received from item boxes, etc.
	get_inputs()

	local outputs = {}
	local attr = course.track_attribute[course.number]

	clear_controller()
	outputs = controller

	local button_a = "P1 A"
	outputs[button_a] = true

	local button_z = "P1 Z"
	if pop.frame_count % 23 == 0 then
		outputs[button_z] = true
	else
		outputs[button_z] = false
	end

	local button_left = "P1 A Left"
	local button_right = "P1 A Right"

	if tiles[39].t == attr or tiles[31].t == attr then
		outputs[button_left] = true
	else
		outputs[button_left] = false
	end

	if tiles[99].t == attr or tiles[115].t == attr then
		outputs[button_right] = true
		outputs[button_left] = false
	else
		outputs[button_right] = false
	end

	return outputs
end

function show_network()
	local text_color = white
	local cell_border = black
	local cell_fill = white
	local line_color = back
	local object_color = black
	local track_cell_border = black
	local track_cell_fill = white

	local genome = {}
	if replay_best_genome then
		genome = copy_genome(global_best_genome)
	else
		genome = pop.species[pop.current_species].genomes[pop.current_genome]
	end

	-- aerial view
	-- scale is 6
	gui.drawBox( -- 60x60 box
		92-box_radius*5-1, 80-box_radius*5-1,
		92+box_radius*5+1, 80+box_radius*5+1,
		black, bblue)
	local i = 1
	for x = -box_radius, box_radius - 1 do -- the tiles
		for z = -box_radius, box_radius - 1 do
			cell_border = black
			if tiles[i].t == course.tr_attr then  -- track attribute
				cell_fill = white
			elseif tiles[i].t == -2.6  or tiles[i].t == -3.2 or
			tiles[i].t == -3.1 or tiles[i].t == -3
			or tiles[i].t == 1.9 then  -- tree, cactus
				cell_fill = green
			elseif tiles[i].t == 1.2 then  -- item box
				cell_fill = blue
			elseif tiles[i].t == -0.7 then  -- green shell
				cell_fill = sgreen
			elseif tiles[i].t == -0.8 then  -- red shell
				cell_fill = red
			elseif tiles[i].t == -1.3 then  -- fake item box
				cell_fill = fbox
			elseif tiles[i].t == -0.6 then  -- banana
				cell_fill = yellow
			elseif tiles[i].t > 0 then  -- unknown
				cell_fill = black
				pn('unknown object: '..tiles[i].t)
			else
				cell_border = none
				cell_fill = none
			end
			gui.drawBox(
				92+x*5, 80+z*5,
				92+x*5+5, 80+z*5+5,
				cell_border, cell_fill
				)
			i = i + 1
		end
	end

	local cells = {}
	local cell = {}

	local i = 1
	for x = -box_radius, box_radius - 1 do -- the tiles
		for y = -box_radius, box_radius - 1 do
			cell = {}
			cell.x = 95+x*5
			cell.y = 83+y*5
			cell.activated = false -- no activation needed on these nodes
			cells[i] = cell
			i = i + 1
		end
	end

	-- buttons
	gui.drawBox(275, 38, 315, 129, black, bblue)
	for o, b in ipairs(button_input_names) do
		cell = {}
		cell.x = 270
		cell.y = 34+10*o
		if controller[b] == true then
			cell.activated = true
			text_color = white
			cell_border = black
			cell_fill = white
		else
			cell.activated = false
			text_color = b_off
			cell_border = b_off
			cell_fill = back
		end
		cells[o + max_nodes] = cell
		gui.drawText(275, 26+10*o, button_actual_names[o], text_color, 9)
		gui.drawBox(268, 32+10*o, 272, 36+10*o, cell_border, cell_fill)

		-- this is for the basic AI
		-- if o == 5 and controller[b] == true then
		-- 	gui.drawLine(80, 49, 270, 84, blue)
		-- elseif o == 5 and controller[b] == false then
		-- 	gui.drawLine(80, 49, 270, 84, bblue)
		-- else
		-- 	gui.drawLine(80, 49, 270, 84, none)
		-- end

		-- if o == 6 and controller[b] == true then
		-- 	gui.drawLine(105, 49, 270, 92, blue)
		-- elseif o == 6 and controller[b] == false then
		-- 	gui.drawLine(105, 49, 270, 92, bblue)
		-- else
		-- 	gui.drawLine(105, 49, 270, 92, none)
		-- end
	end

	for n, node in pairs(current_network.nodes) do
		if n > num_inputs and n <= max_nodes then
			cell = {}
			cell.x = 160
			cell.y = 50
			if node.value > 1 then
				cell.activated = true
			else
				cell.activated = false
			end
			cells[n] = cell
		end
	end

	-- try to reset the x and y of the hidden nodes so the network looks nice
	-- this code closely follows sethbling's code
	for i = 1, 4 do
		for _, gene in pairs(genome.genes) do
			if gene.enable then
				local in_cell = cells[gene.in_node]
				local out_cell = cells[gene.out_node]
				if gene.in_node > num_inputs and
					gene.in_node <= max_nodes then
					in_cell.x = 0.75 * in_cell.x + 0.25 * out_cell.x
					if in_cell.x >= out_cell.x then
						in_cell.x = in_cell.x - 40
					end
					if in_cell.x < 150 then
						in_cell.x = 150
					end

					if in_cell.x > 260 then
						in_cell.x = 260
					end
					in_cell.y = 0.75*in_cell.y + 0.25*out_cell.y
				end
				if gene.out_node > num_inputs and
					gene.out_node <= max_nodes then
					out_cell.x = 0.25 * in_cell.x + 0.75 * out_cell.x
					if in_cell.x >= out_cell.x then
						out_cell.x = out_cell.x + 40
					end
					if out_cell.x < 150 then
						out_cell.x = 150
					end
					if out_cell.x > 260 then
						out_cell.x = 260
					end
					out_cell.y = 0.25 * in_cell.y + 0.75 * out_cell.y
				end
			end -- if enabled
		end -- for loop
	end -- do 4 times

	-- draw hidden nodes, and connections
	for i, cell in pairs(cells) do
		if i > num_inputs and i <= max_nodes then
			if cell.activated then
				cell_border = black
				cell_fill = white
			else
				cell_border = b_off
				cell_fill = back
			end
			gui.drawBox(
				cell.x - 2, cell.y - 2,
				cell.x + 2, cell.y + 2,
				cell_border, cell_fill
				)
		end
	end

	-- draw connections
	for _, gene in pairs(genome.genes) do
		if gene.enable then
			local in_cell = cells[gene.in_node]
			local out_cell = cells[gene.out_node]

			if out_cell.activated then
				line_color = red
			else
				line_color = bred
			end

			gui.drawLine(
				in_cell.x, in_cell.y,
				out_cell.x, out_cell.y,
				line_color
				)
		end
	end

	-- this is the kart
	gui.drawBox(90,106,94,110,none,player[character])
end

function save_here()
	local file_name = forms.gettext(load_save_backup)
	create_backup(file_name)
end

function load_generation()
	local file_name = forms.gettext(load_save_backup)
	load_backup(file_name)
end

function replay_best()
	save_here()
	load_generation()
	replay_best_genome = true
	initialize_run(true)
end

function create_backup(file_name)
	local file = assert(io.open(file_name, "w"))

	-- first save the best genome
	file:write(global_best_genome.fitness.." ")
	file:write(global_best_genome.num_neurons.." ")
	file:write(#global_best_genome.genes.. "\n")
	for g, gene in pairs(global_best_genome.genes) do
		file:write(gene.in_node.." ")
		file:write(gene.out_node.." ")
		file:write(gene.weight.." ")
		file:write(gene.innovation.." ")
		if gene.enable then
			file:write("1\n")
		else
			file:write("0\n")
		end
	end

	-- then save the entire population
	file:write(pop.generation.." ")
	file:write(global_max_fitness.." ")
	file:write(#pop.species.."\n")
	for s, species in pairs(pop.species) do
		file:write(species.fitness.." ")
		file:write(species.improvement_age.." ")
		file:write(#species.genomes.."\n")
		for g, genome in pairs(species.genomes) do
			file:write(genome.fitness.." ")
			file:write(genome.num_neurons.." ")
			if genome.received_trial then
				file:write("1 ")
			else
				file:write("0 ")
			end
			file:write(#genome.genes.."\n")
			for h, gene in pairs(genome.genes) do
				file:write(gene.in_node.." ")
				file:write(gene.out_node.." ")
				file:write(gene.weight.." ")
				file:write(gene.innovation.." ")
				if gene.enable then
					file:write("1\n")
				else
					file:write("0\n")
				end
			end
		end
	end

	file:close()
end

function load_backup(file_name)
	local file = assert(io.open(file_name, "r"))

	-- first read the best genome
	global_best_genome = {}
	global_best_genome = new_genome()
	local num_genes = 0
	global_best_genome.fitness,
	global_best_genome.num_neurons,
	num_genes = file:read("*number", "*number", "*number")
	for g = 1, num_genes do
		local genes = new_gene()
		table.insert(global_best_genome.genes, genes)
		genes.in_node,
		genes.out_node,
		genes.weight,
		genes.innovation,
		genes.enable = file:read(
			"*number", "*number", "*number", "*number", "*number"
			)
		if genes.enable == 1 then
			genes.enable = true
		else
			genes.enable = false
		end
	end

	-- then read the rest of the population
	pop = {}
	pop = new_pop()
	local num_species
	pop.generation,
	global_max_fitness,
	num_species = file:read("*number", "*number", "*number")
	for s = 1, num_species do
		local species = new_species()
		table.insert(pop.species, species)
		local num_genomes
		species.fitness,
		species.improvement_age,
		num_genomes = file:read("*number", "*number", "*number")
		for g = 1, num_genomes do
			local genome = new_genome()
			table.insert(species.genomes, genome)
			local received_trial
			genome.fitness,
			genome.num_neurons,
			received_trial,
			num_genes = file:read(
				"*number", "*number", "*number", "*number"
				)
			if received_trial == 1 then
				genome.received_trial = true
			else
				genome.received_trial = false
			end
			for h = 1, num_genes do
				local gene = new_gene()
				table.insert(genome.genes, gene)
				gene.in_node,
				gene.out_node,
				gene.weight,
				gene.innovation,
				gene.enable = file:read(
					"*number", "*number", "*number", "*number", "*number"
					)
				if gene.enable == 1 then
					gene.enable = true
				else
					gene.enable = false
				end
			end -- end gene loop
		end -- end genome loop
	end -- end species loop

	file:close()

	-- we need to advance to the genome we were on when we saved this file
	pop.current_species = 1
	pop.current_genome = 1
	pop.member = 1
	local received_trial = true
	while received_trial do
		next_genome(false)
		local species = pop.species[pop.current_species]
		local genome = species.genomes[pop.current_genome]
		received_trial = genome.received_trial
	end

	initialize_run(false)
end

function is_dead()
	local species = pop.species[pop.current_species]
	local genome = species.genomes[pop.current_genome]

	if distance < 1 then
		fitness = XYZspeed * 0.001
	elseif distance > highest_distance then
		highest_distance = distance
		gain = fitness - distance
		if gain > 0 then
			fitness = distance + gain + round(XYZspeed / 25)
		else
			fitness = distance + round(XYZspeed / 25)
		end
	end

	if fitness > highest_fitness then
		highest_fitness = fitness
		genome.fitness = round(highest_fitness)
		not_advanced = 20
	end

	if fitness > species.fitness then
		species.fitness = fitness
	end

	if fitness > global_max_fitness then
		global_max_fitness = highest_fitness
	end

	 -- cap the frame count bonus
	if pop.frame_count < 1250 then
		advanced = pop.frame_count * 0.2
	else
		advanced = 250
	end
	not_advanced = not_advanced - 1

	if advanced + not_advanced <= 0 then
		genome.received_trial = true
		if replay_best_genome then
			replay_best_genome = false
			load_backup(state_file.."_frequent_backup.txt")
		end
		return true
	end

	return false
end

function clear_controller()
	controller = {}
	for i = 1, #button_input_names do
		local button = button_input_names[i]
		controller[button] = false
	end
end

function remove_under_performers()
	for s = 1, #old_pop.species do
		table.sort(old_pop.species[s].genomes, function(a,b)
			return (a.fitness > b.fitness)
		end)
		-- retain the top 20% to be parents
		local stop = round(#old_pop.species[s].genomes * 0.2)
		if stop < 2 then stop = 2 end
		for g = #old_pop.species[s].genomes, stop, -1 do
			table.remove(old_pop.species[s].genomes)
		end
	end
end

function remove_non_improvers()
	-- drop species that haven't received an improved fitness in 4 generations
	local not_killed_off = {}

	for s = 1, #old_pop.species do
		if old_pop.species[s].fitness > old_pop.species[s].last_fitness then
			old_pop.species[s].last_fitness = old_pop.species[s].fitness
			old_pop.species[s].improvement_age = 0
		else
			old_pop.species[
			s
			].improvement_age = old_pop.species[s].improvement_age + 1
		end

		if old_pop.species[s].improvement_age < 30 or
			old_pop.species[s].fitness >= global_max_fitness then
			table.insert(not_killed_off, old_pop.species[s])
		end
	end

	old_pop.species = not_killed_off
end

function adjust_fitnesses()
	for s = 1, #old_pop.species do
		for g = 1, #old_pop.species[s].genomes do
			local species = old_pop.species[s]
			local genome = species.genomes[g]
			genome.fitness = genome.fitness / #species.genomes
		end
	end
end

function calculate_average_fitness()
	local sum = 0
	for s = 1, #old_pop.species do
		for g = 1, #old_pop.species[s].genomes do
			sum = sum + old_pop.species[s].genomes[g].fitness
		end
	end
	return sum / population
end

function calculate_spawn_levels()
	local spawn_total = 0 -- debug
	local average_fitness = calculate_average_fitness()
	for s = 1, #old_pop.species do
		local species = old_pop.species[s]
		species.spawn_number = 0
		for g = 1, #old_pop.species[s].genomes do
			local genome = species.genomes[g]
			genome.spawn_number = genome.fitness / average_fitness
			species.spawn_number = species.spawn_number + genome.spawn_number
		end
		species.spawn_number = round(species.spawn_number)
		if species.spawn_number > 0 then
			pn('species spawn number: '..species.spawn_number) -- debug
		end
		spawn_total = spawn_total + species.spawn_number -- debug
	end
	pn('spawn total: '..spawn_total) -- debug
end

function contains_innovation(genes, i)
	for g = 1, #genes do
		if genes[g].innovation == i then return true end
	end
	return false
end

function cross_over(g1, g2)
	local baby = new_genome()
	for g = 1, #g1.genes do
		if not contains_innovation(baby.genes, g1.genes[g].innovation) then
			table.insert(baby.genes, copy_gene(g1.genes[g]))
		end
	end
	for g = 1, #g2.genes do
		if not contains_innovation(baby.genes, g2.genes[g].innovation) then
			table.insert(baby.genes, copy_gene(g2.genes[g]))
		end
	end
	baby.num_neurons = math.max(g1.num_neurons, g2.num_neurons)
	return baby
end

function current_pop_size()
	local size = 0
	for s = 1, #pop.species do
		for g = 1, #pop.species[s].genomes do
			size = size + 1
		end
	end
	return size
end

function make_babies()
	pop.species = {}

	-- put the best genome in the population without mutation
	speciate(global_best_genome)

	for s = 1, #old_pop.species do
		local chosen_best_yet = false
		local species = old_pop.species[s]

		while species.spawn_number >= 1 and current_pop_size() < population do

			if not chosen_best_yet then
				-- put the best genome in the new pop first for per
				-- species elitism (Ai book, page 395)
				local best_in_species = {}
				best_in_species.fitness = 0
				for i = 1, #species.genomes do
					if species.genomes[i].fitness > best_in_species.fitness then
						best_in_species = species.genomes[i]
					end
				end
				speciate(best_in_species)
				chosen_best_yet = true
			else
				-- only one genome in this species, so just mutate
				if #species.genomes == 1 then
					local baby = copy_genome(species.genomes[1])
					mutate(baby)
					baby.received_trial = false
					speciate(baby)
				-- enough genomes to have parents
				elseif #species.genomes > 1 then
					if math.random() < 0.8 then -- crossover chance
						local g1_i = math.random(1, #species.genomes)
						local g2_i = math.random(1, #species.genomes)
						local number_of_attempts = 5
						while g1_i == g2_i and number_of_attempts > 0 do
							g2_i = math.random(1, #species.genomes)
							number_of_attempts = number_of_attempts - 1
						end
						local baby = {}
						if g1_i ~= g2_i then
							local genome_1 = species.genomes[g1_i]
							local genome_2 = species.genomes[g2_i]
							baby = cross_over(genome_1, genome_2)
							mutate(baby)
							speciate(baby)
						else
							baby = species.genomes[g1_i]
							mutate(baby)
							speciate(baby)
						end -- if we have two different genomes
					end -- crossover chance
				end -- if 1 or more
			end -- if chosen best
			species.spawn_number = species.spawn_number - 1
		end -- end while
	end -- next species

	-- make sure the population is full
	-- TODO force this to choose from genomes that have a fitness above zero
	local i = 0
	while current_pop_size() < population do
		i = i + 1
		local k = 10
		local r_species
		local r_genome
		local winner = new_genome()
		winner.fitness = 0
		for j = 1, k do
			r_species = old_pop.species[math.random(1, #old_pop.species)]
			r_genome = copy_genome(
				r_species.genomes[math.random(1, #r_species.genomes)]
				)
			if r_genome.fitness > winner.fitness then
				winner = r_genome
			end
		end
		mutate(winner)
		winner.received_trial = false
		speciate(winner)
	end
	if i > 0 then pn("fill: "..i) end
end

function new_generation()
	-- pn('new_generation()')
	create_backup("backup_"..state_file.."_gen_"..pop.generation..".txt")

	math.randomseed(os.time())

	pop.current_species = 1
	pop.member = 1
	pop.generation = pop.generation + 1

	old_pop = {}
	old_pop.species = copy_all_species(pop.species)

	remove_under_performers()
	remove_non_improvers()
	adjust_fitnesses()
	calculate_spawn_levels()
	make_babies()
end

function next_genome(check_for_best)
	local genome = pop.species[pop.current_species].genomes[pop.current_genome]

	if check_for_best and genome.fitness > global_best_genome.fitness then
		global_best_genome = {}
		-- global_best_genome = new_genome()
		global_best_genome = copy_genome(genome)
		create_backup( -- the save button is kind of obsolete when this is here
			state_file.."_"..pop.generation.."_"..pop.current_species.."_"
			..pop.current_genome.."_best.txt"
			)
	end

	if pop.generation > 1 and pop.member % 5 == 0 then
		create_backup(state_file.."_frequent_backup.txt")
	end

	-- go to next genome, species, or generation
	pop.member = pop.member + 1
	pop.current_genome = pop.current_genome + 1
	if pop.current_genome > #pop.species[pop.current_species].genomes then
		pop.current_genome = 1
		pop.current_species = pop.current_species + 1
		if pop.current_species > #pop.species then
			new_generation()
		end
	end
end

function initialize_run(best_run)
	savestate.load(state_file)
	highest_fitness = 0
	highest_distance = 0
	fitness = 0
	gain = 0
	pop.frame_count = -1
	advanced = 0
	not_advanced = 20
	clear_controller()
	if best_run then
		current_network = create_network(global_best_genome)
	else
		current_network = create_network(
			pop.species[pop.current_species].genomes[pop.current_genome]
			)
	end
	controller = get_outputs()
end

function new_node()
	local node = {}
	node.connection_genes = {}
	node.value = 0.0
	return node
end

function create_network(genome)
	local network = {}
	network.nodes = {}

	-- create input nodes
	for i = 1, num_inputs do
		network.nodes[i] = new_node()
	end

	-- create output nodes
	for o = 1, num_buttons do
		network.nodes[max_nodes + o] = new_node()
	end
	-- console.clear()
	table.sort(genome.genes, function (a,b)
		return a.out_node < b.out_node
		end)
	-- create nodes from genes, if they don't exist yet
	for g = 1, #genome.genes do
		local gene = genome.genes[g]
		-- pn(gene)
		if gene.enable then
			-- does this gene's out node exist?
			if network.nodes[gene.out_node] == nil then
				network.nodes[gene.out_node] = new_node()
			end
			-- put this gene into it's out_node's connection_genes table so we
			-- know the weights and the values
			local node = network.nodes[gene.out_node]
			table.insert(node.connection_genes, gene)
			-- does this gene's in_node exist?
			if network.nodes[gene.in_node] == nil then
				network.nodes[gene.in_node] = new_node()
			end
		end
	end

	return network
end

function sigmoid(x)
	return 2 / (1 + math.exp(-x))
end

function evaluate_network()
	local outputs = {}

	for i = 1, num_inputs - 1 do
		current_network.nodes[i].value = tiles[i].t
	end

	for i, node in pairs(current_network.nodes) do
		local sum = 0
		for g = 1, #node.connection_genes do
			local connection = node.connection_genes[g]
			local in_value = current_network.nodes[connection.in_node].value
			sum = sum + connection.weight * in_value
		end

		if #node.connection_genes > 0 then
			node.value = sigmoid(sum)
		end
	end

	for o = 1, num_buttons do
		local button = button_input_names[o]
		if current_network.nodes[max_nodes + o].value > 1 then
			outputs[button] = true
		else
			outputs[button] = false
		end
	end

	return outputs
end

function refresh()
	-- http://tinyclouds.org/rant.html (warning: profanity)
	-- because he criticizes those who space things out nicely
	-- i don't agree with him entirely, but at least it is kind of funny
	pop.frame_count = pop.frame_count + 1

	distance = mainmemory.read_s16_be(dist_addr)

	k_sin    = mainmemory.readfloat(kart.sin,     true)
	k_cos    = mainmemory.readfloat(kart.cos,     true)

	kart_x   = mainmemory.readfloat(kart.x_addr,  true)
	kart_xv  = mainmemory.readfloat(kart.xv_addr, true) * 12
	kart_y   = mainmemory.readfloat(kart.y_addr,  true)
	kart_yv  = mainmemory.readfloat(kart.yv_addr, true) * 12
	kart_z   = mainmemory.readfloat(kart.z_addr,  true)
	kart_zv  = mainmemory.readfloat(kart.zv_addr, true) * 12

	XYspeed  = math.sqrt (kart_xv^2+kart_yv^2)
	XYZspeed = math.sqrt (kart_xv^2+kart_yv^2+kart_zv^2)
end


-------------------------------------------------------------------------------
-------------------------------------    --------------------------------------
-------------------------------------------------------------------------------
event.onexit(game_over)
game = gameinfo.getromname()
right_game = "Mario Kart 64 (USA)" == game

if right_game then
	initialize_things()
	create_form()
else
	pn("wrong game")
end

while right_game do
	refresh()

	if pop.frame_count % 5 == 0 then
		clear_controller()
		-- controller = basic_ai()
		controller = get_outputs()
	end

	handle_form()
	joypad.set(controller)

	if is_dead() then
		next_genome(true)
		initialize_run(false)
	end

	emu.frameadvance()
end
Download .txt
gitextract_w0r8ezht/

├── README.md
└── neat.lua
Condensed preview — 2 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (53K chars).
[
  {
    "path": "README.md",
    "chars": 829,
    "preview": "# Mario Kart 64 - NEAT Algorithm\n\nThis is an implementation of the NEAT algorithm in Lua for Mario Kart 64 and the BizHa"
  },
  {
    "path": "neat.lua",
    "chars": 46661,
    "preview": "--===========================================================================--\n---------------------------NEAT algorith"
  }
]

About this extraction

This page contains the full source code of the nicknlsn/MarioKart64NEAT GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 2 files (46.4 KB), approximately 15.1k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!