Introducing rules to the Ludo Board Game
Now that we have a low-level interface for managing the game state, we start to incrementally introduce the game rules.
Recall that originally we introduced separate
GtGame classes, where the former managed the game state while the latter implemented the rules of the game. These were later unified into a single
Object subclass: #GtLudoGame
instanceVariableNames: 'players squares startSquares goalSquares die announcer feedback winner needToRollDie lastDieRolled playerQueue routeCache'
class. In the text below, we still refer at times to the
Game and the
Board as separate entities, though they are now a single class.
The Board Game
The game offers the high-level interface to the board, and interprets the rules.
It knows which player's turn it is, knows what moves are possible, knows what happens on a move, and knows when the game is over.
A Move consists of (1) the Die being rolled, (2) the current player selecting a token to move.
We can encapsulate a Move as a Die value plus a Token name.
A Move may or may not be valid.
If a 6 is rolled, and the player has a token in the start state, it must be put into play. Otherwise a token in play may be moved.
A token that is put into play moves to the start square.
After a 6, the same player can roll again (stays the current player).
If the landing square is occupied, then
(1) if the token belongs to the same player, the token lands on the next square
(2) otherwise if it belongs to another player, that token is sent back to the start position
Let's start with a Game as a holder for a Board.
At first, the board was a slot holding an instance of Board. Later we made the Game a subclass of Board, pushed down or merged all the methods, and eliminated the Board class and the redundant slot.
We add forwarding views for the Board, players and squares.
We keep track of the current player in a rotating collection in
^ self playerQueue first
For testing purposes, it is easier if we can explicitly set the value of the Die rolled.
game roll: 6
Now we can start to script some moves.
game := GtLudoGame new.
game roll: 6. game moveTokenNamed: 'A'.
We start to implement the various game rules in the
| targetSquare |
self assert: self playerToMove description: 'Roll the die first!'.
self assert: aToken player = self currentPlayer description: 'Token ', aToken name, ' does not belong to current player ', self currentPlayer name.
"We first compute the target square, and then decide what to do."
targetSquare := self computeTargetFor: aToken.
"If the target square is occupied, either:
(a) if the token belongs to another player, that token is sent back to its start state and we land there
(b) if the token belongs to the same player, then we try to land on the next square (again two cases).
In case (b) we must iterate, and if no squares are left, we fall back to the current square."
[ targetSquare notEmpty and: [ targetSquare ~= aToken square ] ]
targetSquare token player = aToken player
| route targetIndex |
route := self currentRoute.
targetIndex := (route indexOf: targetSquare) + 1.
targetIndex <= route size
ifTrue: [ targetSquare := route at: targetIndex ]
ifFalse: [ targetSquare := aToken square ] ]
ifFalse: [ self sendToStart:targetSquare token ] ].
aToken goToSquare: targetSquare.
"At the end, we switch to the next player unless we rolled a 6."
self die topFace = 6 ifFalse: [ self nextPlayer ].
self feedback: self gameState.
method. Note that the logic is split into two parts — computing the target square to land on, and deciding what happens when a token lands on that square.
This is by far the most complex method in the whole package, excluding examples.
classes := GtLudoGame package classes removeAll: (GtLudoGame package classTagNamed: 'Examples') classes; yourself. ((classes flatCollect: #methods) asOrderedCollection sort: [ :a :b | a linesOfCode > b linesOfCode ]) copyFrom: 1 to: 10