LiveCode Desert Island Nim

Posted by Scott McDonald

Not just Nim, Desert Island Nim. This is the classic game of Nim, but with a cuter name and a visual theme of sand, driftwood and woven palm leaves. Never underestimate the power of dressing up a simple game with a fancy name or look. If Candy Crush Saga, which is based on Bejeweled (both could be called "Three In A Row") find a clever name helps, does the same work for Nim?

Of course, Nim doesn't have the variable gameplay of those games. You can win every time with the right stratgegy, boring! But the point is don't underestimate the appeal of a simple concept with pretty graphics and a catchy name. (And it helps if the gameplay has that just one more go to beat my friends or top score temptation.)

Coding wise, in this game you will see:

  • Buttons themed with bitmaps
  • Buttons with a mouse rollover effect
  • An extra hidden card for storing resources
  • Using an array for flexible and simpler coding
  • An AI algorithm that works most when the CPU starts losing

The StartGame handler initialises the variables and calls MakeRocks and UpdateView. The sNumberOfRocks array stores the number of rocks in each row.

command StartGame
  put 0 into sGamesPlayed
  put 0 into sGamesCPUWon
  put empty into sDragRow
  put empty into sRockToDrag
  put true into sYourTurn
  show image "your-turn.png"
  hide image "my-turn.png"
  hide image "game-over-i-win.png"
  hide image "game-over-you-win.png"
  hide button "butnFinishTurn"
  put 3 into sNumberOfRocks[1]
  put 4 into sNumberOfRocks[2]
  put 5 into sNumberOfRocks[3]
  MakeRocks
  UpdateView
end StartGame

An array is a powerful way of tracking the number of rocks in each row. A common novice approach is to have three variables, say sFirstRowCount, sSecondRowCount and sThirdRowCount. Or maybe named sRow1, sRow2 and sRow3. This seems like a good idea, but once you start coding, you find lots of duplicated statements are necessary to repeat similar actions for each variable.

And what happens if you decide to have 4 rows? More code needs to be added to handle the extra row, with more work for you. By using an array to store the details of each row, the code becomes much simpler. All that is needed is a loop that interates though each element of the array to do the required actions.

MakeRocks creates the images required for each row of rocks. Unlike in 23 Matches where all the match images were added to the game in the LiveCode IDE at design time, in Nim a single image named original-rock.png was imported onto the Bitmaps card at design time and then the code below creates the required images. This approach means that if you decide to change the number of rows, or the number of rocks in each row, other than changing the value of the kNumberOfRows constant, and setting the number of rocks for each row in StartGame, the code stays the same.

command MakeRocks
  local firstXY,xy,rockName
  lock screen
  repeat with loopIndex = 1 to the number of images
    if the name of image loopIndex contains "Row" then hide image loopIndex
  end repeat
  put 0 into sTotalNumberOfRocks
  put 300,300 into firstXY
  repeat with loopRow = 1 to kNumberOfRows
    put firstXY into xy
    repeat with loopRockNumber = 1 to sNumberOfRocks[loopRow]
      put RockImageName(loopRow, loopRockNumber) into rockName
      if not exists(image rockName) then
        create image
        set the name of the last image to rockName
        set the rectangle of the image rockName to the rectangle of image "original-rock.png" of card "Bitmaps"
        put image "original-rock.png" of card "Bitmaps" into image rockName
      end if
      set the layer of the image rockName to top
      set the loc of image rockName to xy
      show image rockName
      add 1 to sTotalNumberOfRocks
      add 25 to item 1 of xy
    end repeat
    add 80 to item 2 of firstXY
  end repeat
  unlock screen
end MakeRocks

This illustrates the power of using an array. The number of rocks and rows can change without messing with the code. (Although you may need to fine-tune the initial value of firstXY and the increment to xy to make the rocks fit on the mat.) If you ever find yourself writing code with an increasing number of variables and code doing the same actions over and over on each variable, think about using an array to simplify your code.

Unlike the previous games in this blog that take an inefficient approach when initialising the graphics and images, deleting everything and then re-creating it all from scratch each time you played a game, this code is better. The exists command checks whether each rock image exists before creating it. When there isn't an image with the name set by RockImageName then it is created, making a copy of original-rock.png. Then the image is put in the appropriate location with the value in xy.

In this line:

