F#: Checkers Kata Retrospective

Datetime:2016-08-22 22:00:16          Topic: F#  Test Engineer           Share

Intro

I have been going full throttle in my journey to learn F#. My latest focus was to implement a checkers game.

My Initial Attempt

My first attempt failed miserably. I was trying very hard to “make illegal states unrepresentable” by attempting to leverage the type system. However, I found myself duplicating code almost everywhere just to satisfy that goal. Ultimately, I scrapped my initial approach and started over.

Here were the types I used to model the domain:

(* Types *)
type BlackOption = NorthEast | NorthWest
type RedOption =   SouthEast | SouthWest

type KingOption = 
    | NorthEast 
    | NorthWest
    | SouthEast 
    | SouthWest

type Position =     { X:int; Y:int }

type BlackChecker = Position
type RedChecker =   Position
type BlackKing =    Position
type RedKing =      Position

type King = 
    | BlackKing of BlackKing
    | RedKing   of RedKing

type RedPiece = 
    | RedChecker of RedChecker 
    | RedKing    of RedKing

type BlackPiece = 
    | BlackChecker of BlackChecker 
    | BlackKing    of BlackKing

One mistake that I identified above was my overuse of types. In my case, I was trying to use types to model directions which made my code way more complicated than it needed to be. I think moving forward, I will just create types to model first-class entities of a domain.

Here’s the business logic that I flushed out using TDD

(* Private *)
let private remove item list = list |> List.filter (fun x -> x <> item)

let private setRowPosition y1 y2 y3 index =
    match index with 
    | x when x < 4 -> { X=x; Y=y1 }
    | x when x < 8 -> { X=x-4; Y=y2 }
    | _            -> { X=index-8; Y=y3 }

let private set (x, y) positions (position:Position) =
    match not (positions |> List.exists (fun pos -> pos = { X=x; Y=y })) with
    | true -> { X=x; Y=y }
    | false -> position

let private attemptJump target yDirection source =
    let updateX value = { X=target.X + value
                          Y=target.Y + yDirection }
    match source with
    | position when position.Y + yDirection = target.Y &&
                    position.X + 1 = target.X -> updateX 1

    | position when position.Y + yDirection = target.Y &&
                    position.X - 1 = target.X -> updateX -1
    | _ -> source

let private initializeBlack () =
    let setPosition index =
        index |> setRowPosition 7 6 5

    let blackCheckers = List.init 12 setPosition |> List.map (fun pos -> { X=pos.X; Y=pos.Y })
    blackCheckers

let private initializeRed () =
    let setPosition index =
        index |> setRowPosition 0 1 2

    let redCheckers =   List.init 12 setPosition |> List.map (fun pos -> { X=pos.X; Y=pos.Y })
    redCheckers

(* Exposed *)
let moveBlack direction positions (checker:BlackChecker) =
    let position = checker

    match direction with
    | BlackOption.NorthEast -> (positions, position) ||> set ((position.X + 1), (position.Y + 1 ))
    | BlackOption.NorthWest -> (positions, position) ||> set ((position.X - 1), (position.Y + 1 ))

let moveRed direction positions (checker:RedChecker) =
    let position = checker

    match direction with
    | RedOption.SouthEast -> (positions, position) ||> set ((position.X + 1), (position.Y - 1 ))
    | RedOption.SouthWest -> (positions, position) ||> set ((position.X - 1), (position.Y - 1 ))

let moveKing direction positions (king:King) =

    let position = match king with
                   | King.BlackKing k
                   | King.RedKing   k -> k

    let result = match direction with
                 | NorthEast -> (positions, position) ||> set ((position.X + 1), (position.Y + 1 ))
                 | NorthWest -> (positions, position) ||> set ((position.X - 1), (position.Y + 1 ))
                 | SouthEast -> (positions, position) ||> set ((position.X + 1), (position.Y - 1 ))
                 | SouthWest -> (positions, position) ||> set ((position.X - 1), (position.Y - 1 ))

    match king with
    | King.BlackKing _ -> King.BlackKing result
    | King.RedKing   _ -> King.RedKing   result
    
