5
\$\begingroup\$

FEN strings are compact representations of chess board positions which allows you to derive the necessary information to start playing a chess game from that position. This includes things like the pieces on the board, castle statuses, etc. More information here: https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation

I have written a simple function in go to decode a FEN string into separate variables with suitable types for each of the desired pieces of information that FEN strings represent, and I would like to see if I am parsing the FEN string in a reasonable manner.

import (
    "strings"
)

type pair struct {
    x, y int8
}

func decodeFen(fen string) ([8][8]int8, int8, [4]bool, pair, int8, int) {
    split := strings.Split(fen, " ")
    var board [8][8]int8
    board_split := strings.Split(split[0], "/")
    for r, row := range board_split {
        var col_ind int8
        for _, char := range row {
            if char >= 49 && char <= 57 {
                for i := int8(0); i < int8(char) - 48; i++ {
                    board[r][col_ind] = '-'
                    col_ind++
                }
            } else {
                board[r][col_ind] = int8(char)
                col_ind++
            }
        }
    }
    var color int8
    if split[1] == "w" {
        color = 1
    } else {color = 2}
    var castle_status [4]bool
    var status_map = map[rune]int8 {
        'K' : 0, 'Q' : 1, 'k' : 2, 'q' : 3}
    if split[2] != "-" {
        for _, char := range split[2] {
            castle_status[status_map[char]] = true
        }
    }
    var ep_pos pair = pair{-1, -1}
    if split[3] != "-" {
        x_label := split[3][0]
        y_label := split[3][1]
        y_label -= 48
        ep_pos = pair{int8(x_label - 97), int8(y_label - ((y_label - 1) - (8 - y_label)) - 1)}
    }
    var halfmove_clock int8 = int8(split[4][0]) - 48
    var fullmove_number int = int(split[5][0]) - 48
    return board, color, castle_status, ep_pos, halfmove_clock, fullmove_number
}

This is really just a parsing problem, and to split the strings I decided to use the Split() function from the strings go library, rather than writing my own. Any improvements and feedback for this code would be great, and I apologize if I am not correctly asking this question as this is my first post in this community.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Code should be correct, maintainable, robust, reasonably efficient, and, most importantly, readable.


To be certain that the code is correct, carefully read the specification and related documents.

Forsyth–Edwards Notation

PGN-SPEC

Portable Game Notation

Algebraic notation (chess)

Chess

For a code review, compare the specification and the code implementing it side-by-side.


In chess, there are pieces placed on squares which are organized into ranks and files to form a board. There doesn't appear to be any references to these basic concepts, apart from board, in decodeFen. Therefore, comparison of code to specification is difficult.

"PGN is "Portable Game Notation", a standard designed for the representation of chess game data using ASCII text files. PGN data is represented using a subset of the eight bit ISO 8859/1 (Latin 1) character set." Therefore,

var board [8][8]int8

is incorrect. Characters are not type int8 (8-bit signed integers). Characters are type byte (8 bits).

var board [8][8]byte

The code

for _, char := range row { ... }

expects a UTF-8 encoded string. UTF-8 encoding is not the same as ISO 8859/1 encoding.

Go is a safe, statically typed language. The code

board[r][col_ind] = int8(char)

is a failed attempt to correct errors when ISO 8859/1 encoding is decoded as UTF-8 encoding.

The decodeFen code is a "stream-of-conciousness" code that is hard to read and prove correct. The code is missing fundamental concepts like functions to encapsulate complexity and implementation details. For example, organize decodeFen as a series of calls to parse functions for each FEN field.


As an example, consider the FEN piece placement field. Here's a first draft:

pieces.go:

package main

import (
    "errors"
    "fmt"
    "strings"
)

type Board [8][8]byte

const (
    whitePieceLetters = "PNBRQK"
    blackPieceLetters = "pnbrqk"
    pieceLetters      = whitePieceLetters + blackPieceLetters
)

func isPiece(p byte) bool {
    for i := 0; i < len(pieceLetters); i++ {
        if p == pieceLetters[i] {
            return true
        }
    }
    return false
}

var (
    ErrRankOutOfRange = errors.New("rank out of range")
    ErrFileOutOfRange = errors.New("file out of range")
    ErrPieceInvalid   = errors.New("piece invalid")
)

func parseFENPieces(board *Board, pieces string) error {
    ranks := strings.Split(pieces, "/")
    if len(ranks) != len(board) {
        return ErrRankOutOfRange
    }
    for r, rank := range ranks {
        f := 0
        for i := 0; i < len(rank); i++ {
            piece := rank[i]
            if piece >= '0' && piece <= '9' {
                for j := byte(0); j < piece-'0'; j++ {
                    if f >= len(board[0]) {
                        return ErrFileOutOfRange
                    }
                    board[r][f] = '-'
                    f++
                }
            } else {
                if f >= len(board[0]) {
                    return ErrFileOutOfRange
                }
                if !isPiece(piece) {
                    return ErrPieceInvalid
                }
                board[r][f] = piece
                f++
            }
        }
        if f != len(board[0]) {
            return ErrFileOutOfRange
        }
    }
    return nil
}

func main() {
    fens := []string{
        // https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
        "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
        "rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2",
        "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2",
    }
    var board Board
    for _, fen := range fens {
        fields := strings.Split(fen, " ")
        if len(fields) > 0 {
            pieces := strings.Split(fen, " ")[0]
            fmt.Println(pieces)
            err := parseFENPieces(&board, pieces)
            fmt.Println(err)
            for i := range board {
                fmt.Printf("%c\n", board[i])
            }
        }
    }
}

Playground: https://play.golang.org/p/-pPnCs2GMao

Output:

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
<nil>
[r n b q k b n r]
[p p p p p p p p]
[- - - - - - - -]
[- - - - - - - -]
[- - - - - - - -]
[- - - - - - - -]
[P P P P P P P P]
[R N B Q K B N R]

Use chess terminology to match the specification.

The chess board data structure is ubiquitous. Give it a type:

type Board [8][8]byte

In Go, arguments are passed by value. For an array, the value is the entire array. Therefore, for efficiency, we use a pointer (8 bytes or 4 bytes) rather than an array (64 bytes).

Input must be valid. Make no assumptions about external input. Report errors.

Don't use magic values: 49 is '0', 57 is '9'.

And so forth.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.