mchess-server/chess/board.go
Marco ff2ec599fe Introduce PGN helpers
In order to simplify special moves like en passant or castling for the
client, we want to deliver the board state after every move (and not
only start square and end square).

With PGN we can encode a chess position into a string.
This commit implies changes to logic of the pieces' shortnames. This
will break the client/server connection (at least for promotions).
2023-08-12 11:24:40 +02:00

207 lines
5.5 KiB
Go

package chess
import (
"errors"
"mchess_server/types"
"github.com/samber/lo"
)
type Position map[types.Coordinate]Piece
type Board struct {
position Position
history []types.Move
colorToMove types.ChessColor
state types.AdditionalState
}
func newBoard() Board {
return Board{
position: make(Position),
history: make([]types.Move, 0),
colorToMove: types.White,
state: types.AdditionalState{},
}
}
func (b *Board) Init() {
var coord types.Coordinate
for i := 1; i <= 8; i++ {
coord.Row = 2
coord.Col = i
b.position[coord] = Pawn{Color: types.White}
coord.Row = 7
coord.Col = i
b.position[coord] = Pawn{Color: types.Black}
}
b.position[types.Coordinate{Row: 1, Col: 1}] = Rook{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 2}] = Knight{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 3}] = Bishop{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 4}] = Queen{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 5}] = King{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 6}] = Bishop{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 7}] = Knight{Color: types.White}
b.position[types.Coordinate{Row: 1, Col: 8}] = Rook{Color: types.White}
b.position[types.Coordinate{Row: 8, Col: 1}] = Rook{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 2}] = Knight{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 3}] = Bishop{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 4}] = Queen{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 5}] = King{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 6}] = Bishop{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 7}] = Knight{Color: types.Black}
b.position[types.Coordinate{Row: 8, Col: 8}] = Rook{Color: types.Black}
}
func (b *Board) CheckAndPlay(move types.Move) (bool, Violation) {
tempBoard := b.getCopyOfBoard()
//Check start square of move
pieceAtStartSquare := tempBoard.getPieceAt(move.StartSquare)
if pieceAtStartSquare == nil {
return false, NoPieceAtStartSquare
}
move.ColorMoved = pieceAtStartSquare.GetColor()
if move.ColorMoved != tempBoard.colorToMove {
return false, WrongColorMoved
}
move.PieceMoved = GetShortNameForPiece(pieceAtStartSquare)
//Check end square of move
pieceAtEndSquare := tempBoard.getPieceAt(move.EndSquare)
if pieceAtEndSquare != nil {
if pieceAtEndSquare.GetColor() == pieceAtStartSquare.GetColor() {
return false, TargetSquareIsOccupied
}
}
wasSpecialMove, err := tempBoard.handleSpecialMove(move)
if err != nil {
return false, InvalidMove
}
if !wasSpecialMove {
allMovesExceptBlocked := pieceAtStartSquare.GetAllNonBlockedMoves(tempBoard, move.StartSquare)
legal := lo.Contains(allMovesExceptBlocked, move.EndSquare)
if !legal {
return false, InvalidMove
}
//We play the move on the temporary board
delete(tempBoard.position, move.StartSquare)
tempBoard.position[move.EndSquare] = pieceAtStartSquare
}
kingAttacked, err := tempBoard.isKingOfMovingColorInCheck(move.ColorMoved)
if err != nil {
return false, SomethingWentWrong
}
if kingAttacked {
return false, KingInCheck
}
//We play the move on the real board
b.position = tempBoard.position
b.history = tempBoard.history
b.colorToMove = b.colorToMove.Opposite()
b.appendMoveToHistory(move)
pieceAtStartSquare.AfterMoveAction(b, move.StartSquare)
return true, ""
}
func (b Board) isKingOfMovingColorInCheck(color types.ChessColor) (bool, error) {
//Check if king of moving color is in check -> move not allowed
//Do that by checking if the king is in a square attacked by the other color.
ownKingCoordinate := b.getSquareOfPiece(King{Color: color})
if ownKingCoordinate == nil {
return false, errors.New("no king found")
}
kingIsAttacked := b.isSquareAttacked(*ownKingCoordinate, color.Opposite())
if kingIsAttacked {
return true, nil
}
return false, nil
}
func (b Board) getSquareOfPiece(piece Piece) *types.Coordinate {
for k, v := range b.position {
if v == piece {
return &k
}
}
return nil
}
func (b Board) isSquareAttacked(square types.Coordinate, byColor types.ChessColor) bool {
var attackedSquares []types.Coordinate
for square, piece := range b.position {
if piece.GetColor() == byColor {
attackedSquares = append(attackedSquares, piece.GetAllAttackedSquares(b, square)...)
}
}
return lo.Contains(attackedSquares, square)
}
func (b Board) getPieceAt(coord types.Coordinate) Piece {
piece, found := b.position[coord]
if !found {
return nil
}
return piece
}
func (b *Board) appendMoveToHistory(move types.Move) {
b.history = append(b.history, move)
}
func (b Board) getLastMove() types.Move {
if len(b.history) < 1 {
return types.Move{}
}
return b.history[len(b.history)-1]
}
func (b Board) getCopyOfBoard() Board {
return Board{
position: b.position.getCopyOfPosition(),
history: b.history,
colorToMove: b.colorToMove,
state: b.state,
}
}
func (p Position) getCopyOfPosition() Position {
position := make(Position)
for coord, piece := range p {
position[coord] = piece
}
return position
}
func (b *Board) handleSpecialMove(move types.Move) (bool, error) {
var was bool
var err error
var pieceAtStartSquare = b.getPieceAt(move.StartSquare)
switch piece := pieceAtStartSquare.(type) {
case Pawn:
was, err = piece.HandlePossiblePromotion(b, move)
if !was {
was, err = piece.HandleEnPassant(b, move, b.getLastMove())
}
case King:
was, err = piece.HandleCastling(b, move)
}
return was, err
}