152. Recreating the Threes game app – part 1
I was playing the popular Threes game app, and I never seem to get any better at it, so I wondered if there was a strategy I was missing. That was a good enough excuse to program it in Codea.
If you haven’t done much game programming in Codea, you should find these posts interesting, because I will cover things like
- drawing the board and tiles
- storing board positions and other details
- handling game “states”
- handling player swipes (to slide the tiles)
And there is something extra – the real reason for doing this – monte carlo simulation to find the best move to play next.
Anyway, feel free to join me on this little project.
The game of Threes
First, if you don’t know the game, this video will explain it
What I’m not going to include
It’s always a good idea to keep things simple to start with, and worry about the frills later (as long as that doesn’t require you to rewrite half your program!).
So I’m not going to worry about putting rounded edges on the tiles, nor animating the player swipes (ie moving the tiles as the finger moves), nor the opening menu screens etc. I want to focus on actual play.
Threes also starts adding higher value tiles after a while (6, 12 or 24) and shows them with a + instead of their value. I didn’t program this because it would have taken me ages to figure out their rules for doing this.
Part 1 – create the initial board position
In this post, I’ll simply try to draw the board and put some initial tiles on it. It’s quite a long post, but just skip any parts that don’t interest you.
Threes doesn’t have any published rules, so I’ve had to guess them by playing it a few times. It seems to start with 9 randomly chosen tiles with a value of 1, 2 or 3.
Anyway, I’m getting ahead of myself, because I haven’t created a board yet. It is a 4×4 table with a border around each tile.
So I’ve drawn a board that is similar (without rounded edges), with this code. See the numbered notes below the code for explanations.
function CreateBoard() --[1] cellWidth,cellHeight,cellGap,textSize=100,133,10,48 --[2] imgBoard=image(cellWidth*4+cellGap*5,cellHeight*4+cellGap*5) --[3] setContext(imgBoard) --[4] background(150, 192, 211, 255) --choose some colours fill(177, 199, 210, 255) local w=cellGap --[5] for r=1,4 do local h=cellGap for c=1,4 do rect(w,h,cellWidth,cellHeight) h=h+cellHeight+cellGap end w=w+cellWidth+cellGap end setContext() --[6] end
[1] – I’m putting this into its own function because there is quite a lot of code, and all of it is just doing one task – drawing the board. This keeps the code out of my setup function, leaving it nice and simple.
[2] – First, I’ll set the size of each tile (I called them cells instead of tiles, sorry), the gap between tiles, and the text size. It’s a good idea to store these settings in variables rather than hard coding the numbers through your code, because if you change your mind, then you only have to change the numbers here.
[3] I create an image big enough to hold 4 tiles and the gaps between them
[4] I’m going to be drawing this board 60 times a second. I can either draw it from nothing, ie drawing all the little squares every time, or else, I can create an image containing the whole board, then just show that single image each time I draw. So I’m using the second approach. The setContext command tells Codea to draw to an image in memory instead of to the screen.
[5] Now I loop through the rows and columns of the 4×4 table of tiles, drawing a rectangle where each tile will go. I use variables w and h to store the position of the current tile, and update them as I go.
[6] I’m done drawing on my image, so I tell Codea to stop drawing on it.
Create some initial tiles
Now I’ll create 9 random tiles to put on the board, using the code below. Again, see the numbered notes underneath, for explanations.
function Initialise() --[1] boardValues={ [0]={0,0,0}, --[2] [1]={color(255),color(0,0,255),0}, [2]={color(255),color(255,0,0),0}, [3]={color(0),color(255),3}, [6]={color(0),color(255),9}, [12]={color(255,0,0),color(255),27}, [24]={color(255,0,0),color(255),81}, [48]={color(255,0,0),color(255),243}, [96]={color(255,0,0),color(255),729}, [192]={color(255,0,0),color(255),2187}, [384]={color(255,0,0),color(255),6561}} board={} --[3] for i=1,4 do board[i]={0,0,0,0} end --now create the initial tiles for i=1,9 do local n=RandInt(3) --[4] while true do --[5] local r=RandInt(4) local c=RandInt(4) if board[c][r]==0 then board[c][r]=n break end end end NextTile=RandInt(3) --[6] --calculate the position of each tile on the screen --[7] --first calculate bottom left position of board bottomPos=vec2(WIDTH/2-cellWidth*2-cellGap*2.5,HEIGHT/2-cellHeight*2-cellGap*2.5) boardPos={} --table to hold positions for c=1,4 do boardPos[c]={} for r=1,4 do --[8] boardPos[c][r]=vec2(bottomPos.x+(cellWidth+cellGap)*(c-1)+cellGap, bottomPos.y+(cellHeight+cellGap)*(r-1)+cellGap) end end nextTilePos=vec2(bottomPos.x-cellWidth-10,HEIGHT/2) end --returns random integer between 1 and b (inclusive) --[4] function RandInt(a) return math.floor(math.random()*a)+1 end
[1] Again, I put this in its own function. Later, when we actually play the game, we will re-use it each time we restart the game.
[2] I wrote down the colour of the tile and text, for each number used in the game, and stored them in a table to make things easy. I’m storing three things – background tile colour, text colour, and the score that goes with each number. Note that the first row is all zero’s, because empty tiles will contain zero, so I need to include them (for scoring, as you’ll see later).
You may be puzzled by the way I’ve written each row of the table. Why put a number in square brackets and make it equal to a little table of three values?
The reason is that I want to be able to look up tile colours and scores just based on their number, so I a tile is “3”, then I can easily get its colours or score. Putting the numbers in square brackets tells Codea that they are lookup “keys” for the table, and when you provide Codea with one of these keys, it will give you back the little table of three values after the equal sign.
So boardValues[3] ={color(0),color(255),3}
which means that boardValues[3][2] is the text color, color(255)
This kind of lookup table is called a dictionary in most languages, and most languages keep them separate from ordinary tables. Lua lets its tables do everything, which can be very confusing at first.
[3] I set up an empty table to hold the positions of the number tiles, and then create a 4×4 table filled with zero’s
[4] I wrote a little function, RandInt, to choose a random integer between 1 and a, where a is a number you provide. This is because I need to choose tile numbers between 1 and 3, and I also need to choose a random row and column (1 to 4) for each tile. If you’re not used to writing little functions like this to handle small parts of your program, you should get used to it, because it makes your code cleaner, avoids duplicating code, and makes testing much easier.
[5] Having selected a random number, I need to find an empty square to put it in. I choose a row and column at random, and if that tile is empty (zero), I put the number in there, otherwise I go back and try again.
“Trying again” is done by wrapping this part of the code in a loop that starts off with while true do. This seems ridiculous, because it is shorthand for “while true==true do”, which is of course always true, so the loop will go forever. This is what I want, though. I want the loop to keep going until I tell it to stop. And when I find an empty tile, then I use the command break, to exit the loop, and go on to the next random tile.
[6] I also need to create the next tile that will be added after the player’s move.
[7] Another thing I’m going to be doing 60 times a second, is drawing the tiles on the screen. Their positions will depend on the size of the player’s screen, but they will always be the same for each player. So it makes sense to calculate them at the beginning, to minimise the work in drawing. There’s nothing very clever about this code. I also calculate the location of the next tile to be added, which I’m putting on the left of the board.
[8] Note on the next line, how I am storing the x,y positions in something called vec2. This is nothing more than a little table with two values, and they are usually used for working with positions in 2D graphics, and there are special functions that give you the distance or angle between two vec2’s. I’m using them for convenience, nothing more.
Drawing the board
So now I want to draw the board and tiles. Here is my code. I have put it in a separate function called DrawBoard, and I also have a separate function called DrawTile that draws each tile. Note how compact the code is, when I can look up the tile positions and colours from the tables I created earlier.
function DrawBoard() --draw board itself sprite(imgBoard,WIDTH/2,HEIGHT/2) --draw the tiles for c=1,4 do for r=1,4 do if board[c][r]>0 then DrawTile(boardPos[c][r],board[c][r]) end end end --show next tile to come DrawTile(nextTilePos,NextTile) end function DrawTile(p,v) fill(boardValues[v][2]) --background colour rect(p.x,p.y,cellWidth,cellHeight) fill(boardValues[v][1]) --text colour font("ArialRoundedMTBold") fontSize(textSize) text(v,p.x+cellWidth/2,p.y+cellHeight/2) end
End of stage 1
So now I just need to add the setup and draw functions (which you might have thought I would have started with), and we can try it out. Because we’ve put everything in separate functions, setup and draw are extremely simple.
function setup() Settings() CreateBoard() Initialise() end function draw() background(135, 187, 213, 255) DrawBoard() end
Just one note – I am running a Settings function in setup, and I didn’t show that above. All I did was move a couple of the setup lines (size of tiles, tile colours etc) into their own function called Settings, to keep all of them together. You’ll see this in the final code, which you can download here.
This is the result, when I run it
In the next part, we’ll make the game playable.