Last active
March 17, 2022 16:06
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
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