let jumpRed ((redChecker:RedChecker) , (redCheckers:RedChecker list)) (blackPiece:BlackPiece) =

    let yIncrementValue = 1
    let maxY = 7

    let set position piece =
       match position with
       | pos when pos = piece -> position , redCheckers
       | _                    -> position , redCheckers |> remove redChecker

    match blackPiece with
    | BlackChecker piece
    | BlackKing    piece -> let location = (piece |> attemptJump redChecker yIncrementValue)
                            piece |> set location

let jumpBlack ((blackChecker:BlackChecker) , (blackCheckers:BlackChecker list))  (redPiece:RedPiece) =

    let yIncrementValue = -1
    let minY = 0

    let set position piece =
       match position with
       | pos when pos = piece -> position , blackCheckers
       | _                    -> position , blackCheckers |> remove blackChecker

    match redPiece with
    | RedChecker piece
    | RedKing    piece -> let location = (piece |> attemptJump blackChecker yIncrementValue)
                          piece |> set location

My initial implementation above was devastatingly complicated. Hence, I found myself writing the same function with a different signature just to satisfy “making illegal states unrepresentable”. Unfortunately, the path that I chose was not sustainable.

Testing

I can’t deny it, I have a fetish for test automation. However, where I often operate off of muscle memory when writing automated tests for viewmodel commands within a MVVM architecture, I often hesitate when attempting to write automated tests using functional code.

My tests for that implementation was the following:

(* Tests *)
open NUnit.Framework
open FsUnit

[<Test>]
let ``move black checker northeast``() =
    ([], { X=1; Y=1 } ) ||> moveBlack BlackOption.NorthEast 
                         |> should equal { X=2; Y=2 }
[<Test>]
let ``move black checker northwest``() =
    ([], { X=1; Y=1 }) ||> moveBlack BlackOption.NorthWest 
                       |> should equal { X=0; Y=2 }
[<Test>]
let ``move red checker southeast``() =
    ([], { X=1; Y=1 }) ||> moveRed RedOption.SouthEast
                        |> should equal { X=2; Y=0 }
[<Test>]
let ``move red checker southwest``() =
    ([], { X=1; Y=1 }) ||> moveRed RedOption.SouthWest 
                        |> should equal { X=0; Y=0 }
[<Test>]
let ``black checker jumps red checker northeast``() =
    let redChecker = { X=1 ; Y=1 }
    let target = (redChecker, [redChecker])

    BlackChecker { X=0 ; Y=0 } |> jumpRed target
                               |> fst
                               |> should equal { X=2 ; Y=2 }
[<Test>]
let ``black checker jumps red checker northwest``() =
    let redChecker = { X=1 ; Y=1 }
    let target = (redChecker, [redChecker])

    BlackChecker { X=2 ; Y=0 } |> jumpRed target
                               |> fst
                               |> should equal { X=0 ; Y=2 }
[<Test>]
let ``red checker removed after being jumped``() =
    let redChecker = { X=1 ; Y=1 }
    let target = (redChecker, [redChecker])

    BlackChecker { X=2 ; Y=0 } |> jumpRed target
                               |> snd
                               |> should equal []
[<Test>]
let ``black checker removed after being jumped``() =
    let target = { X=1 ; Y=1 }, []
    RedChecker { X=2 ; Y=2 } |> jumpBlack target
                             |> snd
                             |> should equal []
[<Test>]
let ``red checker jumps black checker southeast``() =
    let blackChecker = { X=3 ; Y=2 }
    let target = blackChecker, [blackChecker]

    RedChecker { X=2 ; Y=3 } |> jumpBlack target
                             |> fst
                             |> should equal { X=4 ; Y=1 }
[<Test>]
let ``red checker jumps black checker southwest``() =
    let blackChecker = { X=1 ; Y=2 }
    let target = blackChecker, [blackChecker]
    RedChecker { X=2 ; Y=3 } |> jumpBlack target
                             |> fst
                             |> should equal { X=0 ; Y=1 }
