diff --git a/chess/board.go b/chess/board.go index 3e2b2d3..105b377 100644 --- a/chess/board.go +++ b/chess/board.go @@ -6,67 +6,76 @@ import ( "github.com/samber/lo" ) -type Board map[types.Coordinate]Piece +type Position map[types.Coordinate]Piece -func (b Board) Init() { +type Board struct { + position Position + history []types.Move +} + +func newBoard() Board { + return Board{ + position: make(Position), + history: make([]types.Move, 0), + } +} + +func (b *Board) Init() { var coord types.Coordinate for i := 1; i <= 8; i++ { coord.Row = 2 coord.Col = i - b[coord] = Pawn{Color: types.White} + b.position[coord] = Pawn{Color: types.White} coord.Row = 7 coord.Col = i - b[coord] = Pawn{Color: types.Black} + b.position[coord] = Pawn{Color: types.Black} } - b[types.Coordinate{Row: 1, Col: 1}] = Rook{Color: types.White} - b[types.Coordinate{Row: 1, Col: 2}] = Knight{Color: types.White} - b[types.Coordinate{Row: 1, Col: 3}] = Bishop{Color: types.White} - b[types.Coordinate{Row: 1, Col: 4}] = Queen{Color: types.White} - b[types.Coordinate{Row: 1, Col: 5}] = King{Color: types.White} - b[types.Coordinate{Row: 1, Col: 6}] = Bishop{Color: types.White} - b[types.Coordinate{Row: 1, Col: 7}] = Knight{Color: types.White} - b[types.Coordinate{Row: 1, Col: 8}] = Rook{Color: types.White} + 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[types.Coordinate{Row: 8, Col: 1}] = Rook{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 2}] = Knight{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 3}] = Bishop{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 4}] = Queen{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 5}] = King{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 6}] = Bishop{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 7}] = Knight{Color: types.Black} - b[types.Coordinate{Row: 8, Col: 8}] = Rook{Color: types.Black} + 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) CheckMove(move types.Move) (bool, string) { +func (b *Board) CheckAndPlay(move types.Move) (bool, string) { // We make a copy of the original board to play moves on it, // We can play the move on it and then check if it is invalid tempBoard := b.getCopyOfBoard() //Check start square of move - pieceAtStartSquare := b.getPieceAt(move.StartSquare) + pieceAtStartSquare := tempBoard.getPieceAt(move.StartSquare) if pieceAtStartSquare == nil { return false, "no piece at start square" } - movingColor := pieceAtStartSquare.GetColor() + move.ColorMoved = pieceAtStartSquare.GetColor() + move.PieceMoved = GetShortNameForPiece(pieceAtStartSquare) //Check end square of move - pieceAtEndSquare := b.getPieceAt(move.EndSquare) + pieceAtEndSquare := tempBoard.getPieceAt(move.EndSquare) if pieceAtEndSquare != nil { if pieceAtEndSquare.GetColor() == pieceAtStartSquare.GetColor() { return false, "same-coloured piece at end square" } } - var wasPromotionMove bool - // var piece types.PieceShortName - switch pieceAtStartSquare.(type) { - case Pawn: - wasPromotionMove, _ = tempBoard.handlePossiblePromotion(move, movingColor) - } - if !wasPromotionMove { + wasSpecialMove := tempBoard.handleSpecialMove(move) + + if !wasSpecialMove { // At the moment, we do not need to check if the correct color is moving, //since we are only reading moves from the player whose turn it is. allMovesExceptBlocked := pieceAtStartSquare.GetAllMovesButBlocked(tempBoard, move.StartSquare) @@ -76,18 +85,18 @@ func (b Board) CheckMove(move types.Move) (bool, string) { } //We play the move on the temporary board - delete(tempBoard, move.StartSquare) - tempBoard[move.EndSquare] = pieceAtStartSquare + delete(tempBoard.position, move.StartSquare) + tempBoard.position[move.EndSquare] = pieceAtStartSquare } //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 := tempBoard.getSquareOfPiece(King{Color: movingColor}) + ownKingCoordinate := tempBoard.getSquareOfPiece(King{Color: move.ColorMoved}) if ownKingCoordinate == nil { - return false, string(movingColor) + " king not found" + return false, string(move.ColorMoved) + " king not found" } - kingIsAttacked := tempBoard.isSquareAttacked(*ownKingCoordinate, movingColor.Opposite()) + kingIsAttacked := tempBoard.isSquareAttacked(*ownKingCoordinate, move.ColorMoved.Opposite()) if kingIsAttacked { return false, "king is attacked after move" } @@ -106,13 +115,15 @@ func (b Board) CheckMove(move types.Move) (bool, string) { // If a piece can be moved into the path, it is no checkmate //We play the move on the real board - b = tempBoard + b.position = tempBoard.position + b.history = tempBoard.history + b.appendMoveToHistory(move) return true, "" } func (b Board) getSquareOfPiece(piece Piece) *types.Coordinate { - for k, v := range b { + for k, v := range b.position { if v == piece { return &k } @@ -123,7 +134,7 @@ func (b Board) getSquareOfPiece(piece Piece) *types.Coordinate { func (b Board) isSquareAttacked(square types.Coordinate, byColor types.ChessColor) bool { var attackedSquares []types.Coordinate - for square, piece := range b { + for square, piece := range b.position { attackedSquares = append(attackedSquares, piece.GetAllMovesButBlocked(b, square)...) } @@ -131,7 +142,7 @@ func (b Board) isSquareAttacked(square types.Coordinate, byColor types.ChessColo } func (b Board) getPieceAt(coord types.Coordinate) Piece { - piece, found := b[coord] + piece, found := b.position[coord] if !found { return nil } @@ -139,17 +150,59 @@ func (b Board) getPieceAt(coord types.Coordinate) Piece { return piece } -func (b Board) handlePossiblePromotion(move types.Move, color types.ChessColor) (bool, types.PieceShortName) { +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, + } +} + +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 { + var was bool + var pieceAtStartSquare = b.getPieceAt(move.StartSquare) + + switch pieceAtStartSquare.(type) { + case Pawn: + was = b.handlePossiblePromotion(move) + if !was { + was = b.handleEnPassant(move, b.getLastMove()) + } + } + return was +} + +func (b *Board) handlePossiblePromotion(move types.Move) bool { var isPromotionMove bool var promotionToPiece types.PieceShortName + //TODO(m): What if message does not contain a promotion, but should be because a pawn is moved to the end square messageContainsPromotion := move.IsPromotionMove() if messageContainsPromotion { promotionToPiece = *move.PromotionToPiece } - switch color { + switch move.ColorMoved { case types.White: if move.StartSquare.Row == types.RangeLastValid-1 && move.EndSquare.Row == types.RangeLastValid { @@ -164,19 +217,63 @@ func (b Board) handlePossiblePromotion(move types.Move, color types.ChessColor) } if isPromotionMove { - delete(b, move.StartSquare) - b[move.EndSquare] = GetPieceForShortName(promotionToPiece) + delete(b.position, move.StartSquare) + b.position[move.EndSquare] = GetPieceForShortName(promotionToPiece, move.ColorMoved) } - return isPromotionMove, promotionToPiece + return isPromotionMove } -func (b Board) getCopyOfBoard() Board { - board := make(map[types.Coordinate]Piece) +func (b *Board) handleEnPassant(move, lastMove types.Move) bool { + var wasEnPassant bool - for coord, piece := range b { - board[coord] = piece + if lastMove.PieceMoved != types.PawnShortName { + return false } - return board + switch move.ColorMoved { + case types.White: + if lastMove.StartSquare.Row != 7 || lastMove.EndSquare.Row != 5 { + wasEnPassant = false + } + if move.StartSquare.Row != 5 { + wasEnPassant = false + } + if move.EndSquare.Row != 6 { + wasEnPassant = false + } + if move.StartSquare.Col == lastMove.EndSquare.Col+1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + } + if move.StartSquare.Col == lastMove.EndSquare.Col-1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + } + case types.Black: + if lastMove.StartSquare.Row != 2 || lastMove.EndSquare.Row != 4 { + wasEnPassant = false + } + if move.StartSquare.Row != 4 { + wasEnPassant = false + } + if move.EndSquare.Row != 3 { + wasEnPassant = false + } + if move.StartSquare.Col == lastMove.EndSquare.Col+1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + } + if move.StartSquare.Col == lastMove.EndSquare.Col-1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + } + } + + if wasEnPassant { //play the move + delete(b.position, lastMove.EndSquare) + b.position[move.EndSquare] = GetPieceForShortName(move.PieceMoved, move.ColorMoved) + } + + return wasEnPassant } diff --git a/chess/board_test.go b/chess/board_test.go index dfd1f68..e9d7238 100644 --- a/chess/board_test.go +++ b/chess/board_test.go @@ -8,64 +8,191 @@ import ( ) func Test_CheckMove_validPawnMove(t *testing.T) { - var board = make(Board) + var board = newBoard() - board[types.Coordinate{Col: 1, Row: 1}] = Pawn{Color: types.White} - board[types.Coordinate{Col: 1, Row: 5}] = King{Color: types.White} - board[types.Coordinate{Col: 8, Row: 5}] = King{Color: types.Black} + board.position[types.Coordinate{Col: 1, Row: 2}] = Pawn{Color: types.White} + board.position[types.Coordinate{Col: 2, Row: 4}] = Pawn{Color: types.Black} + board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White} + board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black} move := types.Move{ - StartSquare: types.Coordinate{Col: 1, Row: 1}, - EndSquare: types.Coordinate{Col: 1, Row: 2}, + StartSquare: types.Coordinate{Col: 1, Row: 2}, + EndSquare: types.Coordinate{Col: 1, Row: 3}, } - good, _ := board.CheckMove(move) + good, _ := board.CheckAndPlay(move) + assert.True(t, good) + + //we take the pawn + secondMove := types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 4}, + EndSquare: types.Coordinate{Col: 1, Row: 3}, + } + good, _ = board.CheckAndPlay(secondMove) assert.True(t, good) } -func Test_CheckMove_invalidPawnMoves(t *testing.T) { - var board = make(Board) +func Test_CheckMove_enPassant(t *testing.T) { + var board = newBoard() - board[types.Coordinate{Col: 2, Row: 5}] = Pawn{Color: types.White} - board[types.Coordinate{Col: 1, Row: 5}] = King{Color: types.White} - board[types.Coordinate{Col: 7, Row: 5}] = Queen{Color: types.Black} - board[types.Coordinate{Col: 8, Row: 5}] = King{Color: types.Black} + board.position[types.Coordinate{Col: 6, Row: 4}] = Pawn{Color: types.Black} + board.position[types.Coordinate{Col: 5, Row: 2}] = Pawn{Color: types.White} + board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White} + board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black} move := types.Move{ - StartSquare: types.Coordinate{Col: 2, Row: 5}, - EndSquare: types.Coordinate{Col: 2, Row: 6}, + StartSquare: types.Coordinate{Col: 5, Row: 2}, + EndSquare: types.Coordinate{Col: 5, Row: 4}, } + good, reason := board.CheckAndPlay(move) + assert.True(t, good) + assert.Empty(t, reason) + assert.Equal(t, Pawn{Color: types.White}, board.position[types.Coordinate{Col: 5, Row: 4}]) + + newMove := types.Move{ + StartSquare: types.Coordinate{Col: 6, Row: 4}, + EndSquare: types.Coordinate{Col: 5, Row: 3}, + } + good, reason = board.CheckAndPlay(newMove) + assert.True(t, good) + assert.Empty(t, reason) + // the black pawn is on its correct square + assert.Equal(t, Pawn{Color: types.Black}, board.position[types.Coordinate{Col: 5, Row: 3}]) + //the white pawn is gone + assert.Nil(t, board.position[types.Coordinate{Col: 5, Row: 4}]) +} + +func Test_CheckMove_invalidPawnMoves(t *testing.T) { t.Run("pawn is blocked", func(t *testing.T) { - testBoard := board.getCopyOfBoard() - testBoard[types.Coordinate{Col: 2, Row: 6}] = Pawn{Color: types.Black} - legalMove, _ := testBoard.CheckMove(move) + var board = newBoard() + + board.position[types.Coordinate{Col: 2, Row: 5}] = Pawn{Color: types.White} + board.position[types.Coordinate{Col: 1, Row: 5}] = King{Color: types.White} + board.position[types.Coordinate{Col: 7, Row: 5}] = Queen{Color: types.Black} + board.position[types.Coordinate{Col: 8, Row: 5}] = King{Color: types.Black} + board.position[types.Coordinate{Col: 2, Row: 6}] = Pawn{Color: types.Black} + + move := types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 5}, + EndSquare: types.Coordinate{Col: 2, Row: 6}, + } + legalMove, _ := board.CheckAndPlay(move) assert.False(t, legalMove) }) - t.Run("king of moving color is in check after move", func(t *testing.T) { - good, _ := board.CheckMove(move) - assert.False(t, good) + t.Run("pawn moves to the side", func(t *testing.T) { + var board = newBoard() + boardBeforeMove := board + + board.position[types.Coordinate{Col: 1, Row: 5}] = King{Color: types.White} + board.position[types.Coordinate{Col: 8, Row: 5}] = King{Color: types.Black} + board.position[types.Coordinate{Col: 2, Row: 5}] = Pawn{Color: types.White} + + move := types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 5}, + EndSquare: types.Coordinate{Col: 3, Row: 5}, + } + legal, _ := board.CheckAndPlay(move) + + assert.False(t, legal) + assert.Equal(t, boardBeforeMove, board) + + move = types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 5}, + EndSquare: types.Coordinate{Col: 1, Row: 5}, + } + legal, _ = board.CheckAndPlay(move) + + assert.False(t, legal) + assert.Equal(t, boardBeforeMove, board) + + move = types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 5}, + EndSquare: types.Coordinate{Col: 6, Row: 5}, + } + legal, _ = board.CheckAndPlay(move) + + assert.False(t, legal) + assert.Equal(t, boardBeforeMove, board) }) + + // t.Run("king of moving color is in check after move", func(t *testing.T) { + // good, _ := board.CheckMove(move) + // assert.False(t, good) + // }) } func Test_CheckMove_validPromotion(t *testing.T) { - var board Board = make(Board) + var board Board = newBoard() - board[types.Coordinate{Col: 1, Row: 7}] = Pawn{Color: types.White} - board[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White} + board.position[types.Coordinate{Col: 1, Row: 7}] = Pawn{Color: types.White} + board.position[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White} - board[types.Coordinate{Col: 8, Row: 7}] = King{Color: types.Black} + board.position[types.Coordinate{Col: 8, Row: 7}] = King{Color: types.Black} - shortName := types.Queen + shortName := types.QueenShortName move := types.Move{ StartSquare: types.Coordinate{Col: 1, Row: 7}, EndSquare: types.Coordinate{Col: 1, Row: 8}, PromotionToPiece: &shortName, } - - good, reason := board.CheckMove(move) + good, reason := board.CheckAndPlay(move) assert.Empty(t, reason) assert.True(t, good) + assert.Equal(t, Queen{Color: types.White}, board.getPieceAt(types.Coordinate{Row: 8, Col: 1})) +} + +func Test_CheckMove_HistoryWorks(t *testing.T) { + var board = newBoard() + + board.position[types.Coordinate{Col: 3, Row: 7}] = Pawn{Color: types.Black} + board.position[types.Coordinate{Col: 1, Row: 2}] = Pawn{Color: types.White} + board.position[types.Coordinate{Col: 1, Row: 5}] = King{Color: types.White} + board.position[types.Coordinate{Col: 8, Row: 5}] = King{Color: types.Black} + + firstMove := types.Move{ + StartSquare: types.Coordinate{Col: 1, Row: 2}, + EndSquare: types.Coordinate{Col: 1, Row: 3}, + } + secondMove := types.Move{ + StartSquare: types.Coordinate{Col: 3, Row: 7}, + EndSquare: types.Coordinate{Col: 3, Row: 5}, + } + thirdMove := types.Move{ + StartSquare: types.Coordinate{Col: 1, Row: 3}, + EndSquare: types.Coordinate{Col: 1, Row: 4}, + } + + good, _ := board.CheckAndPlay(firstMove) + assert.True(t, good) + + good, _ = board.CheckAndPlay(secondMove) + assert.True(t, good) + + good, _ = board.CheckAndPlay(thirdMove) + assert.True(t, good) + + expectedHistory := []types.Move{ + { + StartSquare: types.Coordinate{Col: 1, Row: 2}, + EndSquare: types.Coordinate{Col: 1, Row: 3}, + PieceMoved: 'p', + ColorMoved: "white", + }, + { + StartSquare: types.Coordinate{Col: 3, Row: 7}, + EndSquare: types.Coordinate{Col: 3, Row: 5}, + PieceMoved: 'p', + ColorMoved: "black", + }, + { + StartSquare: types.Coordinate{Col: 1, Row: 3}, + EndSquare: types.Coordinate{Col: 1, Row: 4}, + PieceMoved: 'p', + ColorMoved: "white", + }, + } + assert.Equal(t, expectedHistory, board.history) } diff --git a/chess/game.go b/chess/game.go index 67e3a91..5af9643 100644 --- a/chess/game.go +++ b/chess/game.go @@ -1,9 +1,9 @@ package chess import ( + "log" "mchess_server/api" "mchess_server/types" - "log" "time" "github.com/google/uuid" @@ -25,7 +25,7 @@ const ( func NewGame() *Game { var game Game = Game{ id: uuid.New(), - board: make(map[types.Coordinate]Piece), + board: newBoard(), } game.board.Init() @@ -75,7 +75,7 @@ func (game *Game) Handle() { gameState = CheckMove case CheckMove: - valid, reason := game.board.CheckMove(receivedMove) + valid, reason := game.board.CheckAndPlay(receivedMove) if valid { gameState = CheckPlayerChange diff --git a/chess/piece_interface.go b/chess/piece_interface.go index 2f8ed80..24fbacc 100644 --- a/chess/piece_interface.go +++ b/chess/piece_interface.go @@ -9,22 +9,41 @@ type Piece interface { GetColor() types.ChessColor } -func GetPieceForShortName(name types.PieceShortName) Piece { +func GetPieceForShortName(name types.PieceShortName, color types.ChessColor) Piece { var piece Piece switch name { case 'p': - piece = Pawn{} + piece = Pawn{Color: color} case 'q': - piece = Queen{} + piece = Queen{Color: color} case 'k': - piece = King{} + piece = King{Color: color} case 'b': - piece = Bishop{} + piece = Bishop{Color: color} case 'r': - piece = Rook{} + piece = Rook{Color: color} case 'n': - piece = Knight{} + piece = Knight{Color: color} } return piece } + +func GetShortNameForPiece(piece Piece) types.PieceShortName { + var name types.PieceShortName + switch piece.(type) { + case Pawn: + name = 'p' + case Queen: + name = 'q' + case King: + name = 'k' + case Bishop: + name = 'b' + case Rook: + name = 'r' + case Knight: + name = 'n' + } + return name +} diff --git a/types/move.go b/types/move.go index 6423efc..658930b 100644 --- a/types/move.go +++ b/types/move.go @@ -1,14 +1,13 @@ package types type Move struct { - StartSquare Coordinate `json:"startSquare"` - EndSquare Coordinate `json:"endSquare"` - PromotionToPiece *PieceShortName `json:"promotionToPiece,omitempty"` + StartSquare Coordinate `json:"startSquare"` + EndSquare Coordinate `json:"endSquare"` + PieceMoved PieceShortName + ColorMoved ChessColor + PromotionToPiece *PieceShortName `json:"promotionToPiece,omitempty"` } func (m Move) IsPromotionMove() bool { - if m.PromotionToPiece != nil { - return true - } - return false + return m.PromotionToPiece != nil } diff --git a/types/shortname.go b/types/shortname.go index 27d42f4..6ac727d 100644 --- a/types/shortname.go +++ b/types/shortname.go @@ -3,10 +3,10 @@ package types type PieceShortName rune const ( - Pawn PieceShortName = 'p' - Rook PieceShortName = 'r' - Knight PieceShortName = 'n' - Bishop PieceShortName = 'b' - Queen PieceShortName = 'q' - King PieceShortName = 'k' + PawnShortName PieceShortName = 'p' + RookShortName PieceShortName = 'r' + KnightShortName PieceShortName = 'n' + BishopShortName PieceShortName = 'b' + QueenShortName PieceShortName = 'q' + KingShortName PieceShortName = 'k' )