== Lesson 12: Score, Lives, and Game Over image::learn/12-score.png[The finished lesson, 480] Your game keeps score, takes lives, and says GAME OVER. It is nearly a game. Three things are still missing. It has no front door: it is in the middle of the action the instant it loads. It forgets: quit, and the best score anybody ever got goes with it. And it keeps track of what it is doing with two true-or-false variables, `over` and `dying`, which is the trap lesson seven warned you about. This lesson fixes all three. One variable for what the game is doing, a title screen to start from, a high score that is still there tomorrow, and a second game that starts exactly like the first. === A Game Is Always in Exactly One State Watch any arcade machine for two minutes. It shows a title and waits for somebody. Somebody presses start and it plays. The player runs out of lives and it says so, then waits again. The machine is always doing exactly one of those things. Dodge, in lesson seven, kept that in one variable holding a word: `"waiting"`, `"playing"`, or `"over"`. Lesson nine's game went back to a true-or-false `over`, and lesson ten added `dying` beside it. Count what two of those can say. Each is `true` or `false`, so together they make four combinations. Three mean something: playing, dying, and over. The fourth, both at once, is nonsense, and your code has to be careful never to produce it. It is careful: look at `endGame`, which sets `dying` back to `false` before it sets `over` to `true`. Now add a title screen. That is a third flag, eight combinations, and four of them nonsense. That is lesson seven's trap exactly, and it is where small games go to die. So do what Dodge did. Four states, one variable. Put the names of the states under your other constants: [source,lua] ---- local TITLE = "title" local PLAYING = "playing" local DYING = "dying" local OVER = "over" ---- Then take `over` and `dying` out of your variables, and put one line in their place: [source,lua] ---- local state = TITLE ---- Dodge used the words themselves. Names in capitals are not cleverer, and they do not catch a spelling mistake: Lua treats a misspelled name as `nil` and a misspelled word as a word that matches nothing, and either one gives you a game that quietly stops responding. What the names buy is a list. Every state the game can be in is written down once, at the top of the file, where you can see all four at a glance. Everywhere the game used to set a flag, it now sets the state. In `updateRocks`, where the last life goes: [source,lua] ---- if lives <= 0 then state = DYING deathChannel = channel ---- In `endGame`, two lines become one, and the nonsense combination is gone because there is nothing left to combine: [source,lua] ---- function endGame() deathChannel = -1 state = OVER musicStop(theme, 1500) end ---- And in `startGame`, the two lines that cleared both flags become `state = PLAYING`. === One Place That Decides Every frame, `onOverlayUpdate` makes the two decisions that matter: what moves, and what words go on top. What moves is one test: [source,lua] ---- if state == PLAYING then updateShip() updateShots() updateRocks() end ---- That replaces `if not over and not dying then`, and it says what it means. On the title, while the last explosion plays, and after the game is over, nothing moves. You get that by not calling the functions that move things. Stopping a game is not a special feature; it is leaving things out. The world is still drawn every frame, whatever the state, exactly as Dodge drew it. Only the words change. Replace the `drawHud()` line with this: [source,lua] ---- if state == TITLE then drawTitle() else drawHud() end ---- `drawTitle` is new, and it goes after `drawStars`: [source,lua] ---- function drawTitle() overlayPrint(25, 6, "R O C K S") overlayPrint(25, 9, "BEST " .. best) overlayPrint(22, 12, "PRESS 1 TO PLAY") end ---- `best` is the best score so far, and it arrives in the next section; the game will not run until it does. Before the first game the list of rocks is empty, so what the title shows is the stars drifting past and the ship waiting at the bottom, which is all a title screen needs. One line has to go, or you will never see the title at all. Delete the `startGame()` at the very bottom of the file. The game begins on its title now, and *Starting Again* says why that line matters. `drawHud` asks one question of its own. Change its `if over then` to: [source,lua] ---- if state == OVER then ---- The input callback needs the same kind of thinking. Here it is whole: [source,lua] ---- function onInputPressed(what) if what == SWITCH_LEFT then goLeft = true elseif what == SWITCH_RIGHT then goRight = true elseif what == SWITCH_BUTTON1 and state == PLAYING then shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY } soundPlay(shootClip, 0, 35) elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then soundPlay(clickClip) startGame() end end ---- Read it from the top. The arrow keys are recorded whatever the state, because they only say which keys are down, and nothing moves the ship unless the state is `PLAYING`. Fire means something only while playing. And `1` starts a game from the title or from the game over screen, and not while the last explosion is still sounding: the game is not over until it says so. The parentheses work the way they do in arithmetic. The `or` inside them is worked out first, and then the `and` joins it to the key. === Score and Lives You have had both since lesson nine, and they hardly change. What is new is the best score, which starts life as an ordinary variable. Put it with the others, below `lives`: [source,lua] ---- local best = 0 ---- Show it in the line across the top, between the other two. In `drawHud`: [source,lua] ---- overlayPrint(1, 1, "SCORE " .. score) overlayPrint(25, 1, "BEST " .. best) overlayPrint(50, 1, "LIVES " .. lives) ---- And give `endGame` the job of noticing a record: [source,lua] ---- if score > best then best = score end ---- Run it. The title says `BEST 0`. Press `1`, shoot something, lose, and the game over screen shows your score as the best. Press `1` again and the best stays while the score starts over. Now quit, run it again, and look at the title: `BEST 0`. The record lived exactly as long as the program did. That is the second half of this lesson. While you are looking at lives, notice that lesson nine wrote the test as `lives <= 0`, not `lives == 0`, and keep it that way. Today nothing can take two lives at once, so the two can only ever agree. The day you add a rock that costs two, `== 0` steps straight over zero and the player keeps playing on minus one life, while `<= 0` keeps working. Ask for what you mean. === Starting Again You have already deleted the `startGame()` from the bottom of the file. In lesson ten the first game was started by that line and every later one by pressing `1`, which is two ways in. Now there is one. Every game, the first included, starts when somebody presses `1` and `startGame` runs, so the first game and the fifth start exactly the same way. `startGame` has to put back everything a game changes: [source,lua] ---- function startGame() shipX = (screenWidth - shipWidth) / 2 score = 0 lives = START_LIVES state = PLAYING deathChannel = -1 shots = {} for i = 1, ROCK_COUNT do rocks[i] = {} newRock(rocks[i]) end musicSetVolume(50) musicPlay(theme, -1) end ---- It is just as important to know what it leaves alone, and why. `best` outlives every game; that is its whole job. The stars are scenery. And `goLeft` and `goRight` are not the game's at all. They are the keyboard, and only the keyboard should change them. That last one is a bug you have just fixed without noticing. Lesson ten's input callback started with `if over then`, and while the game was over it threw every key away except `1`. Press the right arrow while GAME OVER is showing, keep holding it, and press `1`. The new ship sits still: the press of the right arrow was thrown away, the release has not happened yet, and so `goRight` is `false` while the key is plainly down. The ship stays put until you let go and press again. The new callback records the arrow keys in every state, so the held key is still held when the game begins. Bugs in starting again are the most common bugs in any game, and they all feel the same to play: the second game is subtly wrong and the first one was fine. When that happens, read `startGame` and ask two questions. What did the last game change that this one does not put back? And what is this one putting back that was never the game's to touch? === A Number That Outlives the Program Everything your game has held so far -- `score`, `lives`, the list of rocks -- lives in memory, and memory is gone the instant the program stops. Singe gives every game a save of its own: a set of names with values under them, written to disk for you. It is another part of the game kit from lesson eleven, so it is the same kind of thing as `collideRects`: plain calls, nothing to load or free. [source,lua] ---- saveSet("highScore", 4200) ---- `saveSet` takes a name -- the *key* -- and a value. The key is yours to choose and never changes; the value is whatever you want to remember. Numbers, strings, `true` and `false`, and whole tables of those all go in. Things that only make sense while the program is running do not. A sprite handle is only a number, so it saves, and tomorrow it is a number that means nothing. A function cannot be written down at all, and trying to save one stops the game with an error rather than letting you find out next week. Reading it back is the other half. Change the line you added for `best`: [source,lua] ---- local best = saveGet("highScore", 0) ---- It runs once, when the game starts. The second argument is the *default*: what to answer when nothing has ever been saved under that key. More about that in a moment, because it is the whole of this lesson's error. And make `endGame` write the record down when it falls: [source,lua] ---- function endGame() deathChannel = -1 state = OVER musicStop(theme, 1500) if score > best then best = score saveSet("highScore", best) saveFlush() end end ---- You do not have to ask Singe to write the file. It writes once at the end of any frame in which something changed, however many keys you set, and again when the game shuts down. `saveFlush` writes it immediately instead: a new high score is worth a file write of its own, because an arcade cabinet gets switched off at the wall and a frame is a long time. Run it, set a score, quit, and run it again. The title remembers. The reference's Save section lists the rest of the family. `saveDelete` forgets one key, `saveClear` forgets everything -- that is the "reset high scores" in a service menu -- and `saveGetAll` hands you a copy of the lot, which is useful when you want to print the whole save while you are hunting something. === Where the File Actually Goes It does not go beside your script, and this is not an accident. A finished game may live on a disc, on a network share, or inside a single packed file (lesson thirty), and none of those can be written to. A game that saves next to its own script works perfectly on the machine you wrote it on and fails on half the machines it is installed on. Singe 2 did it that way and it was wrong. So everything Singe writes for a game -- the save, screenshots, its log -- goes in one place, and `singeGetDataPath` will tell you where: [source,lua] ---- debugPrint(singeGetDataPath()) ---- Put that just below the `dofile` line for a moment and run it. It says `data/rocks/`: the `data` folder that appeared in your work folder the first time you ran anything, in lesson one, and inside it a folder named after your game's folder. In there you will find `save.json`. Open it in your text editor: it is plain text, and your high score is in there under the name you gave it. That is the answer whenever you need a file of your own as well. Build the name from `singeGetDataPath()` and it lands somewhere writable; write to a name of your own invention and sooner or later it does not. === The First Time, There Is Nothing The very first time anybody runs your game, no high score has ever been saved. `saveGet` has nothing to hand back, so it hands back the default you gave it: [source,lua] ---- local best = saveGet("highScore", 0) ---- Nothing saved, so `best` is `0`, the title screen says `BEST 0`, and the first game anybody plays sets a record. That is exactly what you want, and it costs one extra argument. Leave the `0` out and `saveGet` has no default to fall back on, so it answers `nil` -- the value Lua uses for "there is nothing here", which you met in lesson three. `nil` is not zero. You cannot add to it, you cannot compare it with a number, and you cannot glue it onto a string. It goes wrong on the first frame, every time, on every machine where the game has not been played -- which includes every machine your players are about to install it on, and not the one you are testing on. Give every `saveGet` a default. It is the cheapest habit in this book. === What Just Happened [source,lua] ---- local TITLE = "title" ---- A constant: a name you set once and never change, in capitals so that you can see at a glance that it is one, as in lesson seven. Lua does not enforce it -- nothing stops you assigning to `TITLE` -- so the capitals are a message to the next person reading, who is you in a fortnight. [source,lua] ---- if state == PLAYING then ---- The question that decides whether the world moves. When you add a fifth state later -- a pause, or a bonus round -- the first thing to ask is whether the world moves in it, and this is where the answer goes. [source,lua] ---- elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then ---- Without the parentheses, Lua would join the `and` first and read this as "the key is `1` and the state is the title, or the state is over". Then any key the branches above did not catch would start a game from the game over screen, fire included, and a player still hammering the fire button when the last rock hits would be dropped into a new game the moment GAME OVER appeared. The parentheses make the `or` happen first. When you mix `and` and `or`, add the parentheses even if you think you know the order. The next person to read it will not have to know. [source,lua] ---- saveSet("highScore", best) saveFlush() ---- `saveSet` puts the number in the save; `saveFlush` puts the save on the disk now rather than at the end of the frame. Both are inside the `if`, so the file is only written when the record is actually broken, which is a few times a session rather than sixty times a second. [source,lua] ---- local best = saveGet("highScore", 0) ---- Read once, at startup, into an ordinary variable. The rest of the game reads `best` rather than calling `saveGet` again, because the save is a file and a variable is a variable. Write to both when the record falls. === Try It . *Go back to the title.* An arcade machine does not sit on GAME OVER forever. Look up `timerAfter` in the reference's Timer section, another part of the game kit, and use it in `endGame` to put the state back to `TITLE` five seconds after the game ends. Then press `1` quickly after a game ends and make sure the timer does not throw your new game back to the title three seconds in. . *Show the lives as ships.* Draw `shipSprite` once per life in the top right corner with `spriteDraw` instead of printing the number. A `for` loop from lesson four, and `shipWidth` tells you how far apart to put them. . *Save more than a number.* Add `saveSet("gamesPlayed", saveGet("gamesPlayed", 0) + 1)` to `startGame`, and print it on the title screen. Quit, run again, and watch it remember. . *Find the file and break it.* Open `data/rocks/save.json` in your editor, change the high score to something absurd, and run the game. Then delete the file entirely and run it again. Both of those will happen to your players. . *Reset it from inside the game.* Make a key you would never press by accident call `saveClear()`, and check the title screen afterwards. Careful: `best` is still in memory, and `saveClear` does not touch it. Work out what else has to happen. === Break It on Purpose Take the default out, so the line reads: [source,lua] ---- local best = saveGet("highScore") ---- If you have already set a high score, nothing happens at all, which is the trap: the save has the key, so `saveGet` hands it back. Delete `data/rocks/save.json` first, then run it. The title screen never appears, and you get this: ---- Error executing function 'onOverlayUpdate': rocks/rocks.singe:101: attempt to concatenate a nil value (upvalue 'best') stack traceback: rocks/rocks.singe:101: in function 'drawTitle' rocks/rocks.singe:165: in function 'onOverlayUpdate' ---- Read it the way lesson eight taught you. The file and the line take you to `overlayPrint(25, 9, "BEST " .. best)` in `drawTitle`, and the complaint says something that was `nil` was glued onto a string. `..` joins two strings, and `nil` is not one. If your lines are laid out differently, your numbers will be too. The line it names is not where the mistake is. Line 101 is where the game finally tripped over it; the mistake was made near the top of the file, where `best` was asked for and nothing was there. That gap between where a bad value is made and where it is noticed is the hardest thing about `nil`, and the reason a default costs one argument and saves an evening. Put the `0` back. === What You Learned * A game is in exactly one state at a time, and true-or-false flags that try to say which one breed combinations that mean nothing. * Keep the state in one variable, with its possible values named once at the top of the file. * `onOverlayUpdate` decides what moves and what is said, by asking the state. * Stopping a game is a matter of not calling the things that move it. * Start every game, the first one included, through one function, and make that function put back everything a game changes -- and nothing else. * Keys belong to the keyboard. Record them in every state. * `saveSet` stores a value under a key, and `saveGet` reads it back on a later run. * `saveFlush` writes the file now, for the things you would hate to lose. * The save goes in the game's own data folder, which `singeGetDataPath` names. Never write beside your script. * `saveGet` with no default answers `nil` when nothing was saved, and `nil` breaks the first frame on a machine that has never run your game. * When you mix `and` and `or`, use parentheses. === Next Time Your game is a game now, and it is a folder that only you know how to start. Lesson thirteen gives it a `games.dat`, so the engine's own menu lists it with a title and a picture, and a player who has never heard of you can start it without typing anything.