Dutch BlackJack
This is a console app for up to 6 players in the spirit of old text games, however another interface can easily be created by taking Main as an example
The application comes with a basic console runtime using cats.effect.IO which wraps the domain logic enclosed in the other files. A clean and minimalistic functional approach was taken attempting to reuse and compose correct building blocks.
Since the game itself has no side-effects other than the randomness required for shuffling cards, 99% of the code is of the form A => B, the other one percent is using State to simplify immutable state evolution from S1 to S2.
After a small refactor, the code is split into three modules:
- core: With the relevant, clean base classes to be re-used in the other two modules.
- console: Houses a console application which runs within a cats.effect.IO[] monad. The general idea is that here you can find examples of how to re-use the code in the models package to run any number of other interfaces around it. Especially since the notion of GameEvolver is retrofitable over any applicative effect. You can have an AI game engine that simulates various plays and starts evolving branches in parallel since games are always reproducible from any point in their evolution (even shuffling if you choose to re-use the same random number seed).
- rest: Houses a RESTfull http backend which is currently stateful in terms of housing a games repository in memory, but can be extended to operate statelessly via some distributed key-value storage. Here you can notice the ease with which one can re-purpose the core library, in a functional, type-safe way with minimal boilerplate.
The core module has two packages:
-
model: Hoses the business classes / logic for the game. From Cards, and Players to Game and Actions. Here are the combination peaces you can use to evolve lawful games. For reasons of simplicity, I chose not to use a type driven state machines (https://www.idris-lang.org/drafts/sms.pdf) since it would potentially take me too long to write cleanly, but it is a possible improvement moving forward.
In the scaladocs you will find 5 distinct groups:
- Cards: organizing Types and values related to the cards in the game.
- Player: organizing Player related types and values.
- Game: organizing Game related types and values.
- Turn: organizing Types and values related to game play dynamics.
- Actions: organizing Player actions which evolve the game.
-
system: Houses exception types, and utility methods used to help readability and facilitate some common usage scenarios; like pulling a Left: Either[String, A] result in the model class which doesn't use exceptions into a carrier type (like cats.effect.IO) which has MonadError instances that will keep return types simple and error-handling streamlined as necessary.
Proofs are obtainable through a variety of means, and in terms of strongly typed code we have two predominant mechanisms; constructive proofs, through type inhabitance, that follows from the Curry-Howard isomorphism, and proofs by counter-example or test failures. They are complementary in so far as we acknowledge refining types to where we leave no wiggle room for invalid states is often too time consuming (like our decision to avoid writing a path dependent game state machine) and we can get satisfactory results from accepting no obvious counter examples are detectable. For this there are property based tests written for some of the key invariants (laws) that were identified from the business cases.
The repeat helper method is tested to hold its invariants, as it is fundamental our building blocks are sound and we have evidence to the fact.
In case of Play and Actions, their state is appropriately bound by the type system. It is essentially impossible to create invalid actions or plays.
In case of Cards some invariants where added around rank and suit methods, as well as a counter example examination of points calculation.
In case of Players some action invariants where identified around Betting and Standing actions; more invariants should be considered here; potentially around the other actions players can take: hitting and splitting for. They were not written to avoid for sake of time constraints.
In case of Games a similar position as the Player inavariants should be explorer, but it is non-trivial to identify these invariants, and more specific generators should be considered around the game type; since we chose not to be clear enough with our definitions and the types are capable of holding illegal / unreachable states which would potentially lead us down the wrong path of defensively checking for these states both in the test and production code bases; where it is possibly more productive to hone our type signatures to reduce the state space further into the actually valid states for the game. There are evident trade offs here which we are not addressing in the demo code for this version.
After running packageBin in the sbt console project, from any console, given you have java installed, you should be able to simply run:
bin/eenentwintigen-console (or bin/eenentwintigen-console.bat for windows)
As this has the necessary command line
After running packageBin in the sbt rest project, from any console, given you have java installed, you should be able to simply run:
bin/eenentwintigen-rest (or bin/eenentwintigen-rest.bat for windows)
As this has the necessary command line
sbt console/runPlays a console gamesbt rest/runStarts an HTTP Rest backendsbt testruns the testssbt unidocruns the integrated scaladoc for all modulessbt universal:packageBinGenerates the complete zip file with included sources
This project sbt-revolver so
sbt rest/~reStartoffers a quick way to re start the HTTP backend as you are editting code.