TASVideos

Tool-assisted game movies
When human skills are just not enough

Bisqwit / Lua Functions

Lua functions

This page collects useful Lua functions for use in game bots.

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.


Combined RSS Feed
Bisqwit/LuaFunctions last edited by Bisqwit on 2008-10-07 12:07:57
Page info and history | Latest diff | List referrers | View Source