set the rectangle of the image rockName to (the rectangle of image "original-rock.png" of card "Bitmaps")

the parentheses are optional. In most, cases LiveCode undertands the meaning of long lines like this:

set the rectangle of the image rockName to the rectangle of image "original-rock.png" of card "Bitmaps"

but sometimes it doesn't, and I find the parentheses makes the code more readable.

RockImageName is called from MakeRocks to return the name of an image based on the row and the position in the row. This is a single line of code, but it is called in mouseDown and ProcessAI too, so making a function ensures consistency with the names used for the images, which is a key to keeping the code short in Desert Island Nim.

function RockImageName pRow, pNumber
  return "Row" & pRow & "-Rock" & pNumber
end RockImageName

UpdateView is the last handler called from GameStart. It changes the visibility of the different prompts, and checks if the game is over by checking if sTotalNumberOfRocks is zero. The sTotalNumberOfRocks variable is the number of rocks remaining, so when there are no rocks the game is finished.

All prompts are images. This is another change from the previous games in this blog that simply put the required text into a text field. Using images allows for an interesting font, without the licensing issues of embedding the font. The prompt images were prepared in Paint.NET, and while you wouldn't use this approach for a lot of text, in this game it was quick and easy.

command UpdateView
  lock screen
  hide image "your-turn.png"
  hide image "my-turn.png"
  hide button "butnFinishTurn"
  if sYourTurn then
    if sTotalNumberOfRocks = 0 then
      add 1 to sGamesPlayed
      add 1 to sGamesCPUWon
      show image "game-over-i-win.png"
    else
      show image "your-turn.png"
    end if
  else
    if sTotalNumberOfRocks = 0 then
      add 1 to sGamesPlayed
      show image "game-over-you-win.png"
    else
      show image "my-turn.png"
    end if
  end if
  unlock screen
end UpdateView

The mouseDown handler initiates the dragging of a rock. Here the name of the image is used as a simple way of determining what row the rock is from. (This code would not work with more than 9 rows because only a single char from the target name is put into dragRow.) Then a check is made to ensure only the rock at the right end of a row is dragged. The original location of the rock is stored. This is for later when if a rock is not fully dragged off the mat, it can be returned to the original position.

The row of the rock is stored to ensure that each subsequent time mouseDown is called, you can only start a drag action if the rock is from the same row as the first one.

on mouseDown
  local targetName,dragRow
  if sYourTurn then
    put the short name of the target into targetName
    if targetName begins with "Row" then
      put char 4 of targetName into dragRow
      if targetName = RockImageName(dragRow, sNumberOfRocks[dragRow]) then
        put the loc of the target into sOriginalLoc
        if (dragRow = sDragRow) or (sDragRow is empty) then
          put dragRow into sDragRow
          put targetName into sRockToDrag
        end if
      end if
    end if
  end if
end mouseDown

The mouseMove handler is short, simply moving the image to the mouse location, so on to mouseUp. If sRockToDrag is not empty and the location of the image is not within the rectangle of the image of the woven mat, then the rock is hidden. While typing that sentence I marvel at how much the description is like the LiveCode code being described. LiveCode really is self documenting a lot of the time. After hiding the rock image and updating the number of rocks, a check is made to see if the turn must end because the row is empty.

If the mouseUp event occurs while the rock is still on the mat, the move command puts it back to sOriginalLoc in half a second. As mentioned before in 23 Matches, the move command is one of those neat LiveCode features that can achieve what takes many lines in a less High Level language.

on mouseMove
  if sRockToDrag is not empty then
    set the loc of image sRockToDrag to the mouseLoc
  end if
end mouseMove

on mouseUp
  if sRockToDrag is not empty then
    if the loc of image sRockToDrag is not within the rectangle of image "mat.png" then
      hide image sRockToDrag
      show button "butnFinishTurn"
      subtract 1 from sNumberOfRocks[sDragRow]
      subtract 1 from sTotalNumberOfRocks
      if sNumberOfRocks[sDragRow] = 0 then
        FinishTurn
      end if
    else
      move image sRockToDrag to sOriginalLoc in 30 ticks
    end if
  end if
  put empty into sRockToDrag
end mouseUp

