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 input() or Random, the loop is while or for, and the end is the loop's condition.

See it

Turn after turn

random = Random(42)
field = 0
turn = 1
while field < 20:
roll = random.randint(1, 6)
field = min(field + roll, 20)
print(f"Turn {turn}: roll {roll}, square {field}")
turn += 1
print("Finish! The pawn reached square 20.")
Step by step, in words
  1. A generator with seed 42 and a pawn on square 0. The turn count starts at 1.
  2. Turn 1: the input is a roll of 6, the state becomes square 6, the screen shows the line.
  3. Turn 2: the input is a roll of 1, the state becomes square 7, the screen shows the line.
  4. Turn 3: the input is a roll of 1, the state becomes square 8, the screen shows the line.
  5. Turn 4: the input is a roll of 6, the state becomes square 14, the screen shows the line.
  6. Turn 5: the input is a roll of 3, the state becomes square 17, the screen shows the line.
  7. Turn 6: the input is a roll of 2, the state becomes square 19, the screen shows the line.
  8. Turn 7: the input is a roll of 2, the state becomes square 20, the screen shows the line. Square 20: the condition while 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.

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

Python has no three-part for like Java. So the turn is counted with turn, and the loop is while: while the square is below 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.

That is why in these lessons the generator is Random from the random module, and it gets a starting number, a seed. The same seed gives the same sequence, every time. randint(1, 6) gives a whole number from 1 to 6, both included:

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 remove the seed: Random(). Then the generator picks a seed by itself, and every game is different.
  • 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 is_finished 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.

The new square in move is new_field, not next: in Python next is the name of a built-in function, and it is better not to hide it.

Try it

Where will the pawn land?

What will the program print?

def move(field, roll):
new_field = field + roll
if new_field > 20:
return 20 - (new_field - 20)
return new_field
def is_finished(field):
return field == 20
print(move(10, 4))
print(move(17, 3))
print(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 3, square 0
Turn 2: roll 2, square 0
Turn 3: roll 4, 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?

def is_finished(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 Random(3). The "exactly 20" rule applies, with move from the lesson. Their squares are in a list [0, 0]. Whoever reaches 20 first wins.

Output
Ana: roll 2, square 2
Boris: roll 5, square 5
Ana: roll 5, square 7
Boris: roll 2, square 7
Ana: roll 3, square 10
Boris: roll 5, square 12
Ana: roll 4, square 14
Boris: roll 6, square 18
Ana: roll 5, square 19
Boris: roll 1, square 19
Ana: roll 5, square 16
Boris: roll 1, square 20
Boris wins

2. Guess the number. The program secretly picks a number from 1 to 100 with Random(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
25
12
18
15
16
Output
50: Lower
25: Lower
12: Higher
18: Lower
15: Higher
16: Correct
Guessed in 6 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 a list of the three lines you read: each line is text, and board[r][c] is the character in row r, column c. Python 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 Random(42) and not 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 ·