A sophisticated implementation of Blackjack demonstrating advanced functional programming patterns using CREM (Compositional Representable Executable Machines) and Threepenny GUI with reactive programming principles.
This project showcases a clean, type-safe approach to modeling complex stateful applications through:
- 🎯 CREM State Machines: Compositional, type-safe state modeling with compile-time guarantees
- ⚛️ Threepenny Reactive Model: Functional reactive programming for the web UI
- 🔒 Type-Level Safety: Game phases encoded at the type level preventing invalid transitions
- 🎲 Property-Based Testing: Comprehensive test coverage using Hedgehog generators
The application is built around the principle of correctness by construction, where the type system prevents invalid states and transitions, making bugs impossible rather than just unlikely.
- Complete Blackjack Rules: Hit, Stand, Double Down, Split, Surrender, Insurance
- Multi-Player Support: Concurrent player management with individual game states
- Realistic Card Mechanics: Proper deck shuffling, card dealing, and scoring
- Dealer AI: Automated dealer play following standard casino rules
- Type-Safe State Transitions: Each game phase is a distinct type
- Compositional Architecture: State machines can be combined and extended
- Reactive UI: Real-time updates with functional reactive programming
- Comprehensive Testing: Property-based tests covering all game scenarios
- Multiple Interfaces: Console and web-based gameplay
The project demonstrates phantom types and GADTs to create a state machine where invalid transitions are impossible at compile time. This approach eliminates entire classes of bugs through the type system.
Each game phase is encoded as a type-level constant, creating a phantom type parameter that carries no runtime information but provides compile-time safety:
-- Game phases as type-level constants
data GamePhase
= InLobby | AwaitingBets | DealingCards
| OfferingInsurance | ResolvingInsurance
| OpeningTurn | PlayerTurn | DealerTurn
| ResolvingHands | Result | GameOver
-- The Game type is parameterized by the current phase
data Game (phase :: GamePhase) = Game
{ stdGen :: StdGen
, nextPlayerId :: Int
, state :: GameState phase -- Phase-specific state
}This means you can only call phase-specific functions when the game is in the correct phase:
-- Only valid when game is in 'PlayerTurn phase
decidePlayerTurn :: Game 'PlayerTurn -> PlayerTurnCommand -> Either GameError PlayerTurnEvent
-- Only valid when game is in 'DealerTurn phase
decideDealerPlay :: Game 'DealerTurn -> DealerTurnCommand -> Either GameError DealerTurnEventGeneralized Algebraic Data Types (GADTs) enforce that each phase can only contain appropriate state data:
data GameState (phase :: GamePhase) where
LobbyState :: PlayerMap → GameState 'InLobby
BettingState :: PlayerMap → GameState 'AwaitingBets
DealingState :: PlayerMap → Deck → GameState 'DealingCards
OfferingInsuranceState :: GameContext → GameState 'OfferingInsurance
ResolvingInsuranceState :: GameContext → GameState 'ResolvingInsurance
OpeningTurnState :: OpeningContext → GameState 'OpeningTurn
PlayerTurnState :: InsuranceContext → GameState 'PlayerTurn
DealerTurnState :: InsuranceContext → GameState 'DealerTurn
ResolvingState :: ResolutionContext → GameState 'ResolvingHands
ResultState :: PlayerMap → GameState 'Result
ExitedState :: GameState 'GameOverThis ensures that:
LobbyStatecan only exist whenphase ~ 'InLobbyDealingStaterequires both players and a deckPlayerTurnStateincludes insurance context from previous phases- Pattern matching on state automatically refines the phase type
The state machine topology is defined using Template Haskell and singletons to lift the transition graph to the type level:
$( singletons [d|
gameTopology :: Topology GamePhase
gameTopology = Topology
[ (InLobby, [AwaitingBets])
, (AwaitingBets, [DealingCards])
, (DealingCards, [OfferingInsurance, OpeningTurn])
, (OfferingInsurance, [ResolvingInsurance])
, (ResolvingInsurance, [OpeningTurn, ResolvingHands])
, (OpeningTurn, [PlayerTurn, DealerTurn, ResolvingHands])
, (PlayerTurn, [DealerTurn, ResolvingHands])
, (DealerTurn, [ResolvingHands])
, (ResolvingHands, [Result])
, (Result, [InLobby, GameOver])
]
|])This creates both:
- Value-level topology for runtime state machine execution
- Type-level topology for compile-time transition validation
CREM enables compositional state machine design where multiple machines can be combined using categorical combinators:
-- Base game logic
stateMachine :: StdGen → StateMachine Command Decision
-- Automatic resolution policy (separate concern)
autoPolicy :: StateMachine Decision [Command]
autoPolicy = statelessBase \case
BettingEvt BetPlaced{} → [DealingCmd DealInitialCards]
InsuranceEvt InsuranceResolved{} → [ResolutionCmd ResolveRound]
PlayerTurnEvt{} → [DealerTurnCmd DealerPlay, ResolutionCmd ResolveRound]
DealerTurnEvt{} → [ResolutionCmd ResolveRound]
_ → []
-- Feedback composition: output of main machine feeds policy machine
stateMachineWithAuto :: StdGen → StateMachine Command [Decision]
stateMachineWithAuto stdGen =
let stateMachine' = rmap singleton (stateMachine stdGen)
in Feedback stateMachine' autoPolicyParallel composition allows multiple read models:
-- Game statistics projection
projection :: BaseMachine ProjectionTopology Event Summary
-- Combined write and read models
whole :: StdGen → StateMachine Command (Decision, Summary)
whole stdGen = stateMachine stdGen &&& projectionThe system follows a clean Command Query Responsibility Segregation (CQRS) pattern:
- Commands: External inputs that request state changes
- Events: Immutable facts about what happened
- Decisions: Either successful events or error states
type Decision = Either GameError Event
-- Commands are requests that may fail
data PlayerTurnCommand
= Hit PlayerId | Stand PlayerId | DoubleDown PlayerId
| Split PlayerId | Surrender PlayerId
-- Events are successful outcomes
data PlayerTurnEvent
= HitCard PlayerId Card | PlayerStood PlayerId
| PlayerDoubledDown PlayerId Card
| PlayerSplitHand PlayerId Card Card Card Card
| PlayerSurrendered PlayerIdThe web interface demonstrates Functional Reactive Programming principles using Threepenny GUI:
setupGui :: Window → UI ()
setupGui window = void mdo
rng ← initStdGen
let initialGame = stateMachineWithAuto rng
-- Reactive Model-Update-View
(ui, EventStream commands) ← runComponent (view model)
(decisions, _) ← mapAccum initialGame (runGame <$> commands)
model ← accumB initialModel (flip (foldr update) <$> decisions)
getBody window # set children [ui]The reactive model ensures that:
- State flows unidirectionally from user interactions through the state machine to view updates
- Updates are atomic and always result in consistent state
- Side effects are contained within the state machine transitions
The application demonstrates CREM's compositional capabilities:
-- Base game logic
stateMachine :: StdGen → StateMachine Command Decision
-- Automatic resolution policies
autoPolicy :: StateMachine Decision [Command]
-- Composed system with automatic progression
stateMachineWithAuto :: StdGen → StateMachine Command [Decision]
stateMachineWithAuto stdGen =
let stateMachine' = rmap singleton (stateMachine stdGen)
in Feedback stateMachine' autoPolicyMultiple state machines can be composed using CREM's Feedback and Parallel combinators, allowing for:
- Policy injection: Automated decision-making layers
- Audit trails: Separate machines for logging and monitoring
- Model projections: Read-only views for different user interfaces
The project demonstrates property-based testing using the Hedgehog library, which is more modern and powerful than QuickCheck. Instead of writing specific test cases, we define generators for random test data and properties that should hold for all inputs.
This comprehensive testing approach ensures that:
- All game rules are correctly implemented across all possible inputs
- State transitions maintain consistency and respect domain constraints
- Edge cases are automatically discovered through random generation
- Refactoring is safe because properties act as a comprehensive regression suite
- Documentation exists in the form of executable specifications
├── src/
│ ├── GameTopology.hs # CREM state machine definition
│ ├── Game.hs # Core game logic and decider
│ ├── Types.hs # Domain types and data structures
│ ├── Application.hs # Machine composition and policies
│ └── Game/
│ ├── Lobby.hs # Player management phase
│ ├── Betting.hs # Bet placement logic
│ ├── Dealing.hs # Card distribution
│ ├── Insurance.hs # Insurance bet handling
│ ├── PlayerTurn.hs # Player decision processing
│ ├── DealerTurn.hs # Automated dealer play
│ ├── Resolution.hs # Hand outcome calculation
│ └── Result.hs # Game conclusion logic
├── webapp/
│ ├── Main.hs # Threepenny GUI entry point
│ └── Game/UI/
│ ├── Model.hs # Reactive model state
│ ├── Component.hs # UI component primitives
│ └── View.hs # View rendering logic
├── app/
│ └── Main.hs # Console interface
└── test/
├── Spec.hs # Test suite entry point
├── Game/Gen.hs # Hedgehog generators
└── Game/Test/ # Property-based tests
- GHC 9.4+ with GHC2021 language extensions
- Stack or Cabal for dependency management
# Clone the repository
git clone https://github.com/beefyhalo/blackjack.git
cd blackjack
# Install dependencies
stack install # or cabal install# Interactive terminal game
stack run blackjack# Launch web server (default: http://localhost:8023)
stack run blackjack-webapp
# Then open your browser to play# Execute property-based test suite
stack test
# Run with verbose output
stack test --test-arguments="--verbose"This project demonstrates several advanced functional programming concepts:
- Phantom types for compile-time state safety
- GADTs for type-safe pattern matching
- Type families for dependent types
- Event streams and behaviors
- Compositional UI components
- Unidirectional data flow
- Algebraic data types for precise domain representation
- Smart constructors and invariant preservation
- Error handling with
Eithertypes
- Property-based testing with Hedgehog
- Generator composition and combinators
- Invariant verification across state spaces
- CQRS (Command Query Responsibility Segregation)
- State machine composition patterns
- Separation of concerns through type boundaries
- CREM Library: Compositional Representable Executable Machines
- Threepenny GUI: Haskell web GUI framework
- Hedgehog: Modern property-based testing
Built with ❤️ in Haskell, demonstrating the power of functional programming, type safety, and compositional design.