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
- 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.
- Rules. What is allowed, and what happens after it? "The pawn moves as many squares as the die shows."
- Input. What comes from outside? The player's choice or the number on the die.
- Loop. Turn after turn: input, update the state, show it. Then again.
- 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 = 0turn = 1while field < 20: roll = random.randint(1, 6) field = min(field + roll, 20) print(f"Turn {turn}: roll {roll}, square {field}") turn += 1print("Finish! The pawn reached square 20.")Step by step, in words
- A generator with seed 42 and a pawn on square 0. The turn count starts at 1.
- Turn 1: the input is a roll of 6, the state becomes square 6, the screen shows the line.
- Turn 2: the input is a roll of 1, the state becomes square 7, the screen shows the line.
- Turn 3: the input is a roll of 1, the state becomes square 8, the screen shows the line.
- Turn 4: the input is a roll of 6, the state becomes square 14, the screen shows the line.
- Turn 5: the input is a roll of 3, the state becomes square 17, the screen shows the line.
- Turn 6: the input is a roll of 2, the state becomes square 19, the screen shows the line.
- 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.
- 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);}3 and 34 and 41 and 13 and 31 and 1Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++){ int a = first.Next(1, 7); int b = second.Next(1, 7); Console.WriteLine(a + " and " + b);}5 and 51 and 11 and 14 and 42 and 2from random import Random
first = Random(42)second = Random(42)for i in range(5): a = first.randint(1, 6) b = second.randint(1, 6) print(f"{a} and {b}")6 and 61 and 11 and 16 and 63 and 3function makeRandom(seed) { return () => { seed = (seed + 0x9e3779b9) | 0; let z = seed; z = Math.imul(z ^ (z >>> 16), 0x85ebca6b); z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35); return ((z ^ (z >>> 16)) >>> 0) / 4294967296; };}
const first = makeRandom(42);const second = makeRandom(42);for (let i = 0; i < 5; i++) { const a = Math.floor(first() * 6) + 1; const b = Math.floor(second() * 6) + 1; console.log(`${a} and ${b}`);}2 and 26 and 61 and 15 and 51 and 1The 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");Turn 1: roll 5, square 5Turn 2: roll 3, square 8Turn 3: roll 4, square 12Turn 4: roll 5, square 17Turn 5: roll 5, square 18Turn 6: roll 5, square 17Turn 7: roll 5, square 18Turn 8: roll 6, square 16Turn 9: roll 1, square 17Turn 10: roll 1, square 18Turn 11: roll 1, square 19Turn 12: roll 1, square 20Exactly 20 after 12 turnsstatic int Move(int field, int roll){ int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static bool IsFinished(int field){ return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!IsFinished(field)){ turn++; int roll = random.Next(1, 7); field = Move(field, roll); Console.WriteLine("Turn " + turn + ": roll " + roll + ", square " + field);}Console.WriteLine("Exactly 20 after " + turn + " turns");Turn 1: roll 3, square 3Turn 2: roll 6, square 9Turn 3: roll 4, square 13Turn 4: roll 1, square 14Turn 5: roll 3, square 17Turn 6: roll 5, square 18Turn 7: roll 1, square 19Turn 8: roll 6, square 15Turn 9: roll 6, square 19Turn 10: roll 6, square 15Turn 11: roll 3, square 18Turn 12: roll 6, square 16Turn 13: roll 3, square 19Turn 14: roll 1, square 20Exactly 20 after 14 turnsdef 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
random = Random(7)field = 0turn = 0while not is_finished(field): turn += 1 roll = random.randint(1, 6) field = move(field, roll) print(f"Turn {turn}: roll {roll}, square {field}")print(f"Exactly 20 after {turn} turns")Turn 1: roll 3, square 3Turn 2: roll 2, square 5Turn 3: roll 4, square 9Turn 4: roll 6, square 15Turn 5: roll 1, square 16Turn 6: roll 1, square 17Turn 7: roll 5, square 18Turn 8: roll 1, square 19Turn 9: roll 3, square 18Turn 10: roll 5, square 17Turn 11: roll 1, square 18Turn 12: roll 5, square 17Turn 13: roll 2, square 19Turn 14: roll 1, square 20Exactly 20 after 14 turnsfunction move(field, roll) { const next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
function isFinished(field) { return field === 20;}
const random = makeRandom(7);let field = 0;let turn = 0;while (!isFinished(field)) { turn++; const roll = Math.floor(random() * 6) + 1; field = move(field, roll); console.log(`Turn ${turn}: roll ${roll}, square ${field}`);}console.log(`Exactly 20 after ${turn} turns`);Turn 1: roll 1, square 1Turn 2: roll 3, square 4Turn 3: roll 6, square 10Turn 4: roll 4, square 14Turn 5: roll 3, square 17Turn 6: roll 2, square 19Turn 7: roll 5, square 16Turn 8: roll 2, square 18Turn 9: roll 2, square 20Exactly 20 after 9 turnsNeither 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))10 + 4 = 14. 17 + 3 = 20, exactly. 18 + 5 = 23 is 3 past 20, so the pawn bounces back to 20 − 3 = 17.
Try it
Find the bug
The pawn should move forward, but the program prints square 0 on every turn:
Turn 1: roll 3, square 0Turn 2: roll 2, square 0Turn 3: roll 4, square 0Which line is the bug on?
Pick the line with the bug.
move returns the new square, but the result is thrown away. The function cannot change the caller's field: it only gets the value 0, not the caller's field itself. You need field = move(field, roll). If a line like this sits in while not is_finished(field):, the game never ends.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== compares. A single = is assignment, and here Python stops before it even starts, with SyntaxError. >= will also work, because move never returns more than 20, but == states the rule more clearly: the end is exactly on 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.
Ana: roll 2, square 2Boris: roll 5, square 5Ana: roll 5, square 7Boris: roll 2, square 7Ana: roll 3, square 10Boris: roll 5, square 12Ana: roll 4, square 14Boris: roll 6, square 18Ana: roll 5, square 19Boris: roll 1, square 19Ana: roll 5, square 16Boris: roll 1, square 20Boris wins2. 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.
50251218151650: Lower25: Lower12: Higher18: Lower15: Higher16: CorrectGuessed in 6 triesThe 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.
3XOXOXOOOXXXOOOOX.XXOXXOOOXXX winsO winsNo winnerMore exercises
- Traps. Square 13 sends the pawn back to square 5. Add the rule to
moveand 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
winnerto check whether the game is over.
Test