HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Grand Chess domain model and helper functions

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
helpergrandchessmodelandfunctionsdomain

Problem

So I am trying to write, essentially from a blank slate, a program that plays Grand Chess. In short, it is a chess variant that is played with two extra pieces, on a 10x10 board, no castling, and optional promotion on the 8th and 9th ranks, mandatory on the last. Finally, you may not promote to a piece unless it has been captured (no 2 queens, for example.) Rest of the rules are the same as chess.

These functions will, hopefully, be the basis for both a UI to play the game and a simple engine to play against.

I have somewhat implemented the kind-of-complicated promotion rules, but I am not too sure about the implementation. It should only generate legal moves. The only legality check is checking whether the move ends with the King in check. I am maintaining that you can only promote to a captured piece by maintaining a piece list in the Position Type. this will probably generate tons of garbage, so any performance improvements are welcome.

Edit: The code below is a restructuring / renaming / refactoring of the original code posted here. I did so since there were no answers nor comments and I kept working on the code anyway. The algorithms used and the main logic are the same, however.

Without further ado:

Types.fs

```
namespace Stamma

type Color =
| White
| Black
member x.Opp =
match x with
| White -> Black
| Black -> White

(*
This type is replaced by simple int * int tuples throughout

type Vector =
{ Forward : int // changes per player
East : int } // to the Right of the white player.
*)

type Piece =
| King
| Queen
| Marshal
| Cardinal
| Bishop
| Knight
| Rook
| Pawn

type MoveType =
| Move
| Capture
| Promotion of Piece
| CapAndPromotion of Piece

type Coordinates =
{ Rank : int
File : int }

[]
type Field =
| Empty
| Piece of Piece * Color

type Position =
{ Board : Map
EnPassant : Coordinates
Turn : Color
KingW

Solution

In toFen, you don't need sb to be mutable. You're not reassigning it, but are instead calling methods on it, of which mutable does not affect.

The same thing applies to baseArray in ofFenRank. This doesn't need to be mutable, because you're not directly assigning to the array, but are assigning to elements in the array.

Generally, in F# it's preferred to match instead of if, especially with constants:

let private applyVector turn start (x, y) = 
    { Rank = 
          start.Rank + ((match turn with
                         | White -> 1
                         | _ -> -1)
                        * x)
      File = start.File + y }


Just as well:

let private wayfindCore start pos piece reach = 
    let rec loop start acc reach vector = 
        match reach with
        | 0 -> acc
        | _ -> let dist = applyVector pos.Turn start vector
               match Map.tryFind dist pos.Board with
               | Some(Piece(p, c)) when c <> pos.Turn -> (dist, Capture) :: acc
               | Some(Empty) -> loop dist ((dist, Move) :: acc) (reach - 1) vector
               | _ -> acc
    Piece.vectors piece
    |> List.collect (loop start [] reach)
    |> List.map (fun (d, m) -> start, d, m)


One more:

{ pos with Board = 
               match (pos.EnPassant, movingPiece) with
               | (ep, Pawn) when ep = dist ->
                   newBoard |> Map.add { Rank = start.Rank
                                         File = dist.File } Empty
               | _ -> newBoard
           KingWhite = 
               match (movingPiece, pos.Turn) with
               | (King, White) -> dist
               | _ -> pos.KingWhite
           KingBlack = 
               match (movingPiece, pos.Turn) with
               | (King, Black) -> dist
               | _ -> pos.KingBlack
           CappedWhite = update pos.CappedWhite Black |> remove (fun x -> promo && x = endPiece)
           CappedBlack = update pos.CappedBlack White |> remove (fun x -> promo && x = endPiece)
           Turn = pos.Turn.Opp
           EnPassant = 
               match (movingPiece, abs (start.Rank - dist.Rank)) with
               | (Pawn, 2) ->
                    { Rank = (start.Rank + dist.Rank) / 2
                      File = start.File }
               | _ ->
                    { Rank = 0
                      File = 0 } }


Other than that, there's not much I can say. I'm not an expert in F# by any means. You used tail-call recursion everywhere you could, which is good. You use Option and unions when appropriate, and you used functional paradigms everywhere. The only one I don't like is the mutable esc.

Rewriting toFen as follows should eliminate the need for this mutable variable, and follow functional paradigms as much as possible. (Reduced the number of if statements substantially.)

Note: I haven't yet tested this, you provided no test data, but it should provide the same result.

let toFen board = 
    let sb = System.Text.StringBuilder()
    let rec rLoop r acc =
        let rec fLoop f acc =
            let newAcc = 
                match Map.find { Rank = r
                                 File = f } board with
                | Empty -> acc + 1
                | Piece(p, c) -> 
                    match acc with
                    | v when v > 0 ->
                        sb.Append acc |> ignore
                        0
                    | _ ->
                        sb.Append(Piece.toChar c p) |> ignore
                        acc
            match f with
            | v when v  fLoop (f + 1) newAcc
            | _ -> newAcc

        let newAcc = fLoop 1 acc
        let finalAcc =
            if newAcc > 0 then 
                sb.Append newAcc |> ignore
                0
            elif r > 1 then
                sb.Append '/' |> ignore
                newAcc
            else
                newAcc

        if r > 1 then
            rLoop (r - 1) newAcc
    rLoop 10 0
    sb.ToString()


Basically, we use recursive loops like the functional paradigms expect.

All-in-all this was a very well-written programme, I actually learned a lot from it and it forced me to think functionally for a moment, and helped me remove that esc mutable variable with a little effort. Very good job! :)

Code Snippets

let private applyVector turn start (x, y) = 
    { Rank = 
          start.Rank + ((match turn with
                         | White -> 1
                         | _ -> -1)
                        * x)
      File = start.File + y }
let private wayfindCore start pos piece reach = 
    let rec loop start acc reach vector = 
        match reach with
        | 0 -> acc
        | _ -> let dist = applyVector pos.Turn start vector
               match Map.tryFind dist pos.Board with
               | Some(Piece(p, c)) when c <> pos.Turn -> (dist, Capture) :: acc
               | Some(Empty) -> loop dist ((dist, Move) :: acc) (reach - 1) vector
               | _ -> acc
    Piece.vectors piece
    |> List.collect (loop start [] reach)
    |> List.map (fun (d, m) -> start, d, m)
{ pos with Board = 
               match (pos.EnPassant, movingPiece) with
               | (ep, Pawn) when ep = dist ->
                   newBoard |> Map.add { Rank = start.Rank
                                         File = dist.File } Empty
               | _ -> newBoard
           KingWhite = 
               match (movingPiece, pos.Turn) with
               | (King, White) -> dist
               | _ -> pos.KingWhite
           KingBlack = 
               match (movingPiece, pos.Turn) with
               | (King, Black) -> dist
               | _ -> pos.KingBlack
           CappedWhite = update pos.CappedWhite Black |> remove (fun x -> promo && x = endPiece)
           CappedBlack = update pos.CappedBlack White |> remove (fun x -> promo && x = endPiece)
           Turn = pos.Turn.Opp
           EnPassant = 
               match (movingPiece, abs (start.Rank - dist.Rank)) with
               | (Pawn, 2) ->
                    { Rank = (start.Rank + dist.Rank) / 2
                      File = start.File }
               | _ ->
                    { Rank = 0
                      File = 0 } }
let toFen board = 
    let sb = System.Text.StringBuilder()
    let rec rLoop r acc =
        let rec fLoop f acc =
            let newAcc = 
                match Map.find { Rank = r
                                 File = f } board with
                | Empty -> acc + 1
                | Piece(p, c) -> 
                    match acc with
                    | v when v > 0 ->
                        sb.Append acc |> ignore
                        0
                    | _ ->
                        sb.Append(Piece.toChar c p) |> ignore
                        acc
            match f with
            | v when v < 10 -> fLoop (f + 1) newAcc
            | _ -> newAcc

        let newAcc = fLoop 1 acc
        let finalAcc =
            if newAcc > 0 then 
                sb.Append newAcc |> ignore
                0
            elif r > 1 then
                sb.Append '/' |> ignore
                newAcc
            else
                newAcc

        if r > 1 then
            rLoop (r - 1) newAcc
    rLoop 10 0
    sb.ToString()

Context

StackExchange Code Review Q#141425, answer score: 2

Revisions (0)

No revisions yet.