SciQuest_csp

MCTS-AI, Mancala

Mar 23rd, 2012
227
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 8.50 KB | None | 0 0
  1. Mancala = class()
  2. -- This implements the sowing game called Kalah (Mancala in USA, see wikipedia).
  3. -- I believe having first move is a 5.5 handicap on a 6 bin, 4 stone board.
  4. -- (without this, first player always wins with perfect play)
  5.  
  6. -- the next two functions are self documenting.
  7.  function Mancala:setupParams()
  8.      iparameter("NumberOfBins", 1, 9)
  9.      iparameter("StonesPerBin", 1, 7)
  10.  
  11.      NumberOfBins = readProjectData("numberOfBins", 6)
  12.      StonesPerBin = readProjectData("stonesPerBin", 4)
  13.  end
  14.  
  15.  function Mancala:saveParams()
  16.      saveProjectData("numberOfBins", NumberOfBins)
  17.      saveProjectData("stonesPerBin", StonesPerBin)
  18.  end
  19.  
  20. -- creates an instance of the Mancala board
  21.  function Mancala:init()
  22.      self.player = 0
  23.      self.ai = 1
  24.      self.turn = AIstarts
  25.      selfgameWon = nil
  26.      
  27.      self.numBins = NumberOfBins
  28.      self.startingStones = StonesPerBin
  29.  
  30.      -- the way i chose to represent this causes some ugly code later, but works.
  31.      -- let me know if you have a better implementation!
  32.      self.bins = {}
  33.      self.playerMancala = self.numBins + 1
  34.      self.aiMancala = 2 * self.playerMancala
  35.      for i = 1, 2*self.numBins+2 do
  36.          table.insert(self.bins, i, self.startingStones)
  37.      end
  38.      self.bins[self.playerMancala] = 0
  39.      self.bins[self.aiMancala] = 0
  40.      
  41.      self.totalStones = self.numBins * self.startingStones * 2
  42.  end
  43.  
  44. -- copy function is used to update sim to reflect the board.
  45.  function Mancala:copy(original)
  46.      self.turn = original.turn
  47.      for i = 1, original.aiMancala do
  48.          self.bins[i] = original.bins[i]
  49.      end
  50.      self.gameWon = original.gameWon
  51.  end
  52.  
  53. -- straight forward, if inelegant.
  54.  function Mancala:draw()
  55.      local dx = WIDTH/self.numBins
  56.      local dy = 100
  57.      ellipseMode(CORNER)
  58.      pushMatrix()
  59.      fontSize(36)
  60.      -- specifically, this shouldve been two seperate loops.
  61.      -- im slow to change working code though.
  62.      for i = 0, self.numBins-1 do
  63.          fill(red)
  64.          ellipse(dx*i,0,dx)
  65.          fill(blue)
  66.          ellipse(WIDTH-dx*(i+1), HEIGHT - dx, dx)
  67.          fill(white)
  68.          text(self.bins[1+i], dx*i+dx/2, dx/2)
  69.          text(self.bins[1+i+self.playerMancala], WIDTH-dx*(i+1)+dx/2, HEIGHT -dx/2)
  70.          
  71.      end
  72.      popMatrix()
  73.      fill(blue)
  74.      ellipse(0, (HEIGHT -dx)/2, dx)
  75.      fill(red)
  76.      ellipse(WIDTH -dx, (HEIGHT -dx)/2, dx)
  77.      fill(white)
  78.      text(self.bins[self.aiMancala], dx/2, HEIGHT/2)
  79.      text(self.bins[self.playerMancala], WIDTH -dx/2, HEIGHT/2)
  80.      if self.turn == self.ai then
  81.          fill(blue)
  82.          str = "Blue's Move"
  83.      else
  84.          fill(red)
  85.          str = "Red's Move"
  86.      end
  87.      text(str, WIDTH/2, HEIGHT/2)
  88.  end
  89.  
  90. -- note: no protection against making invalid moves! fix this.
  91.  function Mancala:touched(t)
  92.      local selection = t.x/(WIDTH/self.numBins)
  93.      selection = math.ceil(selection)
  94.      if self.turn == self.ai then
  95.          selection = self.numBins - selection + 1 +self.playerMancala
  96.      end
  97.  
  98.      self:move(selection)
  99.  end
  100.  
  101. -- sows a bin according to the rules of the game.
  102.  function Mancala:move(index)  
  103.      local stones = self.bins[index]
  104.      self.bins[index] = 0
  105.      
  106.      -- while (true) should work too, but i prefer something that if it breaks,
  107.      -- will still terminate. and, im slow to fix working code.
  108.      for i = 1, self.totalStones do
  109.          if stones == 0 then
  110.              -- sowing complete. flip the turn, if conditions aren't met.
  111.              -- then update the winner and jump out of the function.
  112.              if self.turn == self.ai and index ~= self.aiMancala then
  113.                  flipTurn(self)
  114.              elseif self.turn == self.player and index ~= self.playerMancala then
  115.                  flipTurn(self)
  116.              end
  117.              self:updateWinner()
  118.              return
  119.          end
  120.  
  121.          -- wrap around to the start of the board        
  122.          if index < self.aiMancala then
  123.              index = index + 1
  124.          else
  125.              index = 1
  126.          end
  127.          
  128.          local sowHere = true
  129.          if index == self.aiMancala and self.turn == self.player then
  130.              sowHere = false
  131.          elseif index == self.playerMancala and self.turn == self.ai then
  132.              sowHere = false
  133.          end
  134.          
  135.          if sowHere == true then
  136.              self.bins[index] = self.bins[index] + 1
  137.              stones = stones - 1
  138.          end
  139.      end
  140.      -- fairly sure this doesn't get called. oh well, it doesn't hurt to be here.
  141.      self:updateWinner()
  142.  end
  143.  
  144.  function Mancala:validMove (index)
  145.      if self.bins[index] == 0 then
  146.          return false
  147.      end
  148.      return true
  149.  end
  150.  
  151. -- basically just a Get function.
  152.  function Mancala:gameOver()
  153.      return self.gameWon
  154.  end
  155.  
  156. -- should probably also check to see if either player has more than half the total...
  157. -- but this works, and allows you to keep playing if you want.
  158.  function Mancala:updateWinner()
  159.      -- prevents winner from ever changing from who won first.
  160.      if self.gameWon ~= nil then return end
  161.  
  162.      local done = false
  163.      if self:numberOfMoves() == 0 then done = true end
  164.  
  165.      -- this dirty trick of swapping turn twice prevents rewriting code.
  166.      flipTurn(self)
  167.      if self:numberOfMoves() == 0 then done = true end
  168.      flipTurn(self)
  169.  
  170.      if done == true then
  171.          if self.bins[self.playerMancala] < self.bins[self.aiMancala] then
  172.              self.gameWon = self.ai
  173.              return
  174.          elseif self.bins[self.aiMancala] < self.bins[self.playerMancala] then
  175.              self.gameWon = self.player
  176.              return
  177.          else
  178.              self.gameWon = 0.5
  179.              return
  180.          end
  181.      end
  182.      return nil
  183.  end
  184.  
  185. -- i was trying to make my code more modular. i think it ended up being uglier.
  186. -- this is called early in mcts() and in self:numberOfMoves()
  187.  function Mancala:possibleMovesInterval()
  188.      if self.turn == self.ai then
  189.          return vec2(self.playerMancala+1, self.aiMancala-1)
  190.      end
  191.      return vec2(1, self.numBins)
  192.  end
  193.  
  194.  function Mancala:numberOfMoves()
  195.      local interval = self:possibleMovesInterval()
  196.      local count = 0
  197.      for i = interval.x, interval.y do
  198.          if self:validMove(i) == true then
  199.              count = count + 1
  200.          end
  201.      end
  202.      return count
  203.  end
  204.  
  205.  function Mancala:randomMove(numberOfPossibleMoves)
  206.      local myMove = math.random(numberOfPossibleMoves)
  207.      local offset = 0
  208.      if self.turn == self.ai then offset = self.playerMancala end
  209.  
  210.      -- I CHEATED!! THIS ISN'T FULLY RANDOM!!
  211.      -- to improve the strength of the playout i gave it some domain-specific
  212.      -- tendencies (if not knowledge, per se). it has a chance of making certain
  213.      -- moves that i consider 'no brainers', and a lower chance of making what i
  214.      -- consider 'generally strong moves'. this seems to have massively increased    
  215.      -- its performance & strength, though!
  216.      -- it does still spend time making random other moves, however, so it can
  217.      -- still visit the entire search tree (eventually). also, if none of these
  218.      -- conditions are met, it will fall-through to a random move.
  219.      if randBool() then
  220.          if self.bins[offset+self.numBins] ~= 0 then
  221.              self:move(offset+self.numBins)
  222.              return
  223.          end
  224.          if self.bins[offset+self.numBins-1] == 2 then
  225.              self:move(offset+self.numBins-1)
  226.              return
  227.          end
  228.          if 2 <= self.bins[offset+self.numBins-1] and randBool() then
  229.              self:move(offset+self.numBins-1)
  230.              return
  231.          end
  232.          if randBool() then
  233.              for i = offset+self.numBins-2, offset+1, -1 do
  234.                  if self.bins[i] == i then
  235.                      self:move(i)
  236.                      return
  237.                  end
  238.              end
  239.          end
  240.      end
  241.      -- end "cheating"
  242.  
  243.      -- make an actually random move
  244.      for i = 1, self.numBins do
  245.          if self.bins[offset+i] ~= 0 then
  246.              myMove = myMove - 1
  247.              if myMove == 0 then
  248.                  self:move(offset+i)
  249.                  return
  250.              end
  251.          end
  252.      end
  253.  end
  254.  
  255. -- approximates the relative strength of each player's positions.
  256. -- returns a weighted ratio. i should probably cap this at 0..1
  257. -- (currently is -0.5 .. 1.5)
  258.  function Mancala:positionStrength()
  259.      return 0.5 + ((self.bins[self.aiMancala] - self.bins[self.playerMancala]) / self.totalStones)
  260.  end
Advertisement
Add Comment
Please, Sign In to add comment