Skip to content
wannacode

How to think about a game

Problem

Where does a game start?

You want to write a game. Say, a dice race: a pawn starts on square 0, you roll a die and it moves forward until it reaches square 20.

You open the editor and... what do you write first? The menu? The die? The board? If you start with everything at once, an hour later you have a lot of code that still does not run.

It helps to ask the game five questions before you write any code.

The idea

Five questions for every game

  1. State. Which numbers describe the world right now? In the race it is the pawn's square and the turn number. In chess it is where every piece stands and whose turn it is.
  2. Rules. What is allowed, and what happens after it? "The pawn moves as many squares as the die shows."
  3. Input. What comes from outside? The player's choice or the number on the die.
  4. Loop. Turn after turn: input, update the state, show it. Then again.
  5. End. When is the game over, and who wins? "The pawn is on square 20."

The answers are almost the finished code: the state is variables, the rules are functions, the input is ask() or a random number generator, the loop is while or for, and the end is the loop's condition.

See it

Turn after turn

const random = makeRandom(42);
let field = 0;
for (let turn = 1; field < 20; turn++) {
const roll = Math.floor(random() * 6) + 1;
field = Math.min(field + roll, 20);
console.log(`Turn ${turn}: roll ${roll}, square ${field}`);
}
console.log("Finish! The pawn reached square 20.");
Step by step, in words
  1. A generator with seed 42 and a pawn on square 0.
  2. Turn 1: the input is a roll of 2, the state becomes square 2, the screen shows the line.
  3. Turn 2: the input is a roll of 6, the state becomes square 8, the screen shows the line.
  4. Turn 3: the input is a roll of 1, the state becomes square 9, the screen shows the line.
  5. Turn 4: the input is a roll of 5, the state becomes square 14, the screen shows the line.
  6. Turn 5: the input is a roll of 1, the state becomes square 15, the screen shows the line.
  7. Turn 6: the input is a roll of 4, the state becomes square 19, the screen shows the line.
  8. Turn 7: the input is a roll of 1, the state becomes square 20, the screen shows the line. Square 20: the condition field < 20 is now false.
  9. The loop is over because the square reached 20. That is the end of the game.

The whole game is one loop. Each turn, input comes in (the die), the state is updated (the new square) and shown (one line on the screen). The loop's condition is the end: while the square is below 20, there is another turn.

Math.min keeps the pawn on the board: if the die sends it past 20, it stops on 20.

More precisely, if you are curious
  • This game is turn-based: nothing happens until the next input comes. Chess, card games and most of the games in this section work like that.
  • Real-time games (shooters, car races) also have a loop, but it does not wait for the player. It runs 60 times a second: it reads the keys that are pressed right now, moves everything by the time that has passed and draws the frame. If the player presses nothing, the world still moves.

The idea

Same seed, same game

If the rolls are truly random, every run is a different game. Then how do you check that the game works? You do not know what to expect.

Math.random() will not do: you cannot give it a starting number. That is why in these lessons the generator is a small function makeRandom(seed) that gets a starting number, a seed. It returns a function that gives the next number from 0 to 1 (not including 1) each time you call it, just like Math.random(). The same seed gives the same sequence, every time.

For now, copy makeRandom as it is, like ask(). Inside are bitwise operations we will not need. The die is Math.floor(random() * 6) + 1: a number from 0 to 6 (not including 6), cut down to a whole number, plus 1.

Random first = new Random(42);
Random second = new Random(42);
for (int i = 0; i < 5; i++) {
int a = first.nextInt(6) + 1;
int b = second.nextInt(6) + 1;
System.out.println(a + " and " + b);
}
Output
3 and 3
4 and 4
1 and 1
3 and 3
1 and 1

The two generators with 42 give the same numbers. Run the program again and they will be the same. That is why the examples and exercises here have exact output that can be checked.

Look at the output in the other tabs too. Each language has its own generator: from the same seed they give different numbers. That is why examples with random numbers have output for each language.

More precisely, if you are curious
  • When the game is ready for real players, you swap the generator: const random = Math.random;. The rest of the code does not change, because both are called the same way, random(). Then every game is different.
  • makeRandom is not part of the standard library. In a real project a seeded generator comes from a package, such as seedrandom.
  • A common practice is to print the seed at the start of the game. If a player finds a bug, you can play exactly the same game with their seed.

The idea

The smallest working version first

The program from the scene is a whole game in about ten lines. It has no second player, no menu and no rules except "move forward". But it runs from start to end, and it shows that the loop and the end are right.

From there the game grows one rule at a time. The second rule: the pawn has to land on square 20 exactly. If the die sends it further, it bounces back by as many squares as it went over. From 17 with a roll of 5 it reaches 20 and goes back to 18.

