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 methods, the input is Scanner or Random, the loop is while or for, and the end is the loop's condition.
See it
Turn after turn
Random random = new Random(42);int field = 0;for (int turn = 1; field < 20; turn++) { int roll = random.nextInt(6) + 1; field = Math.min(field + roll, 20); System.out.println("Turn " + turn + ": roll " + roll + ", square " + field);}System.out.println("Finish! The pawn reached square 20.");Step by step, in words
- A generator with seed 42 and a pawn on square 0.
- Turn 1: the input is a roll of 3, the state becomes square 3, the screen shows the line.
- Turn 2: the input is a roll of 4, 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 3, the state becomes square 11, the screen shows the line.
- Turn 5: the input is a roll of 1, the state becomes square 12, the screen shows the line.
- Turn 6: the input is a roll of 2, the state becomes square 14, the screen shows the line.
- Turn 7: the input is a roll of 6, the state becomes square 20, the screen shows the line. Square 20: the condition 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.
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.
That is why in these lessons Random gets a starting number, a seed. The same seed gives the same sequence, every time:
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:
new Random(). Then the generator picks a seed from the clock 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 method that returns the new square. The end is a method 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 isFinished prints anything. They only calculate. The printing is in main.
Why do it this way? You can call a method that returns a number with any values you like and check the result: move(17, 5) has to be 18. A method that prints can only be checked by eye. And if tomorrow the game moves from the console to a window, the rules stay the same; only the display changes.
Try it
Where will the pawn land?
What will the program print?
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;}
System.out.println(move(10, 4));System.out.println(move(17, 3));System.out.println(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 5, square 0Turn 2: roll 3, 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 method cannot change the caller's field: the parameter is a copy of the value. You need field = move(field, roll);. If a line like this sits in while (!isFinished(field)), the game never ends.Try it
When does the game end?
What is missing so that the end is exactly on square 20?
static boolean isFinished(int field) {
return field 20;
}== compares. A single = is assignment and does not compile here. >= 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 methods that return a value, and the printing goes in main.
1. A race for two. Ana and Boris take turns rolling, with new Random(3). The "exactly 20" rule applies, with move from the lesson. Their squares are in an array int[2]. Whoever reaches 20 first wins.
Ana: roll 3, square 3Boris: roll 3, square 3Ana: roll 1, square 4Boris: roll 2, square 5Ana: roll 1, square 5Boris: roll 1, square 6Ana: roll 4, square 9Boris: roll 5, square 11Ana: roll 2, square 11Boris: roll 2, square 13Ana: roll 4, square 15Boris: roll 5, square 18Ana: roll 6, square 19Boris: roll 5, square 17Ana: roll 4, square 17Boris: roll 5, square 18Ana: roll 1, square 18Boris: roll 4, square 18Ana: roll 5, square 17Boris: roll 2, square 20Boris wins2. Guess the number. The program secretly picks a number from 1 to 100 with new 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 method String hint(int guess, int secret) that returns the text.
50758894979910050: Higher75: Higher88: Higher94: Higher97: Higher99: Higher100: CorrectGuessed in 7 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 method char winner(char[][] board) that returns X, O, or . if there are no three in a row, column or diagonal.
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