The game consists of 3 scenes. A scene is just a screen that serves a specific purpose. The scenes are, in order:
- Create character
- Game
- Result
They show in succession, such that the player can create their character, play the game, and view how far they got.
Brick exposes a super handy API for working with program state, and state transitions. However, this implementation has its limits. In the scene for the game itself, the Brick app is represented by the following type:
app :: Scene GameState ≡ App GameState () Name
Now, all of app
's fields are dependent upon the type arguments one provides to the App
type.
With the above implementation, all of app
's constituents have a well-defined type (which we cannot change).
The issue arises...
How can we implement monads over
GameState
without severely compromising code quality?
How about doing something like
type Scene a = forall m. Monad m => ReaderT Config m a
Well, though it would technically work, this has a few problems:
- Lots of refactoring! Anything that touches the game state needs to be rewritten to more convoluted code
- Not a common approach: I searched a fair bit, and found no programs using Brick with this approach
While learning Brick, I have used this project.
It uses a series of custom type aliases to model a StateT
on top of the GameState
.
This seemed nice, but the eventual implementation had the following issues:
- The Tetris game model is different from mine, requiring severe refactoring
- The implementation did not solve my problem
- The implementation was quite overengineered for my needs
Yes! We did it! Though I found no direct guidance for this approach, the following threads hinted me to this approach:
We can thus create a monad stack over EventM
(which, notably is a monad), for the following type:
type GameEvent a = ReaderT Config (EventM Name GameState) a
This fits super well with my event-based approach!
With this approach, we can access the entire monad stack in each GameEvent a
.
At the time of writing, we only use ReaderT
, but there may be more monads at a later point.
This allows us to get, for example, difficulty setting and ASCII-only mode from the environment in all game events.
There is, however, one limitation that I was not able to work around.
Anything outside of the game events cannot access the Reader
environment.
One such function is appDraw
, which naturally needs to know whether the ASCII-only
flag is enabled.
We work around this by explicitly passing the flag into the function.
In this project, we use QuickCheck
for property based testing.
The test suite is quite limited, but demonstrates a setup and testing the properties of a binary tree.
To run tests, you can either:
- Run
cabal test --enable-tests
, which will run tests once - Run
cabal configure --enable-tests
in order to permanently enable tests. This creates acabal.project.local
file (which is ignored by Git)
Thanks to James for the demo!