The FinishTurn handler is called when the number of rocks in a row is zero during your turn, or if the Finished Turn button is clicked. The Finished Turn button is a Transparent button that uses two images from the Bitmaps card for its appearance. The images were again made in Paint.NET and set in the Properties of the button on the Icons & Border pane in the LiveCode IDE at design time. With the icon and hoverIcon properties set, the button has a nice mouse rollover effect because the hoverIcon image is the same as the icon, but offset downwards and to the right by 2 pixels. Again LiveCode makes visual effects easy with no coding required.

FinishTurn updates a couple of variables and calls UpdateView and ProcessAI.

command FinishTurn
  put false into sYourTurn
  put empty into sDragRow
  UpdateView
  ProcessAI
end FinishTurn

ProcessAI first checks whether there are any rocks left, if there are zero rocks the game is over. Then instead just alternating between playing smart or random like in 23 Matches, a simple formula makes the CPU try to win the first game, and any other time it is winning less than 70% of the games. It is set to win the first game to challenge you, but it then plays randomly until you have won at least one or two games.

The code here for a winning game is based on the algorithm in Wikipedia. It is based on a mathematical procedure called the "nim sum". You can read more about it here, and it makes it difficult (but not impossible) to beat the CPU. When the CPU is playing random, it may still win, but only by luck.

Once takeRow and takeCount are set, either randomly or by the AI, the rocks are moved to a location off the right end of the mat, with a random value used for the y position to make the movement more interesting. The move takes 60 ticks which is the same as one second. Perhaps it would be more "realistic" if the number of ticks had a random variation, but I think the variation in direction of the rock is sufficient to make it seem interesting.

Then it is your turn and UpdateView is called. If in ProcessAI the last match is taken, UpdateView shows the "I win" boast for the CPU.

command ProcessAI
  local takeRow,takeCount,rockName,nimSum,rowNimSum
  if sTotalNumberOfRocks > 0 then
    put 0 into takeCount
    if (sGamesPlayed = 0) or ((sGamesCPUWon / sGamesPlayed) < 0.7) then
      -- play smart
      -- find the nim-sum
      put 0 into nimSum
      repeat with loopIndex = 1 to kNumberOfRows
        put sNumberOfRocks[loopIndex] bitXOR nimSum into nimSum
      end repeat
      put 0 into takeRow
      put 0 into sNumberOfRocks[takeRow]
      repeat with loopIndex = 1 to kNumberOfRows
        if sNumberOfRocks[loopIndex] > 0 then
          put sNumberOfRocks[loopIndex] bitXOR nimSum into rowNimSum
          if rowNimSum <= sNumberOfRocks[loopIndex] then
            if sNumberOfRocks[loopIndex] > sNumberOfRocks[takeRow] then
              put loopIndex into takeRow
              put sNumberOfRocks[loopIndex] - rowNimSum into takeCount
            end if
          end if
        end if
      end repeat
    else
      -- play randomly
      repeat until takeCount > 0
        put random(kNumberOfRows) into takeRow
        if sNumberOfRocks[takeRow] > 0 then put random(sNumberOfRocks[takeRow]) into takeCount
      end repeat
    end if
    repeat takeCount times
      wait 1 + random(60) ticks
      put RockImageName(takeRow, sNumberOfRocks[takeRow]) into rockName
      move image rockName to (880,60+random(500)) in 60 ticks
      hide image rockName
      subtract 1 from sNumberOfRocks[takeRow]
      subtract 1 from sTotalNumberOfRocks
    end repeat
    put true into sYourTurn
    UpdateView
  end if
end ProcessAI

Desert Island Nim is not going to hold your attention for long once you figure out the winning strategy. Not being a graphics artist, means no award for slick looks either. (Some may say it is downright ugly.) But that is not the point. Give your next game an interesting name; create, buy or find some cool graphics for it and make a simple idea into attractive game. Which link would you click at a game website: Desert Island Nim, or Nim?

Download the complete LiveCode Desert Island Nim stack made in LiveCode 6.1.2 from here: LiveCode Nim.livecode

Happy LiveCoding.

Credit: Sand texture from Free Seamless Textures

Tagged: intermediate strategy turn based

Monday, March 31, 2014

0 Responses

Be the first to make a comment.


COMMENTS ARE CLOSED

 

Legal Stuff

All blog posts are copyright and cannot be re-used without permission. But all the code and scripts are dedicated to the public domain. Use such code and scripts in any way you want, but I am not responsible if they don't work for you.

Contact

Further comments or feedback? You can contact me by email at: