Initial commit - All files

This commit is contained in:
MSWS
2024-12-12 03:24:39 -08:00
commit bb3658c01b
9 changed files with 433 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
chess

65
board/board.go Normal file
View File

@@ -0,0 +1,65 @@
package board
import (
"errors"
"strings"
)
// Single byte to store 2D coordinates,
// as the max value we need to store in a given
// dimension is 8.
// We store the X coordinate in the upper 4 bits,
// and the Y coordinate in the lower 4 bits
type Coordinate byte
func (coord Coordinate) GetCoords() (byte, byte) {
return byte(coord) >> 4, byte(coord) & 0b1111
}
func CreateCoordInt(x int, y int) Coordinate {
return CreateCoordByte(byte(x), byte(y))
}
func CreateCoordByte(x byte, y byte) Coordinate {
return Coordinate(x<<4 + y)
}
type Castlability struct {
CanQueenSide bool
CanKingSide bool
}
// A board that represents a given game.
// Represents all data that is stored in a FEN string.
// i.e. given a Board, you can find the FEN, and vice-versa
type Board struct {
// The game's pieces, in [row][col]
// with the first row, first column being the bottom
// left of the board from white's perspective (i.e. a1)
Board *[8][8]Piece
Active Piece
WhiteCastling Castlability
BlackCastling Castlability
EnPassant Coordinate
HalfMoves int
FullMoves int
}
func FromFEN(str string) (*Board, error) {
result := Board{}
records := strings.Split(str, " ")
if len(records) != 6 {
return nil, errors.New("malformed FEN string, expected 6 records")
}
board, err := GenerateBoard(records[0])
if err != nil {
return &result, err
}
result.Board = board
return &result, nil
}

128
board/board_test.go Normal file
View File

@@ -0,0 +1,128 @@
package board
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestGetCoords(t *testing.T) {
t.Parallel()
tests := map[string]struct {
input byte
x, y byte
}{
"Zero": {
input: 0b0000_0000,
x: 0,
y: 0,
},
"Middle": {
input: 3<<4 + 3,
x: 3,
y: 3,
},
"Last": {
input: 7<<4 + 7,
x: 7,
y: 7,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
coord := Coordinate(test.input)
x, y := coord.GetCoords()
if x != test.x {
t.Errorf("Expected x to be %x, got %x", test.x, x)
}
if y != test.y {
t.Errorf("Expected y to be %x, got %x", test.y, y)
}
})
}
}
func TestCreateCoordInt(t *testing.T) {
tests := map[string]struct {
x int
y int
expected byte
}{
"Zero": {
x: 0,
y: 0,
expected: 0b0000_0000,
},
"Middle": {
x: 4,
y: 4,
expected: 4<<4 + 4,
},
"Last": {
x: 7,
y: 7,
expected: 7<<4 + 7,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
coord := CreateCoordInt(test.x, test.y)
if byte(coord) != test.expected {
t.Errorf("Expected (%d, %d) to become %x, got %x",
test.x, test.y, test.expected, coord)
}
})
}
}
func TestFromFEN(t *testing.T) {
startPos := "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
startRow := [8]Piece{Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook}
pawnRow := [8]Piece{Pawn, Pawn, Pawn, Pawn, Pawn, Pawn, Pawn, Pawn}
startBoard := &Board{
Board: &[8][8]Piece{
startRow, // White starting row
pawnRow, // White pawn row
{},
{},
{},
{},
pawnRow, // Black pawn row
startRow, // Black starting row
},
}
markRowColor(&startBoard.Board[0], White)
markRowColor(&startBoard.Board[1], White)
markRowColor(&startBoard.Board[6], Black)
markRowColor(&startBoard.Board[7], Black)
resultBoard, err := FromFEN(startPos)
if err != nil {
t.Errorf("encountered error when parsing from fen: %v", err)
}
if !cmp.Equal(*startBoard, *resultBoard) {
t.Errorf("Boards are not equal, expected %v, got %v", *startBoard, *resultBoard)
}
}
func markRowColor(row *[8]Piece, color Piece) {
for index, piece := range row {
if piece == 0 {
continue
}
row[index] = row[index] | color
}
}

92
board/fen.go Normal file
View File

@@ -0,0 +1,92 @@
package board
import (
"errors"
"fmt"
"strings"
)
func GenerateBoard(str string) (*[8][8]Piece, error) {
board := &[8][8]Piece{}
ranks := strings.Split(str, "/")
if len(ranks) != 8 {
return board, fmt.Errorf("invalid number of rows (%d)", len(ranks))
}
for rank, row := range ranks {
result, err := GenerateRow(row)
if err != nil {
return board, err
}
board[7-rank] = *result
}
return board, nil
}
func GenerateRow(str string) (*[8]Piece, error) {
row := &[8]Piece{}
if len(str) == 0 {
return row, nil
}
column := 0
for _, c := range str {
if column > len(row) {
return row, errors.New("out of bounds column")
}
if c >= '1' && c <= '8' {
column += int(c - '0')
continue
}
piece, err := GetPiece(c)
if err != nil {
return row, err
}
row[column] = piece
column++
}
return row, nil
}
func GetPiece(c rune) (Piece, error) {
white := true
if c > 'Z' {
white = false
c = 'A' + (c - 'a')
}
var result Piece
switch c {
case 'P':
result = Pawn
case 'N':
result = Knight
case 'B':
result = Bishop
case 'R':
result = Rook
case 'Q':
result = Queen
case 'K':
result = King
}
if result == 0 {
return 0, fmt.Errorf("unknown piece type: %c (%d)", c, c)
}
if !white {
result |= Black
}
return result, nil
}

117
board/fen_test.go Normal file
View File

@@ -0,0 +1,117 @@
package board
import "testing"
func TestGenerateBoard(t *testing.T) {
startRow := [8]Piece{Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook}
pawnRow := [8]Piece{Pawn, Pawn, Pawn, Pawn, Pawn, Pawn, Pawn, Pawn}
board := [8][8]Piece{
startRow, // White starting row
pawnRow, // White pawn row
{},
{},
{},
{},
pawnRow, // Black pawn row
startRow, // Black starting row
}
markRowColor(&board[0], White)
markRowColor(&board[1], White)
markRowColor(&board[6], Black)
markRowColor(&board[7], Black)
tests := map[string]struct {
input string
result [8][8]Piece
}{
"Starting": {
input: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
result: board,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
result, err := GenerateBoard(test.input)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if *result != test.result {
t.Errorf("boards are not equal, expected %v, got %v",
test.result, *result)
}
})
}
}
func TestGenerateRow(t *testing.T) {
tests := map[string]struct {
input string
result [8]Piece
}{
"Empty Row": {
input: "8",
result: [8]Piece{},
},
"All Pawns": {
input: "pppppppp",
result: [8]Piece{Pawn | Black, Pawn | Black, Pawn | Black, Pawn | Black, Pawn | Black, Pawn | Black, Pawn | Black, Pawn | Black},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
result, err := GenerateRow(test.input)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if *result != test.result {
t.Errorf("rows do not match, expected %v, got %v",
test.result, *result)
}
})
}
}
func TestGetPiece(t *testing.T) {
tests := []struct {
input rune
result Piece
}{
{input: 'K', result: King | White},
{input: 'Q', result: Queen | White},
{input: 'R', result: Rook | White},
{input: 'B', result: Bishop | White},
{input: 'N', result: Knight | White},
{input: 'P', result: Pawn | White},
{input: 'k', result: King | Black},
{input: 'q', result: Queen | Black},
{input: 'r', result: Rook | Black},
{input: 'b', result: Bishop | Black},
{input: 'n', result: Knight | Black},
{input: 'p', result: Pawn | Black},
}
for _, test := range tests {
t.Run(string(test.input), func(t *testing.T) {
result, err := GetPiece(test.input)
if err != nil {
t.Errorf("encountered unexpected error: %v", err)
}
if result != test.result {
t.Errorf("expected %x, got %x", test.result, result)
}
})
}
}

16
board/pieces.go Normal file
View File

@@ -0,0 +1,16 @@
package board
type Piece byte
const (
White Piece = iota
Black Piece = 1 << (iota - 1)
King
Queen
Rook
Bishop
Knight
Pawn
f
)

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/msws/chess
go 1.22.8
require github.com/google/go-cmp v0.6.0 // indirect

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

7
main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println(rune('Z'))
}