[<Test>]
let ``cannot move black checker in a position already allocated``() =
    let redChecker = { X=2; Y=2 }
    let blackChecker = { X=1; Y=1 }
    let positions = [redChecker]

    (positions, blackChecker) ||> moveBlack BlackOption.NorthEast 
                               |> should equal { X=1; Y=1 }
[<Test>]
let ``cannot move red checker in a position already allocated``() =
    let redChecker = { X=1; Y=1 }
    let blackChecker = { X=2; Y=0 }
    let positions = [blackChecker]

    (positions, redChecker) ||> moveRed RedOption.SouthEast 
                             |> should equal { X=1; Y=1 }
[<Test>]
let ``first set count equals 12`` () =
    initializeBlack () |> List.length
                       |> should equal 12
[<Test>]
let ``second set count equals 12`` () =
    initializeRed () |> List.length
                     |> should equal 12
[<Test>]
let ``red checkers starts at (0,0)`` () =
    let checkers = initializeRed ()
    let firstChecker = checkers.Head
    let position = firstChecker
    position |> should equal { X=0; Y=0 }

[<Test>]
let ``black checkers starts at (0,7)`` () =
    let checkers = initializeBlack ()
    let firstChecker = checkers.Item 0
    let position = firstChecker
    position |> should equal { X=0; Y=7 }

[<Test>]
let ``black checker jumps to king``() =
    let redChecker = { X=1 ; Y=6 }
    let target = (redChecker, [redChecker])

    BlackChecker { X=0 ; Y=5 } |> jumpRed target
                               |> fst
                               |> should equal { X=2 ; Y=7 }
[<Test>]
let ``black checker jumps to king reduces red count``() =
    let redChecker = { X=1 ; Y=6 }
    let target = (redChecker, [redChecker])

    BlackChecker { X=0 ; Y=5 } |> jumpRed target
                               |> snd
                               |> should equal []
[<Test>]
let ``red checker jumps to king``() =
    let blackChecker = { X=1 ; Y=1 }
    let target = (blackChecker, [blackChecker])

    RedChecker { X=0 ; Y=2 } |> jumpBlack target
                             |> fst
                             |> should equal { X=2 ; Y=0 }
[<Test>]
let ``red checker jumps to king reduces black count``() =
    let blackChecker = { X=1 ; Y=1 }
    let target = (blackChecker, [blackChecker])

    RedChecker { X=0 ; Y=2 } |> jumpBlack target
                             |> snd
                             |> should equal []
[<Test>]
let ``Red king moves backwards`` ()=
    ([], King.RedKing { X=1; Y=0 }) ||> moveKing NorthWest
                                     |> should equal (King.RedKing { X=0; Y=1 })
[<Test>]
let ``Black king moves backwards`` ()=
    ([], King.BlackKing { X=1; Y=7 }) ||> moveKing SouthWest
                                       |> should equal (King.BlackKing { X=0; Y=6 })
[<Test>]
let ``red king jumps checker``() =
    let blackChecker = { X=1 ; Y=1 }
    let target = (blackChecker, [blackChecker])

    RedKing { X=0 ; Y=2 } |> jumpBlack target
                          |> fst
                          |> should equal { X=2 ; Y=0 }

In the code above, I made heavy use of the “||>” operator. This operator lets a developer pipe a tuple of values into a function only if the last two parameters of the function match each tuples type respectively.

Issues identified with my original TDD approach

There were some issues with my approach that I just couldn’t bare to look at.

Specifically, I hated the way I initialized my game:

let private initializeBlack () =
    let setPosition index =
        index |> setRowPosition 7 6 5

    let blackCheckers = List.init 12 setPosition |> List.map (fun pos -> { X=pos.X; Y=pos.Y })
    blackCheckers

let private initializeRed () =
    let setPosition index =
        index |> setRowPosition 0 1 2

    let redCheckers =   List.init 12 setPosition |> List.map (fun pos -> { X=pos.X; Y=pos.Y })
    redCheckers

I knew when I first implemented these functions that a better model could be contrived. Hence, the implementation above has no constraints or visualization. What would be better is if I could design functions that were constrained for successful usage.

Starting Over

