Fixing Ludo autoplay and storeStrings


This last page is about cleaning up a number of open issues. We add some further tests, refactor, and repair the broken bits.


Currently the recording Ludo game records moves, but misses the roll+s. To replay a game via the storeString, the empty rolls without any physical moves are needed.

Instead of recording only moves, we must also record the rolls that do not lead to any token moves.

We could introduce a separate Roll class, but the existing Move already has either 1 or 2 token moves. We could simply extend it to also have zero token moves, but ensure that such empty moves are also recorded.


• check where the Move is created

• check that the token moves are initiaized to an empty collection

• make sure that an empty move (roll with no moves) is recorded

• test it

• design a test case (count the number of moves; ask how many tokens are moved)

• maybe subclass the existing tests

• check that the Moves view works as expected also for empty moves

• fix the storeOn: method

• write a test to verify that the storeString produces an equivalent game

• write a test that exercises autoplay after a short, rigged game (some player close to end)

Handling empty moves

This test case shows only 4 moves, but there are actually 3 empty moves (rolls without moving) after the first two moves.

GtLudoRecordingGameExamples new playerAentersAndLandsOnA

The current Move stores a token, but it should also store a player in case no token is moved.

The token could be initialized to a Blank Token (to avoid nil problems)

Renaming privateMove:

We rename privateMove: to moveToken:

Instead we define moveToken: and moveTokenNamed: in the parent, and just redefine moveToken: in the subclass.

We remove moveTokenNamed: from the subclass and run the tests.

(GtExplicitExampleGroup withAll:
	 GtLudoGame package gtExamplesAllContained) runAll

Where to create the Move?

Currently the Move object is only created in the moveToken: method, but we also want one when we roll but don't move.

Whenever a player rolls the die (on their turn), a new Move object should be created. If there are possible tokens to be moved, then we wait for the move to be made before recording the move. Otherwise we immediately record the empty move.

The magic happens in the onRolled method, which is also invoked by callback when the roll: message is explciitly sent.

One problem is that we introduce a fake roll: if a player rolls the die when they shouldn't. This would make the recording logic unnecessarily complex, so instead we introduce a rollback: method to the die that does not signal an update, and use it to rollback invalid die rolls.

We also factor out a helper method updateOnRoll that does the update on a roll, which we can then extend in the subclass.

We run the tests.

(GtExplicitExampleGroup withAll:
	GtLudoGame package gtExamplesAllContained) runAll

We explore the moves of the previous example and now we see the empty moves.

GtLudoRecordingGameExamples new playerAentersAndLandsOnA

However the Move view is broken for empty moves because a blank token has no game.

We add a player to the move and get the game from there.

Expand views to show the Player of a move.

We commit this version.

Adding some tests

We override playerAentersAndLandsOnA and add some more assertions.

GtLudoRecordingGameExamples>>#playerAentersAndLandsOnA playerAentersAndLandsOnA <gtExample> | game | game := super playerAentersAndLandsOnA. self assert: game moves size equals: 7. self assert: game moves last numberOfTokensMoved equals: 2. game moves do: [ :move | self assert: ((0 to: 2) includes: move numberOfTokensMoved) ]. ^ game


We verify that we can now autoplay a recorded game.

game := GtLudoRecordingGameExamples new playerAentersAndLandsOnA.
game autoPlay: 1000.


We patch up the GtLudoRecordingGame>>#storeOn: storeOn: aStream aStream nextPutAll: '('; nextPutAll: self class name; nextPutAll: ' new'. self moves do: [ :move | aStream nextPutAll: ' roll: '; nextPutAll: move roll asString; nextPutAll: ';'. move canMove ifTrue: [ aStream nextPutAll: ' moveTokenNamed: '''; nextPutAll: move token name; nextPutAll: ''';' ] ]. aStream nextPutAll: ' yourself)' method and check if it works.

script := game storeString

We evaluate the script:

Smalltalk compiler evaluate: script

Now we can add a test for this: GtLudoRecordingGameExamples>>#evaluatedStoreStringYieldsSameStorestring evaluatedStoreStringYieldsSameStorestring <gtExample> | game storeString clonedGame | game := self playerAentersAndLandsOnA. storeString := game storeString. clonedGame := Smalltalk compiler evaluate: storeString. self assert: clonedGame storeString equals: storeString. ^ game

Back in time execution

We would like to reconstruct a game just up to a certain point. Let's take the palyed game from earlier.

game := GtLudoRecordingGameExamples new playerAentersAndLandsOnA.
game autoPlay: 1000.

Now let's just replay the game up to a certain point.

movesToPlay := game moves copyFrom: 1 to: 20.
newGame := GtLudoRecordingGame new.
movesToPlay do: [ :move | 
	newGame roll: move roll.
	move canMove ifTrue: [ newGame moveTokenNamed: move token name ] ].

Added GtLudoMove>>#gtReplayedMoveFor: gtReplayedMoveFor: aView <gtView> ^ (aView explicit) title: 'Replay'; priority: 20; stencil: [ self replayGameToHere ] .

How to get the highlighted move in the replayed state?

It should just be:

newGame moves last 

Check the views.

game := GtLudoRecordingGameExamples new playerAentersAndLandsOnA.
game autoPlay: 1000.

Look at the moves, and check the Move, Replay and Highlight views of a given move.