Skip to content

Instantly share code, notes, and snippets.

@swlaschin
Last active March 17, 2022 16:06
Show Gist options
  • Save swlaschin/2ad8627d0400b2ab70e9f3da08902c9d to your computer and use it in GitHub Desktop.
Save swlaschin/2ad8627d0400b2ab70e9f3da08902c9d to your computer and use it in GitHub Desktop.
Example of Domain Driven Design for the game of checkers. There are two files: a scratch file with a series of designs, and a final version.
(*
Example of domain-driven design for Checkers
Rules from here: https://www.itsyourturn.com/t_helptopic2030.html
A SERIES OF SCRATCH DESIGNS
*)
// As we go through the rules, and learn things, we create a series of designs
module Version1 =
// ==================================
// Section 1 from rules: "Setup"
// From Rules: "Checkers is played on a standard 64 square board."
type Board = Board of unit // don't know much more than this yet
type Row = Row of int
and Col = Col of int
// ISSUE: how to constrain the ints to be valid 1..8?
// Solution: Have a private constructor... we'll deal with that later.
// ISSUE: Only the 32 dark colored squares are used in play.
// Solution: we'll need some constraint for that as well
// From Rules: "Each player begins the game with 12 pieces, or checkers,
// placed in the three rows closest to him or her. "
type Player = {
Pieces : Piece list // we'll just assume there are 12 pieces!
}
// each piece has a position and some properties
and Piece = Piece of PieceProperties
and Position = Row * Col
and PieceProperties = {
Position : Position
Color : PieceColor
// don't know any more properties than position and color yet
}
and PieceColor = Red | Black
//================================
// As we read more of the rules, we get a better idea of the game#
module Version2 =
// ==================================
// Section 2 from rules: "Movement"
// From rules:
// "Basic movement is to move a checker one space diagonally forward."
// "You can not move a checker backwards until it becomes a King, as described below."
// NOTE: That tells us that kings have different movement from normal checkers.
// So now we have a new property for a checker: king or soldier
type PieceType = Soldier | King
type PieceColor = Red | Black
type PieceProperties = {
Position : Position
Type : PieceType
Color : PieceColor
}
// and also, two kinds of movement
type SoldierDirection =
| ForwardLeft
| ForwardRight
type KingDirection =
| ForwardLeft
| ForwardRight
| BackwardLeft
| BackwardRight
// From rules:
// "If a jump is available, you must take the jump, as described in the next question and answer."
// NOTE: This implies the concept of "available moves" for a piece
// It also tells us that there are two kinds of movement, normal and jump
type AvailableMove =
| Normal of Move
| Jump of Move // this might not exist, but if it does, we must use it.
// Where do we get the available moves from? By analyzing the board!
// So lets create a function for this.
// We also need to provide a player (which the same as the color)
type GetAvailableMoves =
Board * Player // input: the board and the player to play
-> AvailableMove list // ouput: the list of available moves
// what's nice about this is that the positions can never be bad because WE create
// the available moves, not the player.
// What is a move anyway? A starting position and a direction.
and Move = {
Position : Position // reuse from previous version
Direction : unit // ISSUE: What should we put here? SoldierDirection or KingDirection?
}
// One fix for the SoldierDirection or KingDirection issue is
// to have TWO kinds of Move, one for each type of piece
type SoldierMove = {
Position : Position
Direction : SoldierDirection
}
type KingMove = {
Position : Position
Direction : KingDirection
}
// Hmm. thats a bit smelly as the types are so similar -- we'll look at tidying this up later
//================================
// Keep reading the rules!
module Version3 =
// ==================================
// Section 3 from rules: "Jumping"
// "Your opponent’s checker is captured and removed from the board. "
// NOTES: This implies that the number of pieces can change.
// So better not store the pieces with the player. Instead, let the Board store the pieces
// and update the Board when a piece is taken
type PlayMove =
Board * Move // input: board and move
-> Board // output: new board, possibly with some pieces missing or crowned
// we can define the Board now as a container for pieces
and Board = {
Pieces: Piece list
}
// "After making one jump, your checker might have another jump available
// from its new position. Your checker must take that jump too. It must
// continue to jump until there are no more jumps available. "
// NOTES: This means that the result of playing a move is TWO choices
// * your move is finished
// * or, you have to keep playing with the same piece AND you have a fixed
// set of (jump) moves you MUST choose from.
type MoveResult =
| NextPlayersTurn of Player
| KeepPlayingJumps of Move list
// and PlayMove is updated too...
type PlayMove =
Board * Move // input: board and move
-> Board * MoveResult // output: board and move result
// "If, at the start of a turn, more than one of your checkers has a
// jump available, then you may decide which one you will move.
// But once you have chosen one, it must take all the jumps that it can. "
// NOTE: This means that an available jump move has a *list* of jumps, not a single jump.
// When a move is played, every jump in that list must be used.
type AvailableMove =
| Normal of Move
| Jump of Move list // changed to list!
// but after the first jump there may be a choice of jumps, so "Move list" doesn't
// capture that. Let define a new type called "JumpMove"
type JumpMove = {
FirstJump : Move
FollowupJumps : JumpMove list // could be empty
}
type AvailableMove =
| Normal of Move
| Jump of JumpMove // updated with new type
// this means that we can change the KeepPlaying MoveResult to only be JumpMoves now
type MoveResult =
| NextPlayersTurn of Player
| KeepPlayingJumps of JumpMove list
//================================
// Setting up the board and the initial moves
// Question: Where do we get the initial Board state from?
// Let's create a function for that!
type InitGame = unit -> Board
//================================
// And we keep on reading the rules!
module Version4 =
// ==================================
// Section 5 from rules: "Crowning"
// "When one of your checkers reaches the opposite side of the board,
// it is crowned and becomes a King."
// NOTE: so we need a function to convert a piece to a king:
type Crown = Piece -> Piece
// Hmm, the function above doesn't convey very much. Ideally we want something
// that makes it clear what's happening
type Soldier = ... // this is a new type we have to create
type King = ...
type Crown = Soldier -> King // that looks much more descriptive!
//================================
// We're done with the rules -- let's fix up any smells
module FixUpSmells =
// these are too similar -- let's simplify
type SoldierMove = {
Position : Position
Direction : SoldierDirection
}
type KingMove = {
Position : Position
Direction : KingDirection
}
// .. so try merging the position into the enum type
type SoldierMovement =
| ForwardLeft of Position // starting position
| ForwardRight of Position
type KingMovement =
| ForwardLeft of Position
| ForwardRight of Position
| BackwardLeft of Position
| BackwardRight of Position
// but now we have to have two kinds of AvailableMove and JumpMove :(
type SoldierAvailableMove =
| Normal of SoldierMovement
| Jump of SoldierJumpMove
type KingAvailableMove =
| Normal of KingMovement
| Jump of KingJumpMove
// yuck!!
// SOLUTION: Parameterize on direction, where direction is SoldierDirection or KingDirection
type AvailableMove<'direction> =
| Normal of Position * 'direction
| Jump of JumpMove<'direction>
and JumpMove<'direction> = {
FirstJump : Position * 'direction
FollowupJumps : JumpMove list // could be empty
}
// for final version -- see other file "Checkers-final.fsx"
(*
Example of domain-driven design for Checkers
Rules from here: https://www.itsyourturn.com/t_helptopic2030.html
FINAL DESIGN
*)
module FinalVersion =
// -----------------------------
// pieces and board
// -----------------------------
type Player = Red | Black
type Row = Row of int
type Col = Col of int
type Position = Row * Col
type PieceProperties = {
Owner : Player
Position : Position
}
// Two distinct types of piece so that Crown function looks nice!
type Soldier = Soldier of PieceProperties
type King = King of PieceProperties
type Crown = Soldier -> King
type Board = {
// NOTE: We could create a DU of Soldier | King, but why bother?
// Just track them separately
Soldiers : Soldier list
Kings: King list
}
// -----------------------------
// Movement
// -----------------------------
type SoldierDirection =
| ForwardLeft
| ForwardRight
type KingDirection =
| ForwardLeft
| ForwardRight
| BackwardLeft
| BackwardRight
type AvailableMove<'direction> =
| Normal of Position * 'direction
| Jump of JumpMove<'direction>
and JumpMove<'direction> = {
FirstJump : Position * 'direction
FollowupJumps : JumpMove<'direction> list
}
// -----------------------------
// Playing the game
// -----------------------------
type InitGame =
// input: nothing
unit
// output: the initial board state
-> Board
type AvailableMoves = {
SoldierMoves: AvailableMove<SoldierDirection>
KingMoves: AvailableMove<KingDirection>
}
type GetAvailableMoves =
// input: the board state and the player to play
Board * Player
// output: the list of available moves for that player
-> AvailableMoves
/// Choice of (a) keep playing jumps with same piece or (b) next player's turn.
type MoveResult<'direction> =
| NextPlayersTurn of Player
| KeepPlayingJumps of JumpMove<'direction> list
type PlayMove<'direction> =
// input: board state and move
Board * AvailableMove<'direction>
// output: new board state, possibly with some pieces missing or crowned
// and choice of keep playing jumps / next player's turn.
-> Board * MoveResult<'direction>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment