I am implementing the logic for a Checkers game in F#. I am writing my code in a library so it can be called from any UI provider, and am trying to do it in good FP style. I currently have the following code; each file is provided in the order of compilation:
Checkers.fs This contains some unions and records that will be used throughout the entire library.
type Player = Black | White
type PieceType = Checker | King
type Coord = {
Row :int
Column :int
} with
static member (+) (coord1 :Coord, coord2 :Coord) =
{Row = coord1.Row + coord2.Row; Column = coord1.Column + coord2.Column}
Piece.fs This is an immutable class representing a Piece. It contains each option as a static member to ease building boards manually.
type Piece(player:Player, pieceType:PieceType) =
member this.Player = player
member this.PieceType = pieceType
member this.Promote() =
new Piece(player, PieceType.King)
override this.Equals (obj) =
let piece = obj :?> Piece
piece.Player = this.Player &&
piece.PieceType = this.PieceType
override this.GetHashCode() =
this.Player.GetHashCode() ^^^ this.PieceType.GetHashCode()
static member WhiteChecker() =
Some <| new Piece(Player.White, PieceType.Checker)
static member BlackChecker() =
Some <| new Piece(Player.Black, PieceType.Checker)
static member WhiteKing() =
Some <| new Piece(Player.White, PieceType.King)
static member BlackKing() =
Some <| new Piece(Player.Black, PieceType.King)
Board.fs This type represents a board, and the only stateful member of this class is immutable, like Piece
. The second optional constructor is for ease of creating a board from a hard-coded list in C#; it may not remain in future versions.
type Board(board) =
new () = Board(Board.DefaultBoard)
new(board :IEnumerable<IEnumerable<Option<Piece>>>) =
let boardArray = List.ofSeq(board.Select(fun r -> List.ofSeq(r)))
Board(boardArray)
member this.Board :Option<Piece> list list = board
member this.Item
with get(coord :Coord) =
this.Board.[coord.Row].[coord.Column]
member this.Item
with get(row :int) =
this.Board.[row]
static member DefaultBoard :Option<Piece> list list =
[
[None; Piece.BlackChecker(); None; Piece.BlackChecker(); None; Piece.BlackChecker(); None; Piece.BlackChecker()];
[Piece.BlackChecker(); None; Piece.BlackChecker(); None; Piece.BlackChecker(); None; Piece.BlackChecker(); None];
[None; Piece.BlackChecker(); None; Piece.BlackChecker(); None; Piece.BlackChecker(); None; Piece.BlackChecker()];
[None; None; None; None; None; None; None; None];
[None; None; None; None; None; None; None; None];
[Piece.WhiteChecker(); None; Piece.WhiteChecker(); None; Piece.WhiteChecker(); None; Piece.WhiteChecker(); None];
[None; Piece.WhiteChecker(); None; Piece.WhiteChecker(); None; Piece.WhiteChecker(); None; Piece.WhiteChecker()];
[Piece.WhiteChecker(); None; Piece.WhiteChecker(); None; Piece.WhiteChecker(); None; Piece.WhiteChecker(); None];
];
FSharpExtensions.fs This module contains many functions for internal use only. Most of these functions are designed to be based on a certain type to make calling them easier; perhaps they should be included in the type rather than being extensions like this?
module FSharpExtensions =
let internal getJumpedCoord(startCoord, endCoord) =
{Row = startCoord.Row - Math.Sign(startCoord.Row - endCoord.Row); Column = startCoord.Column - Math.Sign(startCoord.Column - endCoord.Column)}
let internal checkMoveDirection(piece :Piece, startCoord :Coord, endCoord :Coord) =
match piece.PieceType with
| PieceType.Checker ->
match piece.Player with
| Player.Black -> startCoord.Row < endCoord.Row
| Player.White -> startCoord.Row > endCoord.Row
| PieceType.King -> true
let internal moveIsDiagonal(startCoord :Coord, endCoord :Coord) =
startCoord <> endCoord &&
System.Math.Abs(startCoord.Row - endCoord.Row) = System.Math.Abs(startCoord.Column - endCoord.Column)
let internal kingRowIndex(player) =
match player with
| Player.Black -> 7
| Player.White -> 0
type Board with
member internal board.CoordExists(coord :Coord) =
coord.Row > 0 && coord.Row < board.Board.Length &&
coord.Column > 0 && coord.Column < board.Board.[0].Length
member internal board.IsValidCheckerHop(startCoord :Coord, endCoord :Coord) =
let piece = board.[startCoord].Value
checkMoveDirection(piece, startCoord, endCoord) &&
board.[endCoord].IsNone
member internal board.IsValidKingHop(startCoord :Coord, endCoord :Coord) =
board.[endCoord].IsNone
member internal board.IsValidCheckerJump(startCoord :Coord, endCoord :Coord) =
let piece = board.[startCoord].Value
let jumpedCoord = getJumpedCoord(startCoord, endCoord)
let jumpedPiece = board.[jumpedCoord]
checkMoveDirection(piece, startCoord, endCoord) &&
board.[endCoord].IsNone &&
jumpedPiece.IsSome &&
jumpedPiece.Value.Player <> piece.Player
member internal board.IsValidKingJump(startCoord :Coord, endCoord :Coord) =
let piece = board.[startCoord].Value
let jumpedCoord = getJumpedCoord(startCoord, endCoord)
let jumpedPiece = board.[jumpedCoord]
board.[endCoord].IsNone &&
jumpedPiece.IsSome &&
jumpedPiece.Value.Player <> piece.Player
member internal board.IsValidHop(startCoord :Coord, endCoord :Coord) =
match board.[startCoord].Value.PieceType with
| PieceType.Checker -> board.IsValidCheckerHop(startCoord, endCoord)
| PieceType.King -> board.IsValidKingHop(startCoord, endCoord)
member internal board.IsValidJump(startCoord :Coord, endCoord :Coord) =
match board.[startCoord].Value.PieceType with
| PieceType.Checker -> board.IsValidCheckerJump(startCoord, endCoord)
| PieceType.King -> board.IsValidKingJump(startCoord, endCoord)
member internal board.SetPieceAt(coord :Coord, piece :Option<Piece>) =
let boardItems = List.init 8 (fun row ->
match row with
| i when i = coord.Row ->
List.init 8 (fun col ->
match col with
| j when j = coord.Column -> piece
| _ -> board.[row].[col]
)
| _ -> board.[row]
)
new Board(boardItems)
member internal board.Jump(startCoord :Coord, endCoord :Coord) =
let kingRowIndex = kingRowIndex(board.[startCoord].Value.Player)
let piece =
match endCoord.Row with
| row when row = kingRowIndex -> Some <| board.[startCoord].Value.Promote()
| _ -> board.[startCoord]
let jumpedCoord = getJumpedCoord(startCoord, endCoord)
board.SetPieceAt(startCoord, None).SetPieceAt(endCoord, piece).SetPieceAt(jumpedCoord, None)
member internal board.Hop(startCoord :Coord, endCoord :Coord) =
let kingRowIndex = kingRowIndex(board.[startCoord].Value.Player)
let piece =
match endCoord.Row with
| row when row = kingRowIndex -> Some <| board.[startCoord].Value.Promote()
| _ -> board.[startCoord]
board.SetPieceAt(startCoord, None).SetPieceAt(endCoord, piece)
ExtensionMethods.fs This is really the public API of my library. I designed the methods like this so they could be called as boardInstance.Member(...)
in both C# and F#.
[<Extension>]
type ExtensionMethods() =
[<Extension>]
static member IsValidMove(board :Board, startCoord :Coord, endCoord :Coord) =
board.CoordExists(startCoord) &&
board.CoordExists(endCoord) &&
board.[startCoord].IsSome &&
moveIsDiagonal(startCoord, endCoord) &&
match Math.Abs(startCoord.Row - endCoord.Row) with
| 1 -> board.IsValidHop(startCoord, endCoord)
| 2 -> board.IsValidJump(startCoord, endCoord)
| _ -> false
[<Extension>]
static member Move(board :Board, startCoord :Coord, endCoord :Coord) :Option<Board> =
match ExtensionMethods.IsValidMove(board, startCoord, endCoord) with
| false -> None
| true ->
match Math.Abs(startCoord.Row - endCoord.Row) with
| 1 -> Some <| board.Hop (startCoord, endCoord)
| 2 -> Some <| board.Jump (startCoord, endCoord)
| _ -> None
[<Extension>]
static member Move(board :Board, coordinates :IEnumerable<Coord>) =
let coords = List.ofSeq(coordinates)
match coords.Length with
| b when b >= 3 ->
let newBoard = ExtensionMethods.Move (board, coords.Head, coords.[1])
ExtensionMethods.Move (newBoard, coords.Tail)
| _ -> ExtensionMethods.Move (board, coords.Head, coords.[1])
static member internal Move(board :Option<Board>, coordinates :IEnumerable<Coord>) =
match board.IsSome with
| false -> None
| true -> ExtensionMethods.Move(board.Value, coordinates)
Any and all comments welcome. If you see something that I can improve, or that I am not doing something the idiomatic F# way, please let me know.