diff --git a/pkg/board/bitboard.go b/pkg/board/bitboard.go new file mode 100644 index 0000000..1bd66bb --- /dev/null +++ b/pkg/board/bitboard.go @@ -0,0 +1,82 @@ +package board + +import ( + "strconv" +) + +// Bitboard is defined as +type Bitboard uint64 + +// NewBitboard returns an initialized Bitboard. +func NewBitboard(m map[Square]bool) Bitboard { + s := "" + for sq := 0; sq < 64; sq++ { + if m[Square(sq)] { + s += "1" + } else { + s += "0" + } + } + bb, err := strconv.ParseUint(s, 2, 64) + if err != nil { + panic(err) + } + return Bitboard(bb) +} + +// Map returns a map[Square]bool representation of this Bitboard. +func (bb Bitboard) Map() map[Square]bool { + m := map[Square]bool{} + for sq := 0; sq < 64; sq++ { + if bb&Square(sq).Bitboard() > 0 { + m[Square(sq)] = true + } + } + return m +} + +// Squares returns a []Square of all bits in this Bitboard. +func (bb Bitboard) Squares() []Square { + squares := []Square{} + for sq := 0; sq < 64; sq++ { + if bb&Square(sq).Bitboard() > 0 { + squares = append(squares, Square(sq)) + } + } + return squares +} + +// Occupied returns true if the given Square is occupied. +func (bb Bitboard) Occupied(sq Square) bool { + return (uint64(bb) >> uint64(63-sq) & 1) == 1 +} + +// Draw returns visual representation of the bitboard useful for debugging. +func (bb Bitboard) Draw() string { + s := "\n A B C D E F G H\n" + for r := 7; r >= 0; r-- { + s += Rank(r).String() + s += " " + for f := 0; f < 8; f++ { + sq := NewSquare(File(f), Rank(r)) + if bb.Occupied(sq) { + s += "1" + } else { + s += "0" + } + s += " " + } + s += "\n" + } + return s +} + +// Flip toggles the bit at the given square. +func (bb *Bitboard) Flip(sq Square) { + if !sq.IsValid() { + return + } + oldBB := *bb + newBB := oldBB ^ sq.Bitboard() + *bb = newBB +} diff --git a/pkg/board/board.go b/pkg/board/board.go new file mode 100644 index 0000000..4e4453d --- /dev/null +++ b/pkg/board/board.go @@ -0,0 +1,303 @@ +package board + +import ( + "fmt" + "strconv" + "strings" +) + +// CastleRights is a helper for maintaining castling rights. +type CastleRights struct { + WhiteKingSide bool + WhiteQueenSide bool + BlackKingSide bool + BlackQueenSide bool +} + +// NewCastleRights returns an initialized CastleRights from the given string. +func NewCastleRights(s string) (*CastleRights, error) { + cr := &CastleRights{} + for _, c := range s { + switch c { + case 'K': + cr.WhiteKingSide = true + case 'Q': + cr.WhiteQueenSide = true + case 'k': + cr.BlackKingSide = true + case 'q': + cr.BlackQueenSide = true + default: + return nil, fmt.Errorf("invalid castle-rights: %s", s) + } + } + return cr, nil +} + +func (cr CastleRights) String() string { + var out string + if cr.WhiteKingSide { + out += WhiteKing.String() + } + if cr.WhiteQueenSide { + out += WhiteQueen.String() + } + if cr.BlackKingSide { + out += BlackKing.String() + } + if cr.BlackQueenSide { + out += BlackQueen.String() + } + return out +} + +// Board represents a chess board. +type Board struct { + // White + WhiteKing Bitboard + WhiteQueen Bitboard + WhiteBishop Bitboard + WhiteKnight Bitboard + WhiteRook Bitboard + WhitePawn Bitboard + // Black + BlackKing Bitboard + BlackQueen Bitboard + BlackBishop Bitboard + BlackKnight Bitboard + BlackRook Bitboard + BlackPawn Bitboard + // Convenience + WhitePieces Bitboard + BlackPieces Bitboard + FreeSquares Bitboard + + Turn Color + CastleRights CastleRights + EnPassent *Square + HalfMove uint64 + FullMove uint64 +} + +// NewBoard returns an initialized *Board. +func NewBoard(pieces map[Square]Piece, turn Color, cr CastleRights, ep *Square, halfMove, fullMove uint64) (*Board, error) { + board := new(Board) + board.WhiteKing = NewBitboard(nil) + board.WhiteQueen = NewBitboard(nil) + board.WhiteBishop = NewBitboard(nil) + board.WhiteKnight = NewBitboard(nil) + board.WhiteRook = NewBitboard(nil) + board.WhitePawn = NewBitboard(nil) + board.BlackKing = NewBitboard(nil) + board.BlackQueen = NewBitboard(nil) + board.BlackBishop = NewBitboard(nil) + board.BlackKnight = NewBitboard(nil) + board.BlackRook = NewBitboard(nil) + board.BlackPawn = NewBitboard(nil) + board.Turn = turn + board.CastleRights = cr + board.EnPassent = ep + board.HalfMove = halfMove + board.FullMove = fullMove + for square, piece := range pieces { + switch piece { + case WhiteKing: + board.WhiteKing = board.WhiteKing | NewBitboard(map[Square]bool{square: true}) + case WhiteQueen: + board.WhiteQueen = board.WhiteQueen | NewBitboard(map[Square]bool{square: true}) + case WhiteBishop: + board.WhiteBishop = board.WhiteBishop | NewBitboard(map[Square]bool{square: true}) + case WhiteKnight: + board.WhiteKnight = board.WhiteKnight | NewBitboard(map[Square]bool{square: true}) + case WhiteRook: + board.WhiteRook = board.WhiteRook | NewBitboard(map[Square]bool{square: true}) + case WhitePawn: + board.WhitePawn = board.WhitePawn | NewBitboard(map[Square]bool{square: true}) + case BlackKing: + board.BlackKing = board.BlackKing | NewBitboard(map[Square]bool{square: true}) + case BlackQueen: + board.BlackQueen = board.BlackQueen | NewBitboard(map[Square]bool{square: true}) + case BlackBishop: + board.BlackBishop = board.BlackBishop | NewBitboard(map[Square]bool{square: true}) + case BlackKnight: + board.BlackKnight = board.BlackKnight | NewBitboard(map[Square]bool{square: true}) + case BlackRook: + board.BlackRook = board.BlackRook | NewBitboard(map[Square]bool{square: true}) + case BlackPawn: + board.BlackPawn = board.BlackPawn | NewBitboard(map[Square]bool{square: true}) + default: + panic("invalid piece") + } + } + board.UpdateConvenienceBitboards() + return board, nil +} + +// UpdateConvenienceBitboards updates the convenience bitboards. +func (b *Board) UpdateConvenienceBitboards() { + b.WhitePieces = b.WhiteKing | b.WhiteQueen | b.WhiteBishop | b.WhiteKnight | b.WhiteRook | b.WhitePawn + b.BlackPieces = b.BlackKing | b.BlackQueen | b.BlackBishop | b.BlackKnight | b.BlackRook | b.BlackPawn + b.FreeSquares = ^(b.WhitePieces | b.BlackPieces) +} + +// IsOccupied returns true if the given field is occupied. +func (b Board) IsOccupied(sq Square) bool { + return sq.Bitboard()&b.FreeSquares == 0 +} + +// Piece returns the piece on the given field. +func (b Board) Piece(sq Square) Piece { + contains := func(bb Bitboard, sq Square) bool { + for _, square := range bb.Squares() { + if square == sq { + return true + } + } + return false + } + if contains(b.WhiteKing, sq) { + return WhiteKing + } + if contains(b.WhiteQueen, sq) { + return WhiteQueen + } + if contains(b.WhiteBishop, sq) { + return WhiteBishop + } + if contains(b.WhiteKnight, sq) { + return WhiteKnight + } + if contains(b.WhiteRook, sq) { + return WhiteRook + } + if contains(b.WhitePawn, sq) { + return WhitePawn + } + if contains(b.BlackKing, sq) { + return BlackKing + } + if contains(b.BlackQueen, sq) { + return BlackQueen + } + if contains(b.BlackBishop, sq) { + return BlackBishop + } + if contains(b.BlackKnight, sq) { + return BlackKnight + } + if contains(b.BlackRook, sq) { + return BlackRook + } + if contains(b.BlackPawn, sq) { + return BlackPawn + } + return NoPiece +} + +func (b Board) String() string { + fen := "" + for r := 7; r >= 0; r-- { + for f := 0; f < 8; f++ { + sq := NewSquare(File(f), Rank(r)) + p := b.Piece(sq) + if p != NoPiece { + fen += p.String() + } else { + fen += "1" + } + } + if r != 0 { + fen += "/" + } + } + for i := 8; i > 1; i-- { + repeatStr := strings.Repeat("1", i) + countStr := strconv.Itoa(i) + fen = strings.Replace(fen, repeatStr, countStr, -1) + } + ep := "-" + if b.EnPassent != nil { + ep = b.EnPassent.String() + } + fen += fmt.Sprintf( + " %s %s %s %d %d", + b.Turn, + b.CastleRights, + ep, + b.HalfMove, + b.FullMove, + ) + return fen +} + +// Draw prints an ASCII art of this board to STDOUT. +func (b Board) Draw() string { + s := "\n A B C D E F G H\n" + for r := 7; r >= 0; r-- { + s += Rank(r).String() + s += " " + for f := 0; f < 8; f++ { + p := b.Piece(NewSquare(File(f), Rank(r))) + if p == NoPiece { + s += "." + } else { + s += p.Rune() + } + s += " " + } + s += "\n" + } + return s +} + +// UpdatePiece moves the a piece on the board from the given src to dest square. +func (b *Board) UpdatePiece(src, dest Square) error { + if !b.IsOccupied(src) { + return fmt.Errorf("can not move piece on vacant square: %s", src) + } + if b.IsOccupied(dest) { + return fmt.Errorf("can not move to occupied square: %s", dest) + } + switch b.Piece(src) { + case WhiteKing: + b.WhiteKing.Flip(src) + b.WhiteKing.Flip(dest) + case WhiteQueen: + b.WhiteQueen.Flip(src) + b.WhiteQueen.Flip(dest) + case WhiteRook: + b.WhiteRook.Flip(src) + b.WhiteRook.Flip(dest) + case WhiteBishop: + b.WhiteBishop.Flip(src) + b.WhiteBishop.Flip(dest) + case WhiteKnight: + b.WhiteKnight.Flip(src) + b.WhiteKnight.Flip(dest) + case WhitePawn: + b.WhitePawn.Flip(src) + b.WhitePawn.Flip(dest) + case BlackKing: + b.BlackKing.Flip(src) + b.BlackKing.Flip(dest) + case BlackQueen: + b.BlackQueen.Flip(src) + b.BlackQueen.Flip(dest) + case BlackRook: + b.BlackRook.Flip(src) + b.BlackRook.Flip(dest) + case BlackBishop: + b.BlackBishop.Flip(src) + b.BlackBishop.Flip(dest) + case BlackKnight: + b.BlackKnight.Flip(src) + b.BlackKnight.Flip(dest) + case BlackPawn: + b.BlackPawn.Flip(src) + b.BlackPawn.Flip(dest) + default: + panic("") + } + b.UpdateConvenienceBitboards() + return nil +} diff --git a/pkg/board/move.go b/pkg/board/move.go new file mode 100644 index 0000000..1bbaa2e --- /dev/null +++ b/pkg/board/move.go @@ -0,0 +1,73 @@ +package board + +import ( + "fmt" + "strings" +) + +// MoveProp is defined as +// +type MoveProp uint16 + +// Available MoveProps +const ( + QueenSideCastle MoveProp = 1 << iota + KingSideCastle + Capture + EnPassant + Check + Mate + inCheck +) + +// Move is defined as +type Move struct { + Piece PieceType + FromRank *Rank + FromFile *File + To Square + PromoteTo PieceType + Props MoveProp +} + +func (m Move) String() string { + out := "" + if m.HasProp(KingSideCastle) { + out += "O-O" + } else if m.HasProp(QueenSideCastle) { + out += "O-O-O" + } else { + if m.Piece != NoPieceType && m.Piece != Pawn { + out += strings.ToUpper(m.Piece.String()) + } + if m.FromFile != nil { + out += m.FromFile.String() + } + if m.FromRank != nil { + out += m.FromRank.String() + } + if m.HasProp(Capture) { + out += "x" + } + out += m.To.String() + if m.PromoteTo != NoPieceType { + out += fmt.Sprintf("=%s", m.PromoteTo.String()) + } + } + if m.HasProp(Check) { + out += "+" + } else if m.HasProp(Mate) { + out += "#" + } + return out +} + +// AddProp adds the given MoveProp to this move. +func (m *Move) AddProp(prop MoveProp) { + m.Props = m.Props | prop +} + +// HasProp returns true if this move has the given MoveProp. +func (m Move) HasProp(prop MoveProp) bool { + return m.Props&prop > 0 +} diff --git a/pkg/board/pieces.go b/pkg/board/pieces.go new file mode 100644 index 0000000..4a51d0b --- /dev/null +++ b/pkg/board/pieces.go @@ -0,0 +1,205 @@ +package board + +import ( + "fmt" + "strings" + + "code.c-base.org/gochess/libchess/pkg/runes" +) + +// PieceType defines the type of a piece +type PieceType uint8 + +// The following PieceTypes exist +const ( + NoPieceType PieceType = iota + King + Queen + Rook + Bishop + Knight + Pawn +) + +func (pt PieceType) String() string { + switch pt { + case King: + return "k" + case Queen: + return "q" + case Rook: + return "r" + case Bishop: + return "b" + case Knight: + return "n" + case Pawn: + return "p" + default: + return "-" + } +} + +// PromotableTo returns true if a pawn can be promoted to the given PieceType. +func (pt PieceType) PromotableTo() bool { + switch pt { + case Queen, Rook, Bishop, Knight: + return true + } + return false +} + +// PieceTypes returns a slice of all piece types. +func PieceTypes() [6]PieceType { + return [6]PieceType{King, Queen, Rook, Bishop, Knight, Pawn} +} + +// Piece is defined as +type Piece uint8 + +// The following Pieces exist: +const ( + NoPiece Piece = iota + // White + WhiteKing + WhiteQueen + WhiteRook + WhiteBishop + WhiteKnight + WhitePawn + // Black + BlackKing + BlackQueen + BlackRook + BlackBishop + BlackKnight + BlackPawn +) + +// AllPieces returns a []Piece containing with all available Pieces. +func AllPieces() []Piece { + return []Piece{ + WhiteKing, WhiteQueen, WhiteRook, WhiteBishop, WhiteKnight, WhitePawn, + BlackKing, BlackQueen, BlackRook, BlackBishop, BlackKnight, BlackPawn, + } +} + +// NewPiece returns an initialized Piece based on the given PieceType and Color. +func NewPiece(t PieceType, c Color) Piece { + for _, p := range AllPieces() { + if p.Color() == c && p.Type() == t { + return p + } + } + return NoPiece +} + +// NewPieceByFEN returns an initialized piece based on the given FEN letter representation. +func NewPieceByFEN(c rune) Piece { + switch c { + case 'K': + return WhiteKing + case 'Q': + return WhiteQueen + case 'R': + return WhiteRook + case 'B': + return WhiteBishop + case 'N': + return WhiteKnight + case 'P': + return WhitePawn + case 'k': + return BlackKing + case 'q': + return BlackQueen + case 'r': + return BlackRook + case 'b': + return BlackBishop + case 'n': + return BlackKnight + case 'p': + return BlackPawn + case '1': + return NoPiece + default: + panic(fmt.Errorf("unable to parse piece: %s", string(c))) + } +} + +func (p Piece) String() string { + s := p.Type().String() + if p.Color() == White { + s = strings.ToUpper(s) + } + return s +} + +// Rune returns the UTF-8 char for this piece. +func (p Piece) Rune() string { + var r rune + switch p.Color() { + case White: + switch p.Type() { + case King: + r = runes.WhiteKing + case Queen: + r = runes.WhiteQueen + case Rook: + r = runes.WhiteRook + case Bishop: + r = runes.WhiteBishop + case Knight: + r = runes.WhiteKnight + case Pawn: + r = runes.WhitePawn + } + case Black: + switch p.Type() { + case King: + r = runes.BlackKing + case Queen: + r = runes.BackQueen + case Rook: + r = runes.BlackRook + case Bishop: + r = runes.BlackBishop + case Knight: + r = runes.BlackKnight + case Pawn: + r = runes.BlackPawn + } + } + return string(r) +} + +// Type returns the type of the piece. +func (p Piece) Type() PieceType { + switch p { + case WhiteKing, BlackKing: + return King + case WhiteQueen, BlackQueen: + return Queen + case WhiteRook, BlackRook: + return Rook + case WhiteBishop, BlackBishop: + return Bishop + case WhiteKnight, BlackKnight: + return Knight + case WhitePawn, BlackPawn: + return Pawn + } + return NoPieceType +} + +// Color returns the color of the piece. +func (p Piece) Color() Color { + switch p { + case WhiteKing, WhiteQueen, WhiteRook, WhiteBishop, WhiteKnight, WhitePawn: + return White + case BlackKing, BlackQueen, BlackRook, BlackBishop, BlackKnight, BlackPawn: + return Black + } + return NoColor +} diff --git a/pkg/board/squares.go b/pkg/board/squares.go new file mode 100644 index 0000000..e76b510 --- /dev/null +++ b/pkg/board/squares.go @@ -0,0 +1,268 @@ +package board + +// File represents a file of squares on the chess board. +type File int8 + +// The following files exist: +const ( + FileA File = iota + FileB + FileC + FileD + FileE + FileF + FileG + FileH +) + +// FileChars contains the available file letters a-h. +const FileChars = "abcdefgh" + +func (f File) String() string { + return FileChars[f : f+1] +} + +// Next returns the next File. +func (f File) Next() File { + return File(f + 1) +} + +// Previous returns the previous File. +func (f File) Previous() File { + return File(f - 1) +} + +// Shift returns the theoretical File after shifting by the given int. +func (f File) Shift(i int) File { + return File(int(f) + i) +} + +// IsValid returns true if this file is valid. +func (f File) IsValid() bool { + return FileA <= f && f <= FileH +} + +// Rank represents a rank of squares on the chess board. +type Rank int8 + +// The following ranks exist: +const ( + Rank1 Rank = iota + Rank2 + Rank3 + Rank4 + Rank5 + Rank6 + Rank7 + Rank8 +) + +// RankChars contains all rank chars +const RankChars = "12345678" + +func (r Rank) String() string { + return RankChars[r : r+1] +} + +// Next returns the next Rank. +func (r Rank) Next() Rank { + return Rank(r + 1) +} + +// Previous returns the previous Rank. +func (r Rank) Previous() Rank { + return Rank(r - 1) +} + +// Shift returns the theoretical Rank after shifting the given int. +func (r Rank) Shift(i int) Rank { + return Rank(int(r) + i) +} + +// IsValid returns true if this is a valid rank. +func (r Rank) IsValid() bool { + return Rank1 <= r && r <= Rank8 +} + +// Color is defined as +type Color uint8 + +// Two colors exist +const ( + NoColor Color = iota + White + Black +) + +func (c Color) String() string { + switch c { + case White: + return "w" + case Black: + return "b" + } + return "-" +} + +// Name returns the name of the color. +func (c Color) Name() string { + switch c { + case White: + return "White" + case Black: + return "Black" + } + return "NoColor" +} + +// Other returns the opposite color. +func (c Color) Other() Color { + switch c { + case White: + return Black + case Black: + return White + } + return NoColor +} + +// Side is defined as +type Side uint8 + +// The sides of the board are . +const ( + QueenSide Side = iota + KingSide +) + +// Square represents a square on the chess board. +type Square int8 + +// The following Squares exist +const ( + NoSquare Square = iota - 1 + A1 + B1 + C1 + D1 + E1 + F1 + G1 + H1 + A2 + B2 + C2 + D2 + E2 + F2 + G2 + H2 + A3 + B3 + C3 + D3 + E3 + F3 + G3 + H3 + A4 + B4 + C4 + D4 + E4 + F4 + G4 + H4 + A5 + B5 + C5 + D5 + E5 + F5 + G5 + H5 + A6 + B6 + C6 + D6 + E6 + F6 + G6 + H6 + A7 + B7 + C7 + D7 + E7 + F7 + G7 + H7 + A8 + B8 + C8 + D8 + E8 + F8 + G8 + H8 +) + +// StrToSquareMap maps all Squares to the string representation. +var StrToSquareMap = map[string]Square{ + "a1": A1, "a2": A2, "a3": A3, "a4": A4, "a5": A5, "a6": A6, "a7": A7, "a8": A8, + "b1": B1, "b2": B2, "b3": B3, "b4": B4, "b5": B5, "b6": B6, "b7": B7, "b8": B8, + "c1": C1, "c2": C2, "c3": C3, "c4": C4, "c5": C5, "c6": C6, "c7": C7, "c8": C8, + "d1": D1, "d2": D2, "d3": D3, "d4": D4, "d5": D5, "d6": D6, "d7": D7, "d8": D8, + "e1": E1, "e2": E2, "e3": E3, "e4": E4, "e5": E5, "e6": E6, "e7": E7, "e8": E8, + "f1": F1, "f2": F2, "f3": F3, "f4": F4, "f5": F5, "f6": F6, "f7": F7, "f8": F8, + "g1": G1, "g2": G2, "g3": G3, "g4": G4, "g5": G5, "g6": G6, "g7": G7, "g8": G8, + "h1": H1, "h2": H2, "h3": H3, "h4": H4, "h5": H5, "h6": H6, "h7": H7, "h8": H8, +} + +// NewSquare returns an initialized Square. +func NewSquare(f File, r Rank) Square { + return Square((int(r) * 8) + int(f)) +} + +// Color returns the color of this Square. +func (sq Square) Color() Color { + if ((sq / 8) % 2) == (sq % 2) { + return Black + } + return White +} + +// File returns the File this Square is in. +func (sq Square) File() File { + return File(uint(sq) % 8) +} + +// Rank returns the Rank the Square is in. +func (sq Square) Rank() Rank { + return Rank(uint(sq) / 8) +} + +// Side returns the side of board this square is on. +func (sq Square) Side() Side { + if sq.File() <= FileA && sq.File() <= FileD { + return QueenSide + } + return KingSide +} + +func (sq Square) String() string { + if sq == NoSquare { + return "-" + } + return sq.File().String() + sq.Rank().String() +} + +// IsValid returns true if this is a valid square. +func (sq Square) IsValid() bool { + return int(A1) <= int(sq) && int(sq) <= int(H8) +} + +// Bitboard returns the Bitboard for this square. +func (sq Square) Bitboard() Bitboard { + return Bitboard(uint64(1) << (uint8(63) - uint8(sq))) +} diff --git a/pkg/game/game.go b/pkg/game/game.go new file mode 100644 index 0000000..a6a1c88 --- /dev/null +++ b/pkg/game/game.go @@ -0,0 +1,38 @@ +package game + +import ( + "fmt" + + "code.c-base.org/gochess/libchess/pkg/board" + "code.c-base.org/gochess/libchess/pkg/fen" +) + +// Tag serves as data-container for game tags. +type Tag struct { + Key string + Value string +} + +// Game represents a game of chess. +type Game struct { + Tags []Tag + Board *board.Board + Moves []*board.Move +} + +// NewGame returns an initialized game. +func NewGame() *Game { + b, err := fen.Import(fen.DefaultSetup) + if err != nil { + panic(err) + } + return &Game{ + Tags: []Tag{}, + Board: b, + Moves: []*board.Move{}, + } +} + +func (g Game) String() string { + return fmt.Sprintf("", g.Tags, g.Moves) +} diff --git a/pkg/runes/runes.go b/pkg/runes/runes.go new file mode 100644 index 0000000..dd382fe --- /dev/null +++ b/pkg/runes/runes.go @@ -0,0 +1,19 @@ +package runes + +// The relevant unicode runes for chess are: +const ( + WhiteKing = '♔' + WhiteQueen = '♕' + WhiteRook = '♖' + WhiteBishop = '♗' + WhiteKnight = '♘' + WhitePawn = '♙' + BlackKing = '♚' + BackQueen = '♛' + BlackRook = '♜' + BlackBishop = '♝' + BlackKnight = '♞' + BlackPawn = '♟' + Check = '†' + Mate = '‡' +)