As I mentioned earlier in this article, I was struggling to progress with my implementation of a checkers game. Hence, I was trying to leverage the compiler to make “illegal states unrepresentable”. As a result of my failure, I decided to scrap my implementation and start over.

Updated Types

With some advice from the F# community, I flushed out the following types:

(* Types *)
type Black = BlackKing | BlackSoldier
type Red =   RedKing   | RedSoldier

type Coordinate = int * int

type Piece =
    | Black of Black * Coordinate
    | Red   of Red   * Coordinate

type Space =
    | Occupied  of Piece
    | Available of Coordinate

type Status =
    | BlacksTurn | RedsTurn
    | BlackWins  | RedWins

In retrospect though, I would not have created the Piece type:

type Piece =
    | Black of Black * Coordinate
    | Red   of Red   * Coordinate

The type above caused me some unnecessary pain by complicating my functions.

If I could do it over, I would just have this:

type Piece = 
    | BlackKing    of Black * Coordinate
    | BlackSoldier of Black * Coordinate
    | RedKing      of Black * Coordinate
    | RedSoldier   of Black * Coordinate

Updated Functions

I decided to rethink my model’s operations by constraining the options available for both placements and movements.

For example, in the previous version of my implementation, I initialized the game with the following functions:

let private initializeBlack () =
    let setPosition index =
        index |> setRowPosition 7 6 5

    let blackCheckers = List.init 12 setPosition |> List.map (fun pos -> { X=pos.X; Y=pos.Y })
    blackCheckers

let private initializeRed () =
    let setPosition index =
        index |> setRowPosition 0 1 2

    let redCheckers =   List.init 12 setPosition |> List.map (fun pos -> { X=pos.X; Y=pos.Y })
    redCheckers

As I stated earlier, I was not proud of the implementation above. I felt like the two functions above were not designed with rigorous constraints and could not be visualized, which would ultimately lead to better maintainability.

As a result, I implemented the bootstrapping process for the checker board with the following functions:

let private black coordinate = Occupied (Black (BlackSoldier , coordinate))
let private red   coordinate = Occupied (Red   (RedSoldier   , coordinate))

let startGame () =
    [ red (0,0);  red (2,0);  red (4,0);  red (6,0)
      red (1,1);  red (3,1);  red (5,1);  red (7,1)
      red (0,2);  red (2,2);  red (4,2);  red (6,2)
      
      Available (1,3); Available (3,3); Available (5,3); Available (7,3)
      Available (0,4); Available (2,4); Available (4,4); Available (6,4)
      
      black (1,5);  black (3,5);  black (5,5);  black (7,5)
      black (0,6);  black (2,6);  black (4,6);  black (6,6)
      black (1,7);  black (3,7);  black (5,7);  black (7,7) ] , BlacksTurn

The startGame function noted above is more visual than my previous bootstrapping implementation. Hence, the function above screams the placement of red and black checkers. Thus, I was proud of this bootstrap implementation.

The following functions reflect the latest version of the checkers game:

(* Private *)
let private black coordinate = Occupied (Black (BlackSoldier , coordinate))
let private red   coordinate = Occupied (Red   (RedSoldier   , coordinate))

let getPositions (positions:Space list, status:Status) = positions

let private yDirection = function
    | Black _ -> -1
    | Red   _ ->  1

let private toAvailable = function
    | Available pos -> true
    | _             -> false

let available positions = positions |> List.filter toAvailable

let private availableSelection = function
    | Available pos -> Some pos
    | Occupied _   -> None

let private availablePositions positions = 
    positions |> List.filter toAvailable
              |> List.choose availableSelection

let private getCoordinate = function
    | Available xy -> Some xy
    | _            -> None

let coordinateOf = function
    | Black (checker , pos) -> pos
    | Red   (checker , pos) -> pos

let optionsForSoldier piece = 

    let (sourceX , sourceY) = coordinateOf piece

    (fun pos -> pos = ((sourceX - 1) , (sourceY + (piece |> yDirection) )) ||
                pos = ((sourceX + 1) , (sourceY + (piece |> yDirection) )))

let optionsForKing piece = 

    let (sourceX , sourceY) = coordinateOf piece

    (fun pos -> pos = ((sourceX - 1) , (sourceY + 1 )) ||
                pos = ((sourceX + 1) , (sourceY + 1 )) ||
                pos = ((sourceX - 1) , (sourceY - 1 )) ||
                pos = ((sourceX + 1) , (sourceY - 1 )))

let jumpOptions (sourceX , sourceY) space =
    match space with
    | Occupied p -> match p with
                     | Red   (ch,xy) -> xy = (sourceX + 1, sourceY - 1) ||
                                        xy = (sourceX - 1, sourceY - 1)
    
                     | Black (ch,xy) -> xy = (sourceX + 1, sourceY + 1) ||
                                        xy = (sourceX - 1, sourceY + 1)
    | _ -> false

let jumpsForSoldier piece positions =
    match piece with
    | Black (ch,pos) -> positions |> List.filter (jumpOptions (coordinateOf piece))
    | Red   (ch,pos) -> positions |> List.filter (jumpOptions (coordinateOf piece))

let private isKing piece = 
    match piece with
    | Black (checker , _) -> match checker with
                             | BlackSoldier -> false
                             | BlackKing    -> true

    | Red   (checker , _) -> match checker with
                             | RedSoldier   -> false
                             | RedKing      -> true

let filterOut a b positions =
    positions |> List.filter(fun x -> x <> a && x <> b)

(* Public *)
let startGame () =
    [ red (0,0);  red (2,0);  red (4,0);  red (6,0)
      red (1,1);  red (3,1);  red (5,1);  red (7,1)
      red (0,2);  red (2,2);  red (4,2);  red (6,2)
      
      Available (1,3); Available (3,3); Available (5,3); Available (7,3)
      Available (0,4); Available (2,4); Available (4,4); Available (6,4)
      
      black (1,5);  black (3,5);  black (5,5);  black (7,5)
      black (0,6);  black (2,6);  black (4,6);  black (6,6)
      black (1,7);  black (3,7);  black (5,7);  black (7,7) ] , BlacksTurn

let optionsFor piece positions =

    let sourceX , sourceY = coordinateOf piece

    match piece |> isKing with
    | false -> positions |> availablePositions 
                         |> List.filter (optionsForSoldier piece)

    | true ->  positions |> availablePositions 
                         |> List.filter (optionsForKing piece)

let movePiece destination positions piece =

    let destinationXY = 
        match destination with
        | Available xy -> xy
        | Occupied p  -> coordinateOf p

    let yValueMin , yValueMax = 0 , 7

    let canCrown =
        let yValue = snd destinationXY
        (yValue = yValueMin || 
         yValue = yValueMax) && 
         not (isKing piece)

    match positions |> List.find (fun space -> space = Occupied piece) with
    | Occupied (Black (ch, xy)) -> 
        let checkerType = if canCrown then BlackKing else BlackSoldier
        Available(xy) :: (Occupied(Black(checkerType, destinationXY)))
                      :: (positions |> filterOut (Occupied (Black(ch, xy))) destination)     
        
    | Occupied (Red   (ch, xy)) -> 
        let checkerType = if canCrown then RedKing else RedSoldier
        Available(xy) :: (Occupied(Red(checkerType, destinationXY)))
                      :: (positions |> filterOut (Occupied (Red(ch, xy))) destination) 
    | _ -> positions

let move piece destination (positions,status) =

    let expectedMove = match status with
                       | BlacksTurn -> RedsTurn
                       | RedsTurn   -> BlacksTurn
                       | BlackWins  -> BlackWins
                       | RedWins    -> RedWins

    let canProceed = 
        match piece with
        | Red   _ -> expectedMove = RedsTurn  
        | Black _ -> expectedMove = BlacksTurn

    if not canProceed then (positions , expectedMove)
    else let options   = optionsFor piece positions
         let canMoveTo = (fun target -> options |> List.exists (fun xy -> xy = target))
         
         match getCoordinate destination with
         | Some target -> if canMoveTo target then
                             let updatedBoard = ((positions , piece) ||> movePiece destination)
                             (updatedBoard , expectedMove)
         
                          else (positions , expectedMove)
         | None -> (positions , expectedMove)

let jump target positions source =

    let canJump = 
        positions |> jumpsForSoldier source
                  |> List.exists (fun s -> match s with
                                           | Occupied target -> true
                                           | _                -> false)

    let (|NorthEast|NorthWest|SouthEast|SouthWest|Origin|) (origin , barrier) =
        
        let (sourceX  , sourceY) =  origin
        let (barrierX , barrierY) = barrier

        if   barrierY = sourceY + 1 &&
             barrierX = sourceX - 1
        then SouthWest

        elif barrierY = sourceY + 1 &&
             barrierX = sourceX + 1 
        then SouthEast

        elif barrierY = sourceY - 1 &&
             barrierX = sourceX - 1
        then NorthWest

        elif barrierY = sourceY - 1 &&
             barrierX = sourceX + 1
        then NorthEast

        else Origin

    let jumpToPostion origin barrier =

        let (sourceX  , sourceY) =  origin
        let (barrierX , barrierY) = barrier

        match (origin , barrier) with
        | SouthWest -> (barrierX + 1, barrierY - 1)
        | SouthEast -> (barrierX + 1, barrierY + 1)
        | NorthWest -> (barrierX - 1, barrierY - 1)
        | NorthEast -> (barrierX - 1, barrierY + 1)
        | Origin    -> origin
    
    if canJump then
        let destination = Available (jumpToPostion (coordinateOf source) (coordinateOf target))
        let result = (positions, source) ||> movePiece destination
                                          |> List.filter (fun s -> s <> Occupied target)
        Available (coordinateOf target)::result
    else positions

Tests

The following tests were written:

(* Tests *)
open NUnit.Framework
open FsUnit

[<Test>]
let ``black goes first`` () =
    startGame () |> snd
                 |> should equal BlacksTurn
[<Test>]
let ``get avaialable positions`` () =
    startGame () |> fst
                 |> available
                 |> List.length 
                 |> should equal 8
[<Test>]
let ``get avaialable positions for black soldier`` () =
    // Setup
    let piece = Black ( BlackSoldier , (1,5) )

    // Test
    let available = startGame () |> fst
                                 |> available
                                 |> optionsFor piece
    // Verify
    let northWestAvailable = available |> List.exists (fun pos -> pos = (0,4))
    let northEastAvailable = available |> List.exists (fun pos -> pos = (2,4))

    (northWestAvailable && northEastAvailable) |> should equal true
[<Test>]
let ``get avaialable positions for red soldier`` () =
    // Setup
    let piece = Red ( RedSoldier , (0,2) )

    // Test
    startGame () |> fst
                 |> available
                 |> optionsFor piece
                 |> List.exists (fun pos -> pos = (1,3))
                 |> should equal true
[<Test>]
let ``get avaialable positions for red king`` () =
    // Setup
    let piece = Red ( RedKing , (2,4) )
    let positions = [Available (0,2);  Available (2,2);  Available (4,2);  Available (6,2)
      
                     Available (1,3);  Available (3,3);  Available (5,3);  Available (7,3)
                     Available (0,4);  Occupied piece;  Available (4,4);  Available (6,4)
      
                     Available (1,5);  Available (3,5);  Available (5,5);  Available (7,5)]

    // Test
    positions |> optionsFor piece
              |> List.length
              |> should equal 4
[<Test>]
let ``move red soldier`` () =

    let piece = Red ( RedSoldier , (0,2) )
    let destination = Available (1,3)

    // Test
    startGame () |> move piece destination
                 |> getPositions
                 |> List.exists (fun pos -> pos = Occupied (Red (RedSoldier , (1,3) )))
                 |> should equal true
[<Test>]
let ``moving red soldier leaves space available`` () =

    let piece = Red ( RedSoldier , (0,2) )
    let destination = Available (1,3)

    // Test
    startGame () |> move piece destination
                 |> getPositions
                 |> List.exists (fun pos -> pos = Available (0,2))
                 |> should equal true
[<Test>]
let ``moving red soldier maintains piece count`` () =

    let piece = Red ( RedSoldier , (0,2) )
    let destination = Available (1,3)

    // Test
    startGame () |> move piece destination
                 |> getPositions
                 |> List.length
                 |> should equal 32
[<Test>]
let ``get jump options for red soldier`` () =

    let redPiece =   Red   ( RedSoldier , (0,2) )
    let blackPiece = Black ( BlackSoldier , (1,3) )
    let positions = [Occupied redPiece; Occupied blackPiece]

    // Test
    positions |> jumpsForSoldier redPiece
              |> should equal [Occupied blackPiece]
[<Test>]
let ``red soldier gets new destination when it jumps black soldier`` () =

    let redPiece =   Red   ( RedSoldier , (0,2) )
    let blackPiece = Black ( BlackSoldier , (1,3) )
    let expected = Occupied (Red (RedSoldier , (2,4)))
    let positions = [Occupied redPiece; Occupied blackPiece; Available (2,4)]

    // Test
    (positions,redPiece) ||> jump blackPiece
                          |> List.exists (fun s -> s = expected)
                          |> should equal true

let ``black soldier gets new destination when it jumps red soldier`` () =

    let redPiece =   Red   ( RedSoldier   , (2,2) )
    let blackPiece = Black ( BlackSoldier , (3,3) )
    let expected =   Occupied (Black ( BlackSoldier , (1,1) ))
    let positions =  [Occupied redPiece; Occupied blackPiece; Available (1,1)]

    // Test
    (positions, blackPiece) ||> jump redPiece
                             |> List.exists (fun s -> s = expected)
                             |> should equal true
[<Test>]
let ``black soldier removed after being jumped by red soldier`` () =

    let redPiece =   Red   ( RedSoldier , (0,2) )
    let blackPiece = Black ( BlackSoldier , (1,3) )
    let expected =   Available (1,3)
    let positions =  [Occupied redPiece; Occupied blackPiece]

    // Test
    (positions,redPiece) ||> jump blackPiece
                          |> List.exists (fun s -> s = expected)
                          |> should equal true
[<Test>]
let ``red soldier removed after being jumped by black soldier`` () =

    let redPiece =   Red   ( RedSoldier   , (0,2) )
    let blackPiece = Black ( BlackSoldier , (1,3) )
    let expected =   Available (0,2)
    let positions =  [Occupied redPiece; Occupied blackPiece; expected]

    // Test
    (positions, blackPiece) ||> jump redPiece
                             |> List.exists (fun s -> s = expected)
                             |> should equal true
[<Test>]
let ``movng black soldier to endzone makes king`` () =
    
    let piece = Black ( BlackSoldier , (1,1) )
    let destinationCoordinate = (0,0)
    let destination = Available destinationCoordinate
    let positions = [destination; Occupied piece]
    let expected = Occupied (Black (BlackKing , destinationCoordinate))

    // Test
    (positions, RedsTurn) |> move piece destination
                          |> getPositions
                          |> List.exists (fun pos -> pos = expected)
                          |> should equal true          
[<Test>]
let ``movng red soldier to endzone makes king`` () =

    let piece = Red ( RedSoldier , (6,6) )
    let destinationCoordinate = (7,7)
    let destination = Available destinationCoordinate
    let positions = [destination; Occupied piece]
    let expected = Occupied (Red (RedKing , destinationCoordinate))

    // Test
    (positions, BlacksTurn) |> move piece destination
                            |> getPositions
                            |> List.exists (fun pos -> pos = expected)
                            |> should equal true
[<Test>]
let ``can't move red twice in a row`` () =
    let piece = Red(RedSoldier , (0,2))
    let destination = Available (1,3)

    // Test
    let set1 = startGame() |> move piece destination
    let set2 = set1 |> move piece destination
                    
    set2 |> getPositions
         |> should equal (set1 |> getPositions)

Conclusion

In conclusion, I have been going full throttle in my journey to learn F#. My latest focus was to implement a checkers game. I showed two versions of how the game was implemented. Note that these implementations do not account for multiple jumps for a checker. I have realized that “making illegal states unrepresentable” is a significant challenge in regards to maintainability.





About List