You add a new rule only when the previous one works. If something breaks, you know it is in the last thing you wrote.

The idea

Logic returns, the screen prints

The "exactly 20" rule lives in a function that returns the new square. The end is a function that returns true or false:

static int move(int field, int roll) {
int next = field + roll;
if (next > 20) {
return 20 - (next - 20);
}
return next;
}
static boolean isFinished(int field) {
return field == 20;
}
Random random = new Random(7);
int field = 0;
int turn = 0;
while (!isFinished(field)) {
turn++;
int roll = random.nextInt(6) + 1;
field = move(field, roll);
System.out.println("Turn " + turn + ": roll " + roll + ", square " + field);
}
System.out.println("Exactly 20 after " + turn + " turns");
Output
Turn 1: roll 5, square 5
Turn 2: roll 3, square 8
Turn 3: roll 4, square 12
Turn 4: roll 5, square 17
Turn 5: roll 5, square 18
Turn 6: roll 5, square 17
Turn 7: roll 5, square 18
Turn 8: roll 6, square 16
Turn 9: roll 1, square 17
Turn 10: roll 1, square 18
Turn 11: roll 1, square 19
Turn 12: roll 1, square 20
Exactly 20 after 12 turns

Neither move nor isFinished prints anything. They only calculate. The printing is in the code below the functions.

Why do it this way? You can call a function that returns a number with any values you like and check the result: move(17, 5) has to be 18. A function that prints can only be checked by eye. And if tomorrow the game moves from the console to a window or the browser, the rules stay the same; only the display changes.

Try it

Where will the pawn land?

What will the program print?

function move(field, roll) {
const next = field + roll;
if (next > 20) {
return 20 - (next - 20);
}
return next;
}
function isFinished(field) {
return field === 20;
}
console.log(move(10, 4));
console.log(move(17, 3));
console.log(move(18, 5));
Answer

Try it

Find the bug

The pawn should move forward, but the program prints square 0 on every turn:

Output
Turn 1: roll 1, square 0
Turn 2: roll 3, square 0
Turn 3: roll 6, square 0

Which line is the bug on?

Pick the line with the bug.

Try it

When does the game end?

What is missing so that the end is exactly on square 20?

function isFinished(field) {
  return field  20;
}

Exercise

Three small games

Solve these in your editor. The logic goes in functions that return a value, and the printing goes in the code below them.

1. A race for two. Ana and Boris take turns rolling, with makeRandom(3). The "exactly 20" rule applies, with move from the lesson. Their squares are in an array [0, 0]. Whoever reaches 20 first wins.

Output
Ana: roll 6, square 6
Boris: roll 2, square 2
Ana: roll 2, square 8
Boris: roll 3, square 5
Ana: roll 3, square 11
Boris: roll 1, square 6
Ana: roll 1, square 12
Boris: roll 2, square 8
Ana: roll 3, square 15
Boris: roll 1, square 9
Ana: roll 2, square 17
Boris: roll 4, square 13
Ana: roll 4, square 19
Boris: roll 6, square 19
Ana: roll 1, square 20
Ana wins

2. Guess the number. The program secretly picks a number from 1 to 100 with makeRandom(2026). The player types guesses, one per line. After each one the program says "Higher", "Lower" or "Correct". At the end it says how many tries it took. The hint is a function hint(guess, secret) that returns the text.

Input
50
75
88
81
84
86
87
Output
50: Higher
75: Higher
88: Lower
81: Higher
84: Higher
86: Higher
87: Correct
Guessed in 7 tries

The input is different for each language, because their generators pick a different secret number from the same seed.

3. Who won at tic-tac-toe? The first line is the number of boards. Each board is three lines of three characters: X, O or . for an empty square. Write a function winner(board) that returns X, O, or . if there are no three in a row, column or diagonal.

The board can be an array of the three lines you read: each line is text, and board[r][c] is the character in row r, column c. JavaScript has no separate type for one character, like char in Java: a character is text of length 1.

Input
3
XOX
OXO
OOX
XXO
OOO
X.X
XOX
XOO
OXX
Output
X wins
O wins
No winner

More exercises

  • Traps. Square 13 sends the pawn back to square 5. Add the rule to move and run the race again.
  • Print the seed. Make the seed a variable, print it at the start and run the game with three different seeds.
  • Tic-tac-toe for two. The players type the row and column of their move. After each move, show the board and use winner to check whether the game is over.

Test

Check yourself

1. What is the state of the dice race?
2. Why does the game say makeRandom(42) and not Math.random?
3. Why is the move rule a function that returns the new square, and not a function that prints?
4. What is a good first version of a game?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·