Tracking the state of the Ludo game play

TL;DR

We add various examples to test the progress of a game.

Tracking the state of the game play

The game should always report the state of the game play (Player X rolls the die; Play X moves token Y or Z; Player X has won).

For this, the game needs to know if the Die has been rolled since the last move or not. It therefore also needs to subscribe to rolls of the die.

We start to prepare examples to track the game state.

emptyGame
	<gtExample>
	| game |
	game := self gameClass new.
	self assert: game isOver not.
	self assert: game winner equals: 'No one'.
	self assert: game currentPlayer name equals: 'A'.
	self assert: game playerToRoll.
	self assert: game playerToMove not.
	^ game
    

We add a slot needToRollDie and update it after a move, and after a roll of the die, using the GtLudoGame>>#requireDieRoll requireDieRoll needToRollDie := true. method.

(Browse the senders of requireDieRoll .)

We prepare several more examples.

Player A rolls a 6.

playerArolls6
	<gtExample>
	| game |
	game := self emptyGame.
	game roll: 6.
	self assert: game currentPlayer name equals: 'A'.
	self assert: game playerToRoll not.
	self assert: game playerToMove.
	self
		assert: (game tokensToMove collect: #name) asSet
		equals: { 'A'. 'a' } asSet.
	^ game
    

Player A enters its token A.

playerAentersTokenA
	"Setup for 2. Entering play when there is a token of the same player on the start square (ie after rolling a 6 twice)."
	<gtExample>
	| game |
	game := self playerArolls6.
	game moveTokenNamed: 'A'.
	self assert: (game positionOfTokenNamed: 'A') equals: 1.
	self assert: game currentPlayer name equals: 'A'.
	self assert: game playerToRoll.
	self assert: game playerToMove not.
	self assert: (game tokensToMove collect: #name) asSet equals: Set new.
	^ game
    

Player A moves its token A.

playerAmovesTokenA
	<gtExample>
	| game |
	game := self playerAentersTokenA.

	game roll: 5.
	self
		assert: (game tokensToMove collect: #name) asSet
		equals: { 'A' } asSet.

	game moveTokenNamed: 'A'.
	self assert: (game positionOfTokenNamed: 'A') equals: 6.

	self assert: game currentPlayer name equals: 'B'.
	self assert: game playerToRoll.
	self assert: game playerToMove not.

	self assert: (game tokensToMove collect: #name) asSet equals: Set new.

	^ game
    

And so on.

Different routes for each player

To compute the moves it is useful to have the differents routes for each of the player's tokens, since they start and end on different squares. We add a suitable test to the empty board game.

emptyBoard

	<gtExample>
	| board |
	board := GtLudoGame new.
	self assert: board players size equals: 4.
	self assert: board squares size equals: 40.
	self assert: board tokens size equals: 8.
	board tokens do: [ :token | self assert: token isInStartState ].
	board squares do: [ :square | self assert: square isEmpty ].

	board players do: [ :player | 
		| route |
		route := board routeFor: player.
		self assert: route size equals: 42.
		self assert: route first kind equals: #initial.
		self assert: route last kind equals: #goal.
		self assert: route nextToLast kind equals: #goal ].

	^ board
    

We can also inspect the route for a given player. Here is the route for player B:

board := GtLudoBoardExamples new boardWith2PlayingPlayers.
board routeFor: board players second
  

Now that we have implemented GtLudoGame>>#computeTargetFor: computeTargetFor: aToken "Given a token to move, determine which square it should move to. There are 3 cases for the target square." | route targetIndex | route := self currentRoute. "(1) a token enters play on the first square of the route" (self die topFace = 6 and: [ aToken isInStartState ]) ifTrue: [ aToken enterPlay. ^ route first ]. self assert: aToken isInPlay description: 'Token ' , aToken name , ' is not in play'. "(2) a token in play moves forward to another square on the route" targetIndex := (route indexOf: aToken square) + self die topFace. targetIndex <= route size ifTrue: [ ^ route at: targetIndex ]. "(3) the roll would go past the end of the route (we stay where we are)" ^ aToken square and GtLudoGame>>#moveToken: moveToken: aToken | 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 ] ] whileTrue: [ targetSquare token player = aToken player ifTrue: [ | 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 requireDieRoll. self feedback: self gameState. self notifyGameUpdated , we can actually play the game by clicking on the die and sending move: messages.

GtLudoGameExamples new playerAmovesTokenA
  

Clicking to move a token

When we double-click on a square (element) we ask the square to announce to the game the interest in moving the token occupying that square (if there is one). Here we can click on either A or a.