-- LuaBot, concept bot for FCEU and snes9x and any other emu that has compatible Lua engine
-- qFox, 2 August 2008
-- version 1.07

-- Botscript data following:
-- Rom: -- ROMNAME
-- Comment: -- COMMENT
-- Version: -- VERSION

local maxvalue = 100;    -- always "true", yes, pressed
local minvalue = 0;      -- always "false", no, released

-- the bot kind of works like this function. for every option, a value is requested from the programmer. 
-- if that value is higher then a random value 0-99, this function returns true, else false.
-- the idea is that keys are pressed based on a heuristic value. if you want absolute values, simply
-- return values like minvalue or maxvalue, or their syntactic equals yes/no and press/release (to make
-- the sourcecode easier to read). you can use the variable maybe for 50% chance.
-- this function also reduces a lot of calls to random() if the user uses absolute values.
local function rand_if(n)
	if (n <= minvalue) then return false; end;
	if (n >= maxvalue) then return true; end;
	if (n > math.random(minvalue, maxvalue-1)) then return true; end;
	return false;
end;

local loopcounter = 0;   -- counts the main loop
local key1 = {};         -- holds the to be pressed keys this frame for player 1
local key2 = {};         -- holds the to be pressed keys this frame for player 2
local key3 = {};         -- holds the to be pressed keys this frame for player 3
local key4 = {};         -- holds the to be pressed keys this frame for player 4
local lastkey1 = {};     -- keys pressed in previous frame for player 1
local lastkey2 = {};     -- keys pressed in previous frame for player 2
local lastkey3 = {};     -- keys pressed in previous frame for player 1
local lastkey4 = {};     -- keys pressed in previous frame for player 2
local frame = 0;         -- number of frames (current value is current frame count, incremented at the start of a new frame)
local attempt = 1;       -- number of attempts (current value is current attempt, incremented after the end of an attempt)
local segment = 1;       -- number of segments (current value is current segment, incremented after the end of a segment)
local okattempts = 0;    -- number of successfull attempts (including rollback)
local failattempts = 0;  -- number of failed attempts (including rollback)

local segments = {};     -- table that holds every segment, each segment is another table that consists of the score, ties, savestate (begin of segment), lastkeys, keys pressed, etc.
segments[1] = {};        -- initialize the first segment, we initialize the savestate right after the before code has ran

-- these dont have to be used, but it makes it easier to control here
local maxframes = 400;
local maxattempts = 200;
local maxsegments = 100;

local playingbest = false;    -- when going to the next segment, we need to play the best segment to record, this indicates when we're doing so
local keyrecording1 = {};     -- every key pressed for player 1 is put in here
local keyrecording2 = {};     -- every key pressed for player 2 is put in here
local keyrecording3 = {};     -- every key pressed for player 3 is put in here
local keyrecording4 = {};     -- every key pressed for player 4 is put in here

-- some constants/macro's/whatever to make source easier to read
local press = maxvalue;
local release = minvalue;
local yes = maxvalue;
local maybe = maxvalue/2; -- 50%
local no = minvalue;

-- static constants, will be used by the frontend later
local X = 95;
local Y = 30;
local Z = 0;
local P = 0;
local Q = 0;

local vars = {};              -- variable table. each cell holds a variable. variables are remembered accross segments

-- user defined functions

local function getScore()     -- score of current attempt
	local result = no;
	-- SCORE
	return result;
end;

local function getTie1()      -- tie breaker of current attempt in case score is equal
	local result = no;
	-- TIE1
	return result;
end;

local function getTie2()      -- second tie breaker
	local result = no;
	-- TIE2
	return result;
end;

local function getTie3()      -- third tie breaker
	local result = no;
	-- TIE3
	return result;
end;

local function getTie4()      -- fourth tie breaker
	local result = no;
	-- TIE4
	return result;
end;

local function isRunEnd()     -- gets called 3x! twice in the main loop (every frame). determines whether the bot should quit.
	local result = no;
	-- ISRUNEND
	return result;
end;

local function mustRollBack() -- drop back to previous segment? called at the end of a segment
	local result = no;
	-- MUSTROLLBACK
	return result;
end;

local function isSegmentEnd() -- end of current segment? (usually just x frames or being really stuck (to rollback))
	local result = no;
	-- ISSEGMENTEND
	return result;
end;

local function isAttemptOk()  -- is current run ok? like, did you die? (then the run is NOT ok... :). return no for no and yes for yes or be left by chance.
	local result = yes;
	-- ISATTEMPTOK
	return result;
