Lua functions
This page collects useful Lua functions for use in game bots.
Table of contents
function explain_input()
----
-- This converts the sequence of input into a string for printing.
-- Not specific to any particular game.
-- The input is assumed to be an array of joypad input objects.
-- It aims to express the input in minimal readable length.
-- For example, the result "3*R, 4*R_A, 10*R"
-- means "hold right for 3 frames,
-- then hold right+A for 4 frames,
-- then hold right for 10 frames".
-----
keynames={A="A",B="B",select="SE",start="ST",
up="U",down="D",left="L",right="R"}
local function explain_input(input)
local inputdesc=""
local lastinput={}
local inpcount=0
local function inpkeys(keys)
local msg=""
local num_keys=0
local p=function(s)
if(num_keys>0) then msg=msg.."_" end
msg=msg..s
num_keys=num_keys+1
end
for member,name in pairs(keynames) do if(keys[member]) then p(name) end end
if(num_keys==0) then p("00") end
return msg
end
local function inpflush()
if(inpcount > 0)then
local msg = inpcount.."*"..lastinput
if(inputdesc ~= "") then inputdesc = inputdesc..", " end
inputdesc = inputdesc..msg
end
end
for i,v in ipairs(input) do
local k = inpkeys(v)
if(k ~= lastinput) then inpflush(); inpcount=0; lastinput=k end
inpcount = inpcount+1
end
inpflush()
if(inputdesc == "") then inputdesc = "<no input>" end
return inputdesc
end
function table.clone()
Tables in Lua are assigned by reference. Use this function if you need to make a copy of a table that you can change without affecting the original.
table.clone = function(table)
local res = {}
for k,v in pairs(table)do
res[k]=v
end
return res
end
class InputSimplifier
----
-- This simplifies an input sequence
-- Not specific to any particular game.
-- Inputs generated by random algorithms are highly... random.
-- This function can be used to refine the input by removing as many
-- unnecessary keypresses as possible.
-----
InputSimplifier = {
new=function(self,inputhistory,strat)
local res = {history=inputhistory,pos=1,attempt={},done=0,
resetpoint=1,strategy=strat,anysuccess=0
}
return setmetatable(res, self.mt)
end,
reset=function(self,attemptno)
self.attempt = table.clone(self.history)
local counter = 0
-- First, try removing redundant frames
for frameno,data in pairs(self.attempt)do
counter = counter+1
if counter == self.pos then
table.remove(self.attempt, frameno)
table.insert(self.attempt, self.attempt[#self.attempt])
if explain_input(self.attempt) ~= explain_input(self.history) then
self.resetpoint = counter
return
end
self.pos = self.pos + 1
end
end
if self.strategy == "full" then
-- Next, try removing extra input
for frameno,data in pairs(self.attempt)do
self.attempt[frameno] = table.clone(self.attempt[frameno])
-- Try removing some key
for member,name in pairs(keynames)do
counter = counter+1
if counter == self.pos then
if data[member] then
self.attempt[frameno][member] = nil
self.resetpoint = counter
return
end
self.pos = self.pos + 1
end
end
-- -- Try copying the previous keys
-- -- Note: Disabled. This can cause an infinite loop,
-- -- where input is first removed, then copied
-- -- from the previous frame, then removed again, etc.
-- counter = counter+1
-- if counter == self.pos then
-- if (frameno > 1)
-- and (explain_input(data) ~= explain_input(self.attempt[frameno-1])) then
-- self.attempt[frameno] = table.clone(self.attempt[frameno-1])
-- self.resetpoint = counter
-- return
-- end
-- self.pos = self.pos + 1
-- end
end
end
-- We've tried everything now.
if(self.resetpoint > 1)and(self.anysuccess > 0)then
-- The last reset was a partial one.
-- Do one more full pass in case we missed something
self.resetpoint = 1
self.pos = 1
self.anysuccess = 0
return self:reset(attemptno)
end
self.done = 1
end,
generate=function(self,frameno)
if(frameno >= #self.attempt) then return {} end
return self.attempt[frameno]
end,
are_you_done = function(self)
return self.done > 0
end,
you_failed = function(self)
-- Ignore the latest attempt
self.pos = self.pos + 1
end,
you_rule = function(self)
-- Our attempt worked, go and find a new thing to change!
self.history = self.attempt
self.pos = self.resetpoint
self.anysuccess = 1
end,
get_result = function(self)
return self.history
end,
explain_progress = function(self)
local counter = 0
for frameno,data in pairs(self.attempt)do
counter = counter+1
end
if self.strategy == "full" then
for frameno,data in pairs(self.attempt)do
for member,name in pairs(keynames)do
counter = counter+1
end
counter = counter+1
end
end
return self.pos.."/"..counter
end
}
InputSimplifier.mt={__index=InputSimplifier}
function SimpleBotLoop()
A simple engine for creating input using an abstract gamestate
object and an abstract input generator object, and callback
functions for fails and successes.
----
-- Main program
-----
local function SimpleBotLoop(
state, -- This object tells whether we've achieved a goal
input, -- This object generates input for us
has_ended_func, -- This function tells whether we should stop
when_fail_func, -- This function reports the caller that we failed
when_succ_func) -- This function reports the caller that we succeeded
local attempts, state_actions = 0,
{[-1]=when_fail_func, -- when getstate() returns -1, report failure
[0]=function(i)end, -- when getstate() returns 0, carry on
[1]=when_succ_func} -- when getstate() returns +1, report success
while(has_ended_func(attempts) == false) do
state:reset()
input:reset(attempts)
local inputhistory = {}
repeat
local inp = input:generate(state.frame+1) -- +1 so we get 1..n for frameno
table.insert(inputhistory, inp)
joypad.set(1, inp)
FCEU.frameadvance()
local s = state:getstate()
state_actions[s](inputhistory)
until s ~= 0
attempts=attempts+1
end
end
Example use of InputGenerator
Example of using SimpleBotLoop to find the best input sequence.
First, some bookkeeping. The Bestness object is
responsible of keeping track of the best attempt.
state = Gamestate:new()
local Bestness = {
bestattempt=9999,
bestsave =savestate.create(9), -- Best attempt will be saved to slot 9
bestinput ={},
save = function(self)
savestate.save(self.bestsave)
savestate.persist(self.bestsave)
end,
is_success = function(self,inputhistory)
if state.frame < self.bestattempt then
self.bestattempt = state.frame
self.bestinput = table.clone(inputhistory)
maxframe = state.frame
self:save()
return true
end
return false
end
}
FCEU.speedmode("maximum")
The actual game loop:
-- Find great inputs randomly
SimpleBotLoop(state, InputGenerator:new(),
function(attempts)
return attempts >= 25000
end,
function(inputhistory)
FCEU.message("Fail in "..state.frame.." frames: "..explain_input(inputhistory))
end,
function(inputhistory)
if Bestness:is_success(inputhistory) then
FCEU.message("\aSuccess in "..state.frame.." frames")
local inputdesc = explain_input(inputhistory)
FCEU.message(inputdesc)
end
end
)
Example use of InputSimplifier
Example of using InputSimplifier and SimpleBotLoop to simplify a sequence of input.
(The candidate input is in Bestness.bestinput and it is assumed that that input verbatim will produce a success response from the state object):
local simplifier = InputSimplifier:new(Bestness.bestinput, "full")
SimpleBotLoop(state, simplifier,
function(attempts)
return simplifier:are_you_done()
end,
function(inputhistory)
-- If this simplification caused the solution no longer
-- to work, inform the simplifier so it won't keep the
-- failed input.
FCEU.message("ack "..state.frame.." "..simplifier:explain_progress())
simplifier:you_failed()
end,
function(inputhistory)
FCEU.message("cool "..state.frame.."\n"..explain_input(inputhistory))
simplifier:you_rule()
-- Be prepared for the situation that the simplification
-- yielded an even faster completion time
Bestness:is_success() -- the return value is not relevant.
end
)
Example implementation of class InputGenerator
Example of a random input generator:
----
-- This generates random input
-----
InputGenerator = {
new=function(self)
local res = {lrstate=0,roundno=0}
return setmetatable(res, self.mt)
end,
reset=function(self,attemptno)
self.nextchange = 0
self.roundno = self.roundno + 1
end,
generate=function(self,frameno)
if self.nextchange == 0 then
self.nextchange = math.random(1,12)
self.lrstate = math.random(0,31)
end
self.nextchange = self.nextchange - 1
local inp = {}
for button,bitmask in pairs({left=1, right=2, up=4, A=8, B=16}) do
if(AND(self.lrstate, bitmask) > 0) then inp[button]=1 end
end
return inp
end
}
InputGenerator.mt={__index=InputGenerator}
Example implementation of class Gamestate
----
-- This controls the interface between the bot and the game.
-- Specific to Yie Ar Kung-fu.
-----
function generateobject(code) return setmetatable({}, {__index=code}) end
function memoize(obj,name,f) obj[name]=f; return f end
hpreader = generateobject(function(self,name)
local addr=({enemy=0x331, own=0x340})[name]
return memoize(self,name,
function() return
-- This pattern recurses an unnamed function.
(function(a,b,f) return f(a,b,f) end)(0,0,
function(n,v,f)
return v+(n>8 and 0 or
f(n+1, (memory.readbyte(addr+n) ~= 0xEF and 1 or 0),f))
end) end)
-- Yes, I'm aware of that the above could have been made with a for-loop as well.
end)
Gamestate = {
-- Construct a game state object
new=function(self)
local res = {ohp=hpreader.own(),ehp=hpreader.enemy(),
frame=0,anchor=savestate.create()}
savestate.save(res.anchor)
return setmetatable(res, self.mt)
end,
-- Begin a new attempt
reset=function(self)
savestate.load(self.anchor)
self.frame = 0
end,
-- Check what happened this frame (called after frame advance)
-- Return values: 0=ok, 1=success, -1=failure
getstate=function(self)
self.frame = self.frame+1
if(self.ohp > hpreader.own()) then return -1 end -- failed, took damage
if(self.ehp > hpreader.enemy())then return 1 end -- success, did damage
if(self.frame >= maxframe) then return -1 end -- failed, too much time
return 0
end
}
Gamestate.mt={__index=Gamestate}
Multiplayer LUA bot framework
This framework uses a centralized server for multiplayer LUA games.
-- In Debian GNU/Linux, install these libraries to use:
-- - liblua5.1-soap-dev
-- - liblua5.1-bit-dev
--
require "soap/http"
require "bit"
local buttonmap =
{A=1,B=2,select=4,start=8, up=16,down=32,left=64,right=128,
C=256,X=512,Y=1024,L=2048,R=4096, L1=8192,L2=16384,L3=32768,
R1=65536,R2=131072,R3=262144,Z=524288}
function soapcall(fname, params)
local p = { tag="nums" }
for param,value in pairs(params) do
table.insert(p, {tag=param, value})
end
local ns,meth,ent = soap.http.call(
"http://bisqwit.iki.fi/utils/lua_server.php",
"urn:TasvideosLUAserverQuery",
fname, {p})
return ent
end
Server = {
register = function(gameno,ctrlno,password)
return Server.decode(soapcall("register", {gameno=gameno, ctrlno=ctrlno, password=password})[1])
end,
submit = function(gameno,ctrlno,password,ctrl)
local ctrl_mod = 0x00
for button,bitmask in pairs(buttonmap) do
if(ctrl[button]) then ctrl_mod = ctrl_mod + bitmask end
end
return Server.decode(soapcall("submit", {gameno=gameno, ctrlno=ctrlno, password=password, ctrl=ctrl_mod})[1])
end,
getframe = function(gameno,ctrlno,password)
local response = Server.decode(soapcall("getframe", {gameno=gameno, ctrlno=ctrlno, password=password})[1])
if(type(response) == 'table') then
for ctrlno,bitset in pairs(response) do
local keymap = {}
for button,bitmask in pairs(buttonmap) do
if(bit.band(bitset,bitmask)~=0)then keymap[button]=1 end
end
response[ctrlno]=keymap
end
end
return response
end,
decode = function(soapresponse)
-- print_r(soapresponse)
if soapresponse.attr['xsi:type'] == 'SOAP-ENC:Array' then
local response = {}
for i, elem in ipairs(soapresponse) do
response[i] = Server.decode(elem)
end
return response
elseif soapresponse.attr['xsi:type'] == 'ns2:Map' then
local response = {}
for i, elem in ipairs(soapresponse) do
response[Server.decode(elem[1])] = Server.decode(elem[2])
end
return response
elseif soapresponse.attr['xsi:type'] == 'xsd:int' then
return soapresponse[1]+0
else
return soapresponse[1]
end
end
}
Example game loop (this is for FCEU, but for other emulators, it's pretty much the same):
local gameno = 0x1501096 -- identifies the game you're participating
local password = "g9uzs09qek" -- password for your robot in this game
local ctrlno = 1 -- which controller's input you are providing
if Server.register(gameno, ctrlno, password) ~= 0 then
print("Error registering to the server")
os.exit()
else
while(true)do
local joypadinput = {A=1}
Server.submit(gameno,ctrlno,password, joypadinput)
local inputs = Server.getframe(gameno,ctrlno,password)
if inputs == 0 then
print("Game over")
break
end
for a=1,4 do
joypad.set(a, inputs[a])
end
FCEU.frameadvance()
end
end
It uses a centralized game server at
bisqwit.iki.fi
.
A password is required for each game to prevent bots
from screwing up other players' games.