In the previous episode I implemented the game rules, but did not test them. I also had some reservations about some code I wrote, but predicted it would be mostly right, even without tests. Today’s episode is about pretty printing and operational !
Minor modifications since last time
- I refactored the
getCardVictoryfunctions so that they are now pure. I toyed with the idea of having a monad morphism (I learned today it was called like that to integrate
Reader GameStateactions in the
MonadState GameStatefunctions, but this was not warranted as the functions are so simple.
- I refactored neighborhood relationship so that it encodes more invariants. A player now must have a left and right neighbor. They might be invalid though.
- I refactored the type of the interfaces between the game rules and the players, so that you can’t pass empty lists where they are forbidden. I was later told this type already existed in semigroups.
Why pretty printing ?
I hinted heavily last time that there would be a dedicated pretty printer. An example of such an implementation is in
the ansi-wl-pprint package. It introduces functions and combinators
that let you easily create a
Doc value that will look neat on your screen.
Unfortunately, in order to properly support all text-based backends (IRC, XMPP, email, console) it doesn’t seem to be possible to reuse an existing printer. For example, the color set between all these backends is quite distinct, and some are even capable of printing pictures. I tried to engineer one that would be at the same time flexible, easy to use and good-looking an all backends. Time will tell if this was a success.
I will not give a dissertation on the subject, and have copied the interface from other pretty printing libraries. I will just give some implementation details here.
Basic pretty printing types
Speaking of stealing from other pretty printers, I really should have looked at their code too ! Here are my basic types:
1 2 3 4 5 6 7
So you basically have all “elements” in
PrettyElement, and they can be appended in a monoidal fashion in a
PrettyDoc, which is just a
Seq PrettyElement. This is a very inelegant decision, and I will be sure to
refactor it for the next episode ! Looking at another implementation,
it is clear that a single type was required, and that the Monoidal structure could be achieved by adding
Cat constructors. There is a reason I wrote my type like this though, and it is related to how I intended to solve the
problem of backends with poor or no support for multiline messages, but this will featured in another episode !
Specific design choices
I decided to directly encode the game entities as part of the pretty printing types. That should be obvious from the
list of elements. A
Neighbor or even a
CardType are directly representable, so that the backends can optimize their
Other than that, the code is pretty boring.
A pretty-pretty printer ?
My first backend will be the console, as it will not have any networking or concurrency problems to solve. I used the
aforementioned ansi-wl-pprint package, and wrote a pretty instance
PrettyDoc. This leads to strange code such as
print (PP.pretty (pe something)).
Implementing the GameMonad
During the last episode, I wrote all the rules in an abstract monad that is an instance of
GameMonad, meaning it
featured a few functions for interacting with the players. I took a typeclass approach so that I could start writing the
rules without worrying about the actual implementation of this abstract monad.
Now that the rules are written, it is time to give them a try. In order to do so, I ditched the typeclass, and
expressed it in terms of
ProgramT, from the operational
package. It only takes a few steps to refactor :
The instructions GADT
You must start by writing all the operations that must be supported as a GADT.
We previously had :
1 2 3 4 5 6 7 8 9 10 11
And now have :
1 2 3 4 5 6 7 8
So … there have been some choices going on here. First of all, we need to support all the features we previously had,
MonadError and four game-specific functions. You can spot these four functions quite easily
(along with a new one, which will be covered in a minute). We get
MonadError in the following
1 2 3 4 5
I decided to use the monad transformer
ProgramT over a base
State GameState monad, but encode the error part with
the provided instructions. It would have been easier to encode the state part that way, except I don’t know how to write
an instance for
ProgramT (see this post comment).
The interaction functions no longer have a
GameState in their types, because the interpreter will have access to the
state when decoding this instruction, so it is not necessary to pass it here too.
Mechanically refactor all mentions of GameMonad
Now all you have to do is to replace all type signatures that looked like :
Write an interpreter
I decided to write a generic interpreter, that takes a group of functions
in some monad
m, a base
GameState, and gives you a function that computes any
GameMonad a expression in the
monad. The implementation is pretty obvious, and not very interesting, but it should be easy to write backends now.
Perhaps of interest is the fact that the game state is explicitly passed as a parameter all over the place, so it can be passed to the backends at the interpreter level.
A pure backend
The easiest backend to write is a pure one, with no real player interaction. I could have used
Identity as the base
monad, but instead opted for
State StdGen. That way, I can easily have the “players” play random cards, which will
help with testing.
The implementation is also nothing special, but
made me write a lot of code to support it. In
allowableActions function is pretty tricky, and is not entirely satisfying. Given a game state, a
player name and a list of cards in his hands, it gives a list of all the non obviously stupid legal actions that are
available. It does so in the most direct way, enumerating all possible combinations of resources, neighbor resources,
exchanges, etc. that would work. Then it removes all duplicates, and the actions that are obviously too expensive.
Fortunately, all this code will also be used by the other backends.
So … are there bugs yet ?
I wrote a simple test that checks for errors. Theoretically, the pure backend should always result in games that end well
(we should get a
Right ... instead of a
Left rr. So I wrote a simple property-based test that gets an arbitrary seed
and number of players (between 3 and 7), runs a pure game and checks its result.
And there were runtime errors !
AddMaphad an infinite loop.
allowableActionsfunction sometimes returned no valid actions. I forgot to always add the possibility to drop a card …
To prevent the second case from happening again, I wrote the “prepend drop actions” before the big case statement,
and modified the type of the
askCardSafe function so that it can’t accept an empty list.
This means that if I introduce another bug in
allowableActions, I should get a
Left ... instead of a runtime
There also was a “rule” bug, due to the fact that I had not understood a rule correctly. Basically, I use a fictional 7Th round to emulate the efficiency capability, but there should be no “hand rotation” before that turn. I fixed it wrong once, and then properly. However, I did not discover nor fix this bug because of tests.
The console backend
The opponents still play randomly, which explains the kind of results depicted below, but it is a genuine pleasure to finally play !
I also realized when using the console backends that the messaging functions, while generic, would probably not work
well on all backends. I decided to include more specialized functions, such as
ActionsRecap, which can be passed a map
of all the actions the players undertook in a turn. The current version also lacks a way of getting the results of the
poacher wars between the ages, but that should be trivial to add.
Next time should get more interesting, as I will try to write an interesting backend. It will be a bit harder to design because I want players using distinct backends to be able to participate in the same game.