end;

local function isAttemptEnd() -- end of current attempt? (like when you die or reach a goal)
	local result = no;
	-- ISATTEMPTEND
	return result;
end;

-- the next 2x8 functions determine whether a button should be pressed for player 1 and 2
-- return yes or no for absolute values, return anything between minvalue and maxvalue
-- to set a chance of pressing that button.

local function pressKeyA1()
	local result = no;
	-- bA1
	return result;
end;

local function pressKeyB1()
	local result = no;
	-- bB1
	return result;
end;

local function pressKeyStart1()
	local result = no;
	-- START1
	return result;
end;

local function pressKeySelect1()
	local result = no;
	-- SELECT1
	return result;
end;

local function pressKeyUp1()
	local result = no;
	-- UP1
	return result;
end;

local function pressKeyDown1()
	local result = no;
	-- DOWN1
	return result;
end;

local function pressKeyLeft1()
	local result = no;
	-- LEFT1
	return result;
end;

local function pressKeyRight1()
	local result = no;
	-- RIGHT1
	return result;
end;

local function pressKeyA2()
	local result = no;
	-- bA2
	return result;
end;

local function pressKeyB2()
	local result = no;
	-- bB2
	return result;
end;

local function pressKeyStart2()
	local result = no;
	-- START2
	return result;
end;

local function pressKeySelect2()
	local result = no;
	-- SELECT2
	return result;
end;

local function pressKeyUp2()
	local result = no;
	-- UP2
	return result;
end;

local function pressKeyDown2()
	local result = no;
	-- DOWN2
	return result;
end;

local function pressKeyLeft2()
	local result = no;
	-- LEFT2
	return result;
end;

local function pressKeyRight2()
	local result = no;
	-- RIGHT2
	return result;
end;

local function pressKeyA3()
	local result = no;
	-- bA3
	return result;
end;

local function pressKeyB3()
	local result = no;
	-- bB3
	return result;
end;

local function pressKeyStart3()
	local result = no;
	-- START3
	return result;
end;

local function pressKeySelect3()
	local result = no;
	-- SELECT3
	return result;
end;

local function pressKeyUp3()
	local result = no;
	-- UP3
	return result;
end;

local function pressKeyDown3()
	local result = no;
	-- DOWN3
	return result;
end;

local function pressKeyLeft3()
	local result = no;
	-- LEFT3
	return result;
end;

local function pressKeyRight3()
	local result = no;
	-- RIGHT3
	return result;
end;

local function pressKeyA4()
	local result = no;
	-- bA4
	return result;
end;

local function pressKeyB4()
	local result = no;
	-- bB4
	return result;
end;

local function pressKeyStart4()
	local result = no;
	-- START4
	return result;
end;

local function pressKeySelect4()
	local result = no;
	-- SELECT4
	return result;
end;

local function pressKeyUp4()
	local result = no;
	-- UP4
	return result;
end;

local function pressKeyDown4()
	local result = no;
	-- DOWN4
	return result;
end;

local function pressKeyLeft4()
	local result = no;
	-- LEFT4
	return result;
end;

local function pressKeyRight4()
	local result = no;
	-- RIGHT4
	return result;
end;

-- now follow the "events", one for the start and end of a frame, attempt, segment and whole bot. none of them need to return anything

local function onStart() -- this code should run before the bot starts, for instance to start the game from power on and get setup the game
	-- ONSTART
end;

local function onFinish() -- code ran after the bot finishes
	-- ONFINISH
end;

local function onSegmentStart() -- code ran after initializing a new segment, before onAttemptStart(). framecount is always one fewer then actual frame!
	-- ONSEGMENTSTART
end;

local function onSegmentEnd() -- code ran after a segment finishes, before cleanup of segment vars
	-- ONSEGMENTEND
end;

local function onAttemptStart() -- code ran after initalizing a new attempt, before onInputStart(). not ran when playing back. framecount is always one fewer then actual frame!
	-- ONATTEMPTSTART
end;

local function onAttemptEnd(wasOk) -- code ran after an attempt ends before cleanup code, argument is boolean true when attempt was ok, boolean false otherwise. not ran when playing back
	-- ONATTEMPTEND
end;

local function onInputStart() -- code ran prior to getting input (keys are empty). not ran when playing back
	-- ONINPUTSTART
end;

