From 387125e828ba67a4276e258efc1ff21a63152306 Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 17 Jan 2024 17:38:06 +0100 Subject: [PATCH] Introduce check if the game has ended (checkmate/stalemate) --- api/move.go | 1 + chess/bishop.go | 4 +-- chess/board.go | 65 +++++++++++++++++++++++++++++++++- chess/board_test.go | 75 ++++++++++++++++++++++++++++++++++++++++ chess/game.go | 34 ++++++++++++++++-- chess/king.go | 4 +-- chess/knight.go | 4 +-- chess/pawn.go | 4 +-- chess/piece_interface.go | 2 +- chess/player.go | 18 ++++++++++ chess/queen.go | 4 +-- chess/rook.go | 4 +-- chess/rook_test.go | 4 +-- connection/type.go | 2 +- types/common.go | 5 +-- 15 files changed, 208 insertions(+), 22 deletions(-) diff --git a/api/move.go b/api/move.go index e42a743..794cb81 100644 --- a/api/move.go +++ b/api/move.go @@ -21,6 +21,7 @@ const ( MoveMessage MessageType = "move" InvalidMoveMessage MessageType = "invalidMove" ColorDetermined MessageType = "colorDetermined" + GameEnded MessageType = "gameEnded" ) func (m WebsocketMessage) IsValid() bool { diff --git a/chess/bishop.go b/chess/bishop.go index 30d9dde..e990402 100644 --- a/chess/bishop.go +++ b/chess/bishop.go @@ -9,14 +9,14 @@ type Bishop struct { } func (b Bishop) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { - return b.GetAllNonBlockedMoves(board, fromSquare) + return b.GetAllNonBlockedSquares(board, fromSquare) } func (b Bishop) GetColor() types.ChessColor { return b.Color } -func (b Bishop) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (b Bishop) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { return board.GetNonBlockedDiagonals(fromSquare) } diff --git a/chess/board.go b/chess/board.go index b596134..7a4b7ab 100644 --- a/chess/board.go +++ b/chess/board.go @@ -86,7 +86,7 @@ func (b *Board) CheckAndPlay(move types.Move) (bool, Violation) { } if !wasSpecialMove { - allMovesExceptBlocked := pieceAtStartSquare.GetAllNonBlockedMoves(tempBoard, move.StartSquare) + allMovesExceptBlocked := pieceAtStartSquare.GetAllNonBlockedSquares(tempBoard, move.StartSquare) legal := lo.Contains(allMovesExceptBlocked, move.EndSquare) if !legal { return false, InvalidMove @@ -204,3 +204,66 @@ func (b *Board) handleSpecialMove(move types.Move) (bool, error) { } return was, err } + +type GameEndedReason string + +const ( + NoReason GameEndedReason = "noReason" + WhiteIsCheckmated GameEndedReason = "whiteIsCheckmated" + BlackIsCheckmated GameEndedReason = "blackIsCheckmated" + StalemateReason GameEndedReason = "stalemate" +) + +func (r GameEndedReason) String() string { + return string(r) +} + +func (b *Board) HasGameEnded(lastMove types.Move) (bool, GameEndedReason) { + checkForColor := lastMove.ColorMoved.Opposite() + if checkmate := b.isColorCheckmated(checkForColor); checkmate { + switch checkForColor { + case types.White: + return true, WhiteIsCheckmated + case types.Black: + return true, BlackIsCheckmated + } + } + if b.isStalemate() { + return true, StalemateReason + } + + return false, NoReason +} + +func (b *Board) isColorCheckmated(color types.ChessColor) bool { + inCheck, _ := b.isKingOfMovingColorInCheck(color) + if !inCheck { + return false + } + + copyOfBoard := b.getCopyOfBoard() + + var movesToCheck []types.Move + + for startSquare, piece := range b.position { + if piece.GetColor() == color { + for _, endSquare := range piece.GetAllNonBlockedSquares(*b, startSquare) { + move := types.Move{StartSquare: startSquare, EndSquare: endSquare} + movesToCheck = append(movesToCheck, move) + } + } + } + + for _, move := range movesToCheck { + valid, _ := copyOfBoard.CheckAndPlay(move) + if valid { + return false + } + } + + return true +} + +func (b *Board) isStalemate() bool { + return false +} diff --git a/chess/board_test.go b/chess/board_test.go index 4c3ce04..8032008 100644 --- a/chess/board_test.go +++ b/chess/board_test.go @@ -374,3 +374,78 @@ func Test_Promotion_BlackKing(t *testing.T) { assert.False(t, good) }) } + +func Test_Board_HasGameEnded(t *testing.T) { + board := newBoard() + + t.Run("no checkmate yet", func(t *testing.T) { + board.position[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White} + board.position[types.Coordinate{Col: 7, Row: 6}] = Queen{Color: types.White} + board.position[types.Coordinate{Col: 7, Row: 7}] = Queen{Color: types.White} + + board.position[types.Coordinate{Col: 2, Row: 8}] = King{Color: types.Black} + + whiteMove := types.Move{ + StartSquare: types.Coordinate{Col: 7, Row: 6}, + EndSquare: types.Coordinate{Col: 8, Row: 6}, + ColorMoved: types.White, + } + valid, violation := board.CheckAndPlay(whiteMove) + assert.True(t, valid) + assert.Empty(t, violation) + + gameEnded, reason := board.HasGameEnded(whiteMove) + + assert.False(t, gameEnded) + assert.Equal(t, NoReason, reason) + }) + + t.Run("checkmate move is made", func(t *testing.T) { + blackMove := types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 8}, + EndSquare: types.Coordinate{Col: 1, Row: 8}, + ColorMoved: types.Black, + } + valid, violation := board.CheckAndPlay(blackMove) + assert.True(t, valid) + assert.Empty(t, violation) + + checkmateMove := types.Move{ + StartSquare: types.Coordinate{Col: 8, Row: 6}, + EndSquare: types.Coordinate{Col: 8, Row: 8}, + ColorMoved: types.White, + } + + valid, violation = board.CheckAndPlay(checkmateMove) + assert.True(t, valid) + assert.Empty(t, violation) + + gameEnded, reason := board.HasGameEnded(checkmateMove) + assert.True(t, gameEnded) + assert.Equal(t, BlackIsCheckmated, reason) + }) +} + +func Test_Board_HasGameEnded_RookBlocks(t *testing.T) { + board := newBoard() + + board.position[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White} + board.position[types.Coordinate{Col: 8, Row: 7}] = Queen{Color: types.White} + board.position[types.Coordinate{Col: 7, Row: 7}] = Queen{Color: types.White} + + board.position[types.Coordinate{Col: 1, Row: 8}] = King{Color: types.Black} + board.position[types.Coordinate{Col: 2, Row: 2}] = Rook{Color: types.Black} + + whiteMove := types.Move{ + StartSquare: types.Coordinate{Col: 8, Row: 7}, + EndSquare: types.Coordinate{Col: 8, Row: 8}, + ColorMoved: types.White, + } + valid, violation := board.CheckAndPlay(whiteMove) + assert.True(t, valid) + assert.Empty(t, violation) + + gameEnded, reason := board.HasGameEnded(whiteMove) + assert.False(t, gameEnded) + assert.Equal(t, NoReason, reason) +} diff --git a/chess/game.go b/chess/game.go index ea2c283..1580264 100644 --- a/chess/game.go +++ b/chess/game.go @@ -27,6 +27,7 @@ const ( PlayerToMove CheckMove CheckPlayerChange + GameEnded ) func NewGame() *Game { @@ -85,6 +86,7 @@ func (game *Game) Handle() { defer game.killGame() var receivedMove types.Move + var gameEndReason GameEndedReason var err error for { @@ -110,9 +112,7 @@ func (game *Game) Handle() { case CheckMove: valid, ruleViolation := game.board.CheckAndPlay(receivedMove) - if valid { - game.gameState = CheckPlayerChange - } else { + if !valid { invalidMoveMessage, err := api.GetInvalidMoveMessage(receivedMove, ruleViolation.String()) if err != nil { log.Println("Error marshalling 'colorDetermined' message for player 1", err) @@ -120,7 +120,17 @@ func (game *Game) Handle() { } game.currentTurnPlayer.writeMessage(invalidMoveMessage) game.gameState = PlayerToMove + continue } + + gameEnded, reason := game.board.HasGameEnded(receivedMove) + if gameEnded { + gameEndReason = reason + continue + } + + game.gameState = CheckPlayerChange + case CheckPlayerChange: if game.currentTurnPlayer.Uuid == game.players[0].Uuid { game.currentTurnPlayer = game.players[1] @@ -135,6 +145,10 @@ func (game *Game) Handle() { } game.gameState = PlayerToMove + + case GameEnded: + game.broadcastGameEnd(gameEndReason) + return } log.Println("GameState = ", game.gameState) } @@ -181,6 +195,20 @@ func (game Game) broadcastMove(move types.Move) error { return nil } +func (game Game) broadcastGameEnd(reason GameEndedReason) error { + err := game.GetPlayer1().SendGameEnded(reason) + if err != nil { + return err + } + + err = game.GetPlayer2().SendGameEnded(reason) + if err != nil { + return err + } + + return nil +} + func (game *Game) playerDisconnected(p *Player) { log.Println(string(p.color), " disconnected") playerStillInGame := lo.Filter(game.players, func(player *Player, _ int) bool { diff --git a/chess/king.go b/chess/king.go index c7ee6fe..96bd63c 100644 --- a/chess/king.go +++ b/chess/king.go @@ -9,14 +9,14 @@ type King struct { } func (k King) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { - return k.GetAllNonBlockedMoves(board, fromSquare) + return k.GetAllNonBlockedSquares(board, fromSquare) } func (k King) GetColor() types.ChessColor { return k.Color } -func (k King) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (k King) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { return board.GetNonBlockedKingMoves(fromSquare) } diff --git a/chess/knight.go b/chess/knight.go index 4f31174..9cb801c 100644 --- a/chess/knight.go +++ b/chess/knight.go @@ -9,14 +9,14 @@ type Knight struct { } func (n Knight) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { - return n.GetAllNonBlockedMoves(board, fromSquare) + return n.GetAllNonBlockedSquares(board, fromSquare) } func (n Knight) GetColor() types.ChessColor { return n.Color } -func (n Knight) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (n Knight) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { return board.GetNonBlockedKnightMoves(fromSquare) } diff --git a/chess/pawn.go b/chess/pawn.go index fa3d250..88e7451 100644 --- a/chess/pawn.go +++ b/chess/pawn.go @@ -12,7 +12,7 @@ type Pawn struct { func (p Pawn) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { attackingMoves := make([]types.Coordinate, 0, 2) - allMoves := p.GetAllNonBlockedMoves(board, fromSquare) + allMoves := p.GetAllNonBlockedSquares(board, fromSquare) for _, move := range allMoves { if move.Col != fromSquare.Col { attackingMoves = append(attackingMoves, move) @@ -21,7 +21,7 @@ func (p Pawn) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) [] return attackingMoves } -func (p Pawn) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (p Pawn) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { theoreticalSquares := p.getAllMoves(fromSquare) legalSquares := p.filterBlockedSquares(board, fromSquare, theoreticalSquares) diff --git a/chess/piece_interface.go b/chess/piece_interface.go index c690e90..5696317 100644 --- a/chess/piece_interface.go +++ b/chess/piece_interface.go @@ -6,7 +6,7 @@ import ( ) type Piece interface { - GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate + GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate GetColor() types.ChessColor AfterMoveAction(board *Board, fromSquare types.Coordinate) diff --git a/chess/player.go b/chess/player.go index 28d2b57..c9b593e 100644 --- a/chess/player.go +++ b/chess/player.go @@ -107,6 +107,24 @@ func (p *Player) SendMoveAndPosition(move types.Move, boardPosition string) erro return nil } +func (p *Player) SendGameEnded(reason GameEndedReason) error { + reasonToSend := reason.String() + messageToSend, err := json.Marshal(api.WebsocketMessage{ + Type: api.GameEnded, + Reason: &reasonToSend, + }) + if err != nil { + log.Println("Error while marshalling: ", err) + return err + } + err = p.writeMessage(messageToSend) + if err != nil { + log.Println("Error during message writing:", err) + return err + } + return nil +} + func (p *Player) writeMessage(msg []byte) error { return p.Conn.Write(msg) } diff --git a/chess/queen.go b/chess/queen.go index 7dcb556..9f1b0be 100644 --- a/chess/queen.go +++ b/chess/queen.go @@ -9,14 +9,14 @@ type Queen struct { } func (q Queen) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { - return q.GetAllNonBlockedMoves(board, fromSquare) + return q.GetAllNonBlockedSquares(board, fromSquare) } func (q Queen) GetColor() types.ChessColor { return q.Color } -func (q Queen) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (q Queen) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { squares := board.GetNonBlockedRowAndColumn(fromSquare) squares = append(squares, board.GetNonBlockedDiagonals(fromSquare)...) return squares diff --git a/chess/rook.go b/chess/rook.go index 7ba2361..de572bd 100644 --- a/chess/rook.go +++ b/chess/rook.go @@ -9,14 +9,14 @@ type Rook struct { } func (r Rook) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { - return r.GetAllNonBlockedMoves(board, fromSquare) + return r.GetAllNonBlockedSquares(board, fromSquare) } func (r Rook) GetColor() types.ChessColor { return r.Color } -func (r Rook) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (r Rook) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate { return board.GetNonBlockedRowAndColumn(fromSquare) } diff --git a/chess/rook_test.go b/chess/rook_test.go index 7189a91..66b7598 100644 --- a/chess/rook_test.go +++ b/chess/rook_test.go @@ -14,7 +14,7 @@ func Test_Rook_GetNonBlockedSquares(t *testing.T) { rook := Rook{Color: types.Black} board.position[types.Coordinate{Col: 5, Row: 5}] = rook - squares := rook.GetAllNonBlockedMoves(board, types.Coordinate{Col: 5, Row: 5}) + squares := rook.GetAllNonBlockedSquares(board, types.Coordinate{Col: 5, Row: 5}) assert.Len(t, squares, 14) }) @@ -28,7 +28,7 @@ func Test_Rook_GetNonBlockedSquares(t *testing.T) { board.position[types.Coordinate{Col: 5, Row: 6}] = Pawn{Color: types.White} board.position[types.Coordinate{Col: 6, Row: 5}] = Pawn{Color: types.Black} - squares := rook.GetAllNonBlockedMoves(board, rookCoordinate) + squares := rook.GetAllNonBlockedSquares(board, rookCoordinate) squaresOnLeft := lo.Filter(squares, func(square types.Coordinate, _ int) bool { return square.Row == rookCoordinate.Row && square.Col < rookCoordinate.Col diff --git a/connection/type.go b/connection/type.go index b4c6ebd..4b073ee 100644 --- a/connection/type.go +++ b/connection/type.go @@ -96,7 +96,7 @@ func (conn *Connection) Read() ([]byte, error) { msg, err := conn.buffer.Get() if err != nil { conn.ws = nil - return nil, err // Tell game-handler that connection was lost + return nil, err // TODO: Tell game-handler that connection was lost } return []byte(msg), err diff --git a/types/common.go b/types/common.go index f7c7f2c..a085bd0 100644 --- a/types/common.go +++ b/types/common.go @@ -3,8 +3,9 @@ package types type ChessColor string const ( - White ChessColor = "white" - Black ChessColor = "black" + NoColor ChessColor = "no_color" + White ChessColor = "white" + Black ChessColor = "black" ) func (c ChessColor) Opposite() ChessColor {