19. Lessons from a simple board game – part 2
This post is about the undo feature in the game in the previous post. It may seem a little strange to have a whole post about one feature, but it kind of grew and grew, and got pretty interesting.
Some of this may be familiar to you if you have programming experience, so please skip the bits you know. But there are also a few really powerful Codea/Lua concepts that you should know.
The power of “or”
This really belongs further down but I wanted to make sure you saw it.
Codea has a really neat feature, which is probably best illustrated by example.
function doSomething(a,b) a=a or 1 b=b or 'end'
This looks quite strange, but what is happening is that if the user doesn’t pass through values for a and/or b when calling doSomething, the code will use default values instead. So what the code is saying, is “if a is not nil, use a, otherwise use 1”.
It even works for tables. My undo code usually returns a table, but if there are no more undos, it returns nil. In this case, I want to leave my table as it is, and not replace it with nil. This is easily done with
board=back:restore() or board
so if the restore function returns a table, it replaces board (technically, board now has the address of a different table containing the previous position), and if it returns nil, board is set equal to itself and doesn’t change.
Similarly, you can say, “if a, then …” and this will only be true if a is not nil or false.
This works with almost anything. For example, I have a backup program that makes copies of my code, and instead of including it in each project, I create a “dependency” or link to it (via the + symbol at top right of the code window – the same button that creates new tabs). So my backup program can be shared by all my other projects. But when I share my code with other people, they don’t have my backup code, and any reference to it would cause an error. So what I can do is say
if Backup then ...... -- only runs if Backup exists
So this even works for code!
Copying tables, deep copying
I started by wanting the ability to undo the last move. The obvious way to do this is to take a copy of the board table before making a move. So you might imagine that you could say
backup = board
However, all that happens is that any change to the board table also happens to backup, ie you still only have one table, but now it has two names. Why?
Let’s start with the simplest things – numbers and strings. Codea lets you copy one to another by just saying a=b. But for anything more complex, like a table, Codea doesn’t store that object in the variable, but a pointer to where that object can be found. You can think of it like this – numbers and strings are like mobile phones, that can be carried around and passed to someone else, but your PC stays at home, and you only carry your home address around with you. If you give that address to someone, now you both have the same address but there’s still only one PC.
So when you say backup = board, you are giving backup the address of the table held by board, not the table itself. If you then test if backup=board, it will be true, because they both hold the same address now. Ironically, if you do manage to make a separate copy of board table, called board copy, say, then boardcopy ~= board. This is because the boardcopy table is stored somewhere else to board, so it has a different address, so the addresses (which is what is carried around in the names board and boardcopy) won’t be the same. To test for true equality of two tables, you would need to do a loop and compare each element.
So how do you make a backup copy of a table? If it’s a 1D table, ie a list of items, you can use the table.concat command to convert it to a string, with the option to include a delimiter like a comma between items. You’ll remember I talked about saving user settings with saveProjectData in the last post, and said it wouldn’t work with tables. But if you convert your table to a string, then you can store the string.
Terminology – The process of converting something like a table into something that can be stored is called serialization. That’s all it means – making your data storable.
If you have a 2D table like board, however, table.concat doesn’t work. If you want to make a copy, you have to loop through the whole table and copy one item at a time, and if you want to turn it into a string, it’s the same process.
So if I were making a single undo option, I’d simply make an exact copy of board by looping through all the squares like this (remember, when creating a 2D table, you have to start with a 1D table, and then define each item in that table to be another table, before populating it).
backup={} for i=1,Size do backup[i] = {} --define 2nd dimension for j=1,Size do backup[i][j] = board[i][j] end end
and if the user wanted to undo a move, I would reverse the process and copy back.
Terminology – when you have to copy all the details of an object like a table like this, it’s called a “deep copy”.
Multiple undo
But I became a little ambitious and wanted multiple undos, so that every move is backed up and can be undone, right back to the start.
The first thing I did was to move all my undo code into its own class. This keeps the game code from getting messy, and makes it much easier to reuse the undo code with other projects.
I didn’t want to have potentially dozens of tables, so I decided to store each the board positions after each move as a string. This means I have to be able to convert a 2D table to a string, and back. There are no built in functions, so you have to write your own.
Note – after writing the code below, I generalized it to handle 1D tables as well as 2D tables, so the code in the project is slightly different.
Here is my function to convert from a table to string.
local s={} for i=1,#t do for j=1,#t[i] do s[#s+1]=t[i][j] end end self.backups[#self.backups+1]=#t[1]..','..table.concat(s,',')
You’ll see I do a loop through all columns, then rows, adding the values to a 1D table called s. Why use another table when I want a string? Because Codea works much faster with tables than strings, the reason being that when you add to a string, Codea has to create an entirely new string and discard the old one, and this gets slower as the string lengthens. It probably doesn’t matter with a smallish table like this, but it’s a good habit to get into. And when I’m done, table.concat will turn my 1D table into a string.
The last line
I needn’t have bothered with the commas because each square can only have the number 1 to 6, ie a single digit, but I wrote this so it could be used for other projects where the values might be longer numbers or strings.
The prefix of column number is needed because when I come back to undo a move, I have to convert this long string back into a 2D table, and I need something to tell me how many columns and rows it has.
When I want to undo a move, I reverse the process.
local b=self:split(self.backups[#self.backups]) --split backup string into a 1D table local cols=tonumber(b[1]) --first item tells us number of columns local ii,jj=cols,(#b-1)/cols -- calc number of rows (jj) local n=1 for i=1,ii do --loop through cols t[i]={} --create array for this row for j=1,jj do --loop through row n = n + 1 --counter to help us find the place in b t[i][j]=b[n end end end table.remove(self.backups,#self.backups) --remove latest backup
The first line splits the string back into a 1D table to make it easier to work with. The split function is included in most languages, but not in this version of Codea. This is useful code to keep, and credit for it goes to Codeslinger. I won’t cover it here because it’s quite technical.
Note how the last line removes the latest backup.
Terminology – the table of backups is what is called a “stack”, to which you “push” new items and “pop” items back off. (Yes, pushStyle and popStyle are stacks, which back up style settings and can hold multiple different settings).
So what I have is a class that can store and restore a series of tables (1D or 2D). It doesn’t handle all types of tables, or more than one table at once, but I did say this was a simple game, and this is good enough for me at the moment. But maybe if I reuse the undo class in another game, I can enhance it then. And that’s the beauty of classes, being self contained.
I hope you found some of this interesting. And the game is addictive, too!