local function onInputEnd() -- code ran after getting input (lastkey are still valid) (last function before frame ends, you can still manipulate the input here!). not ran when playing back
	-- ONINPUTEND
end;

-- the bot starts here.. (nothing is added from the user from this point onwards)

onStart();                                              -- run this code first

segments[segment].savestate = savestate.create();       -- create anonymous savestate obj for start of first segment
savestate.save(segments[segment].savestate);            -- save current state to it, it will be reloaded at the start of each frame
local startkey1 = key1;                                 -- save the last key pressed in the onStart. serves as an anchor for the first segment
local startkey2 = key2;
local startkey3 = key3;                                 -- save the last key pressed in the onStart. serves as an anchor for the first segment
local startkey4 = key4;
local startvars = vars;                                 -- save the vars array (it might have been used by the onStart)
lastkey1 = key1;                                        -- to enter the loop...
lastkey2 = key2;
lastkey3 = key3;
lastkey4 = key4;
--FCEU.speedmode("maximum");                            -- uncomment this line to make the bot run faster ("normal","turbo","maximum")

onSegmentStart();
onAttemptStart();

collectgarbage();                                       -- just in case...

-- This will loops for each frame, at the end of the while
-- the frameadvance is called, causing it to advance
while (rand_if(isRunEnd())) do
	loopcounter = loopcounter + 1;   -- count the number of botloops
	--gui.text(200,10,loopcounter);  -- print it on the right side if you want to see the number of total frames
	
	if (not playingbest and rand_if(isAttemptEnd())) then -- load save state, continue with next attempt (disabled when playing back best)
		-- record this attempt as the last attempt
		if (not segments[segment].prev) then segments[segment].prev = {}; end;
		segments[segment].prev.frames = frame;
		segments[segment].prev.attempt = attempt;
		segments[segment].prev.score = getScore();
		segments[segment].prev.tie1 = getTie1();
		segments[segment].prev.tie2 = getTie2();
		segments[segment].prev.tie3 = getTie3();
		segments[segment].prev.tie4 = getTie4();
		segments[segment].prev.ok = rand_if(isAttemptOk()); -- this is the check whether this attempt was valid or not. if not, it cannot become the best attempt.
		-- update ok/failed attempt counters
		if (segments[segment].prev.ok) then 
			okattempts = okattempts + 1;
			onAttemptEnd(true);
		else 
			failattempts = failattempts + 1;
			onAttemptEnd(false);
		end;

		-- if this attempt was better then the previous one, replace it
		-- its a long IF, but all it checks (lazy eval) is whether the current
		-- score is better then the previous one, or if its equal and the tie1
		-- is better then the previous tie1 or if the tie1 is equal to the prev
		-- etc... for all four ties. Only tie4 actually needs to be better, tie1
		-- through tie3 can be equal as well, as long as the next tie breaks the
		-- same tie of the previous attempt :)
		if (segments[segment].prev.ok and (not segments[segment].best or (getScore() > segments[segment].best.score or (getScore() == segments[segment].best.score and (getTie1() > segments[segment].best.tie1 or (getTie1() == segments[segment].best.tie1 and (getTie1() > segments[segment].best.tie1 or (getTie1() == segments[segment].best.tie1 and (getTie1() > segments[segment].best.tie1 or (getTie1() == segments[segment].best.tie1 and getTie1() > segments[segment].best.tie1)))))))))) then
			-- previous attempt was better then current best (or no current best
			-- exists), so we (re)place it.
			if (not segments[segment].best) then segments[segment].best = {}; end;
			segments[segment].best.frames = segments[segment].prev.frames;
			segments[segment].best.attempt = segments[segment].prev.attempt;
			segments[segment].best.score = segments[segment].prev.score;
			segments[segment].best.tie1 = segments[segment].prev.tie1;
			segments[segment].best.tie2 = segments[segment].prev.tie2;
			segments[segment].best.tie3 = segments[segment].prev.tie3;
			segments[segment].best.tie4 = segments[segment].prev.tie4;
			segments[segment].best.keys1 = keyrecording1; -- backup the recorded keys
			segments[segment].best.keys2 = keyrecording2; -- backup the recorded keys player 2
			segments[segment].best.keys3 = keyrecording3; -- backup the recorded keys
			segments[segment].best.keys4 = keyrecording4; -- backup the recorded keys player 2
			segments[segment].best.lastkey1 = lastkey1; -- backup the lastkey
			segments[segment].best.lastkey2 = lastkey2; -- backup the lastkey
			segments[segment].best.lastkey3 = lastkey3; -- backup the lastkey
			segments[segment].best.lastkey4 = lastkey4; -- backup the lastkey
			segments[segment].best.vars = vars; -- backup the vars table
		end
		
		if (rand_if(isSegmentEnd())) then -- the current segment ends, replay the best attempt and continue from there onwards...
			onSegmentEnd();
			if (rand_if(mustRollBack())) then -- rollback to previous segment
				gui.text(50,50,"Rolling back to segment "..(segment-1));
				segments[segment] = nil; -- remove current segment data
				attempt = 0; -- will be incremented in a few lines to be 1
				segment = segment - 1;
				segments[segment].best = nil;
				segments[segment].prev = nil;
				collectgarbage(); -- collect the removed segment please
			else
				playingbest = true; -- this will start playing back the best attempt in this frame
			end;
		end;

		-- reset vars
		attempt = attempt + 1;
		frame = 0;
		keyrecording1 = {}; -- reset the recordings :)
		keyrecording2 = {};
		keyrecording3 = {};
		keyrecording4 = {};
		-- set lastkey to lastkey of previous segment (or start, if first segment)
		-- also set the vars table to the table of the previous segment
		if (segment == 1) then 
			lastkey1 = startkey1;
			lastkey2 = startkey2;
			lastkey3 = startkey3;
			lastkey4 = startkey4;
			vars = startvars;
		else 
			lastkey1 = segments[segment-1].best.lastkey1;
			lastkey2 = segments[segment-1].best.lastkey2;
			lastkey3 = segments[segment-1].best.lastkey3;
			lastkey4 = segments[segment-1].best.lastkey4;
			vars = segments[segment-1].best.vars;
		end;
		-- load the segment savestate to go back to the start of this segment
		if (segments[segment].savestate) then -- load segment savestate and try again :)
			savestate.load(segments[segment].savestate); 
		else
			fceu.crash(); -- this crashes because fceu is a nil table :) as long as gui.popup() doesnt work... we're crashing because no save state exists..? it should never happen.
		end;

		if (rand_if(isRunEnd())) then break; end; -- if end of run, break out of main loop and run in end loop.
		
		if (not playingbest) then onAttemptStart(); end; -- only call this when not playing back best attempt. has decreased frame counter!
	end; -- continues with (new) attempt

	-- increase framecounter _after_ processing attempt-end
	frame = frame + 1; -- inrease the frame count (++frame?)

	if (playingbest and segments[segment].best) then -- press keys from memory (if there are any)
		gui.text(10,150,"frame "..frame.." of "..segments[segment].best.frames);
		if (frame >= segments[segment].best.frames) then -- end of playback, start new segment
			playingbest = false;
			lastkey1 = segments[segment].best.lastkey1;
			lastkey2 = segments[segment].best.lastkey2;
			lastkey3 = segments[segment].best.lastkey3;
			lastkey4 = segments[segment].best.lastkey4;
			vars = segments[segment].best.vars;
			segment = segment + 1;
			segments[segment] = {};
			-- create a new savestate for the start of this segment
			segments[segment].savestate = savestate.create();
			savestate.save(segments[segment].savestate);
			-- reset vars
			frame = 0; -- onSegmentStart and onAttemptStart expect this to be one fewer...
			attempt = 1;

			keyrecording1 = {}; -- reset recordings :)
			keyrecording2 = {};
			keyrecording3 = {};
			keyrecording4 = {};
			-- after this, the next segment starts because playingbest is no longer true
			onSegmentStart();
			onAttemptStart();
			frame = 1; -- now set it to 1
		else
			key1 = segments[segment].best.keys1[frame]; -- fill keys with that of the best attempt
			key2 = segments[segment].best.keys2[frame];
			key3 = segments[segment].best.keys3[frame];
			key4 = segments[segment].best.keys4[frame];
			gui.text(10,10,"Playback best of segment "..segment.."\nFrame: "..frame);
		end;
	end;

	if (rand_if(isRunEnd())) then break; end; -- if end of run, break out of main loop
  -- note this is the middle, this is where an attempt or segment has ended if it would and started if it would!
  -- now comes the input part for this frame
	
	if (not playingbest) then -- when playing best, the keys have been filled above.
		-- press keys from bot
		gui.text(10,10,"Attempt: "..attempt.." / "..maxattempts.."\nFrame: "..frame.." / "..maxframes);
		if (segments[segment] and segments[segment].best and segments[segment].prev) then 
			gui.text(10,30,"Last score: "..segments[segment].prev.score.." ok="..okattempts..", fail="..failattempts.."\nBest score: "..segments[segment].best.score);
		elseif (segments[segment] and segments[segment].prev) then
			gui.text(10,30,"Last score: "..segments[segment].prev.score.."\nBest score: none, fails="..failattempts); 
		end;
		gui.text(10,50,"Segment: "..segment);
		
		key1 = {};
		key2 = {};
		key3 = {};
		key4 = {};

		onInputStart();

		-- player 1
		if (rand_if(pressKeyUp1()) )    then key1.up = 1; end;
		if (rand_if(pressKeyDown1()))   then key1.down = 1; end;
		if (rand_if(pressKeyLeft1()))   then key1.left = 1; end;
		if (rand_if(pressKeyRight1()))  then key1.right = 1; end;
		if (rand_if(pressKeyA1()))      then key1.A = 1; end;
		if (rand_if(pressKeyB1()))      then key1.B = 1; end;
		if (rand_if(pressKeySelect1())) then key1.select = 1; end;
		if (rand_if(pressKeyStart1()))  then key1.start = 1; end;

		-- player 2
		if (rand_if(pressKeyUp2()) )    then key2.up = 1; end;
		if (rand_if(pressKeyDown2()))   then key2.down = 1; end;
		if (rand_if(pressKeyLeft2()))   then key2.left = 1; end;
		if (rand_if(pressKeyRight2()))  then key2.right = 1; end;
		if (rand_if(pressKeyA2()))      then key2.A = 1; end;
		if (rand_if(pressKeyB2()))      then key2.B = 1; end;
		if (rand_if(pressKeySelect2())) then key2.select = 1; end;
		if (rand_if(pressKeyStart2()))  then key2.start = 1; end;

		-- player 3
		if (rand_if(pressKeyUp3()) )    then key3.up = 1; end;
		if (rand_if(pressKeyDown3()))   then key3.down = 1; end;
		if (rand_if(pressKeyLeft3()))   then key3.left = 1; end;
		if (rand_if(pressKeyRight3()))  then key3.right = 1; end;
		if (rand_if(pressKeyA3()))      then key3.A = 1; end;
		if (rand_if(pressKeyB3()))      then key3.B = 1; end;
		if (rand_if(pressKeySelect3())) then key3.select = 1; end;
		if (rand_if(pressKeyStart3()))  then key3.start = 1; end;

		-- player 2
		if (rand_if(pressKeyUp4()) )    then key4.up = 1; end;
		if (rand_if(pressKeyDown4()))   then key4.down = 1; end;
		if (rand_if(pressKeyLeft4()))   then key4.left = 1; end;
		if (rand_if(pressKeyRight4()))  then key4.right = 1; end;
		if (rand_if(pressKeyA4()))      then key4.A = 1; end;
		if (rand_if(pressKeyB4()))      then key4.B = 1; end;
		if (rand_if(pressKeySelect4())) then key4.select = 1; end;
		if (rand_if(pressKeyStart4()))  then key4.start = 1; end;

		onInputEnd();

		lastkey1 = key1;
		lastkey2 = key2;
		lastkey3 = key3;
		lastkey4 = key4;
	
		keyrecording1[frame] = key1; -- record these keys
		keyrecording2[frame] = key2; -- record these keys
		keyrecording3[frame] = key3; -- record these keys
		keyrecording4[frame] = key4; -- record these keys
	end;

	-- actually set the keys here.
	joypad.set(1, key1);
	joypad.set(2, key2);
	joypad.set(3, key3);
	joypad.set(4, key4);

	-- next frame
	FCEU.frameadvance();
end;

onFinish(); -- allow user cleanup before starting the final botloop

-- now enter an endless loop displaying the results of this run.
while (true) do 
	if (segments[segment].best) then gui.text(30,100,"end: max attempt ["..segment.."] had score = "..segments[segment].best.score);
	elseif (segment > 1 and segments[segment-1].best) then gui.text(30,100,"end: no best attempt ["..segment.."]\nPrevious best score: "..segments[segment-1].best.score);
	else gui.text(30,100,"end: no best attempt ["..segment.."] ..."); end;
	FCEU.frameadvance();
end;

-- i dont think it ever reaches this place... perhaps it should, or some event or whatever...
segments = nil;
collectgarbage(); -- collect the segment data... anything else is probably not worth it...
