From 7f206b15fe70242acc6bb8f6d2d3eec088675183 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 20 Jun 2023 23:53:54 +0200 Subject: [PATCH] Pieces are now an interface and starting to enforce rules. --- api/move.go | 19 ++++--- chess/bishop.go | 18 ++++++ chess/board.go | 118 +++++++++++++++++++++++---------------- chess/game.go | 12 +++- chess/king.go | 22 ++++++++ chess/knight.go | 19 +++++++ chess/pawn.go | 83 +++++++++++++++++++++++++++ chess/piece_interface.go | 11 ++++ chess/player.go | 2 +- chess/queen.go | 18 ++++++ chess/rook.go | 20 +++++++ go.mod | 2 + go.sum | 4 ++ types/common.go | 46 +++++++++++++-- 14 files changed, 329 insertions(+), 65 deletions(-) create mode 100644 chess/bishop.go create mode 100644 chess/king.go create mode 100644 chess/knight.go create mode 100644 chess/pawn.go create mode 100644 chess/piece_interface.go create mode 100644 chess/queen.go create mode 100644 chess/rook.go diff --git a/api/move.go b/api/move.go index 566018e..dd66bca 100644 --- a/api/move.go +++ b/api/move.go @@ -6,19 +6,21 @@ import ( ) type WebsocketMessage struct { - Type MessageType `json:"messageType"` - Move *types.Move `json:"move,omitempty"` - Color *types.ChessColor `json:"color,omitempty"` + Type MessageType `json:"messageType"` + Move *types.Move `json:"move,omitempty"` + Color *types.ChessColor `json:"color,omitempty"` + Reason *string `json:"reason,omitempty"` } type MessageType string const ( - MoveMessage MessageType = "move" - ColorDetermined MessageType = "colorDetermined" + MoveMessage MessageType = "move" + InvalidMoveMessage MessageType = "invalidMove" + ColorDetermined MessageType = "colorDetermined" ) -func (m WebsocketMessage) IsValidMove() bool { +func (m WebsocketMessage) IsValidMoveMessage() bool { if m.Type != MoveMessage { return false } @@ -30,5 +32,8 @@ func (m WebsocketMessage) IsValidMove() bool { func GetColorDeterminedMessage(color types.ChessColor) ([]byte, error) { return json.Marshal(WebsocketMessage{Type: ColorDetermined, Color: &color}) - +} + +func GetInvalidMoveMessage(move types.Move, reason string) ([]byte, error) { + return json.Marshal(WebsocketMessage{Type: InvalidMoveMessage, Move: &move, Reason: &reason}) } diff --git a/chess/bishop.go b/chess/bishop.go new file mode 100644 index 0000000..6610a15 --- /dev/null +++ b/chess/bishop.go @@ -0,0 +1,18 @@ +package chess + +import "local/m/mchess_server/types" + +type Bishop struct { + Color types.ChessColor +} + +func (Bishop) AfterMoveAction() { +} + +func (b Bishop) GetColor() types.ChessColor { + return b.Color +} + +func (b Bishop) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { + return []types.Coordinate{} +} diff --git a/chess/board.go b/chess/board.go index 3a1c13a..5e073d7 100644 --- a/chess/board.go +++ b/chess/board.go @@ -1,8 +1,12 @@ package chess -import "local/m/mchess_server/types" +import ( + "local/m/mchess_server/types" -type Board map[types.Coordinate]types.Piece + "github.com/samber/lo" +) + +type Board map[types.Coordinate]Piece func (b Board) Init() { var coord types.Coordinate @@ -10,73 +14,71 @@ func (b Board) Init() { for i := 1; i <= 8; i++ { coord.Row = 2 coord.Col = i - b[coord] = types.Piece{Class: types.Pawn, Color: types.White} + b[coord] = Pawn{Color: types.White, HasMoved: false} coord.Row = 7 coord.Col = i - b[coord] = types.Piece{Class: types.Pawn, Color: types.Black} + b[coord] = Pawn{Color: types.Black, HasMoved: false} } - b[types.Coordinate{Row: 1, Col: 1}] = types.Piece{Class: types.Rook, Color: types.White} - b[types.Coordinate{Row: 1, Col: 2}] = types.Piece{Class: types.Knight, Color: types.White} - b[types.Coordinate{Row: 1, Col: 3}] = types.Piece{Class: types.Bishop, Color: types.White} - b[types.Coordinate{Row: 1, Col: 4}] = types.Piece{Class: types.Queen, Color: types.White} - b[types.Coordinate{Row: 1, Col: 5}] = types.Piece{Class: types.King, Color: types.White} - b[types.Coordinate{Row: 1, Col: 6}] = types.Piece{Class: types.Bishop, Color: types.White} - b[types.Coordinate{Row: 1, Col: 7}] = types.Piece{Class: types.Knight, Color: types.White} - b[types.Coordinate{Row: 1, Col: 8}] = types.Piece{Class: types.Rook, Color: types.White} + 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[types.Coordinate{Row: 8, Col: 1}] = types.Piece{Class: types.Rook, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 2}] = types.Piece{Class: types.Knight, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 3}] = types.Piece{Class: types.Bishop, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 4}] = types.Piece{Class: types.Queen, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 5}] = types.Piece{Class: types.King, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 6}] = types.Piece{Class: types.Bishop, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 7}] = types.Piece{Class: types.Knight, Color: types.Black} - b[types.Coordinate{Row: 8, Col: 8}] = types.Piece{Class: types.Rook, Color: types.Black} -} - -func (b Board) GetPieceAt(coord types.Coordinate) (types.Piece, bool) { - piece, found := b[coord] - - if !found { - piece = types.Piece{} - } - - return piece, found + 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} } func (b Board) CheckMove(move types.Move) (bool, string) { - pieceAtStartSquare, found := b.GetPieceAt(move.StartSquare) - if !found { + pieceAtStartSquare := b.getPieceAt(move.StartSquare) + if pieceAtStartSquare == nil { return false, "no piece at start square" } - movingColor := pieceAtStartSquare.Color + movingColor := pieceAtStartSquare.GetColor() - pieceAtEndSquare, found := b.GetPieceAt(move.EndSquare) - if found { - if pieceAtEndSquare.Color == pieceAtStartSquare.Color { + pieceAtEndSquare := b.getPieceAt(move.EndSquare) + if pieceAtEndSquare != nil { + if pieceAtEndSquare.GetColor() == pieceAtStartSquare.GetColor() { return false, "same-coloured piece at end square" } } // 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. + legal := lo.Contains(pieceAtStartSquare.GetAllLegalAndIllegalMoves(b, move.StartSquare), move.EndSquare) + if !legal { + return false, "not a legal square" + } + //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. - oppKingCoordinate := b.getSquareOfPiece(types.Piece{ - Class: types.King, - Color: movingColor}) - if oppKingCoordinate == nil { - return false, string(movingColor) + " king not found" - } - - b.isSquareAttacked(*oppKingCoordinate, movingColor.Opposite()) + ownKingCoordinate := b.getSquareOfPiece(King{Color: movingColor}) + if ownKingCoordinate == nil { + return false, string(movingColor) + " king not found" + } + + kingIsAttacked := b.isSquareAttacked(*ownKingCoordinate, movingColor.Opposite()) + if kingIsAttacked { + return false, "king is attacked after move" + } //Check for checkmate //Is every square that the king can move to attacked? And can no other //piece block? -> checkmate + //only check if paths of attacking pieces can be blocked + //Maybe for checking checkmate, we have to check the 'path' in which the //checkmate is given @@ -84,10 +86,15 @@ func (b Board) CheckMove(move types.Move) (bool, string) { // in this scenaria the path are all the squares between queen and king. // If a piece can be moved into the path, it is no checkmate + //We play the move + delete(b, move.StartSquare) + b[move.EndSquare] = pieceAtStartSquare + + pieceAtStartSquare.AfterMoveAction() return true, "" } -func (b Board) getSquareOfPiece(piece types.Piece) *types.Coordinate { +func (b Board) getSquareOfPiece(piece Piece) *types.Coordinate { for k, v := range b { if v == piece { return &k @@ -97,8 +104,21 @@ func (b Board) getSquareOfPiece(piece types.Piece) *types.Coordinate { } func (b Board) isSquareAttacked(square types.Coordinate, byColor types.ChessColor) bool { - attacked := false - - //get every legal move of color to check if this square is attacked - return attacked + var attackedSquares []types.Coordinate + + for square, piece := range b { + attackedSquares = append(attackedSquares, piece.GetAllLegalAndIllegalMoves(b, square)...) + } + + return lo.Contains(attackedSquares, square) +} + +func (b Board) getPieceAt(coord types.Coordinate) Piece { + piece, found := b[coord] + + if !found { + return nil + } + + return piece } diff --git a/chess/game.go b/chess/game.go index e9a106d..00c99df 100644 --- a/chess/game.go +++ b/chess/game.go @@ -25,7 +25,7 @@ const ( func NewGame() *Game { var game Game = Game{ id: uuid.New(), - board: make(map[types.Coordinate]types.Piece), + board: make(map[types.Coordinate]Piece), } game.board.Init() @@ -76,10 +76,18 @@ func (game *Game) Handle() { case CheckMove: valid, reason := game.board.CheckMove(receivedMove) - log.Println(reason) if valid { gameState = CheckPlayerChange + } else { + log.Println("invalid move because " + reason) + invalidMoveMessage, err := api.GetInvalidMoveMessage(receivedMove, reason) + if err != nil { + log.Println("Error marshalling 'colorDetermined' message for player 1", err) + return + } + game.currentTurnPlayer.writeMessage(invalidMoveMessage) + gameState = PlayerToMove } case CheckPlayerChange: if game.currentTurnPlayer.Uuid == game.players[0].Uuid { diff --git a/chess/king.go b/chess/king.go new file mode 100644 index 0000000..b9daaec --- /dev/null +++ b/chess/king.go @@ -0,0 +1,22 @@ +package chess + +import "local/m/mchess_server/types" + +type King struct { + Color types.ChessColor + HasMoved bool +} + +// AfterMoveAction implements Piece. +func (k King) AfterMoveAction() { + k.HasMoved = true +} + +// GetColor implements Piece. +func (k King) GetColor() types.ChessColor { + return k.Color +} + +func (k King) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { + return []types.Coordinate{} +} diff --git a/chess/knight.go b/chess/knight.go new file mode 100644 index 0000000..fcbd4d2 --- /dev/null +++ b/chess/knight.go @@ -0,0 +1,19 @@ +package chess + +import "local/m/mchess_server/types" + +type Knight struct { + Color types.ChessColor +} + +// AfterMoveAction implements Piece. +func (Knight) AfterMoveAction() { +} + +func (k Knight) GetColor() types.ChessColor { + return k.Color +} + +func (k Knight) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { + return []types.Coordinate{} +} diff --git a/chess/pawn.go b/chess/pawn.go new file mode 100644 index 0000000..e85b3c5 --- /dev/null +++ b/chess/pawn.go @@ -0,0 +1,83 @@ +package chess + +import ( + "local/m/mchess_server/types" + + "github.com/samber/lo" +) + +type Pawn struct { + Color types.ChessColor + HasMoved bool +} + +func (p Pawn) AfterMoveAction() { + p.HasMoved = true +} + +func (p Pawn) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { + theoreticalSquares := p.getAllMoves(fromSquare) + legalSquares := p.filterBlockedSquares(board, fromSquare, theoreticalSquares) + + return legalSquares +} + +func (p Pawn) GetColor() types.ChessColor { + return p.Color +} + +func (p Pawn) getAllMoves(fromSquare types.Coordinate) []types.Coordinate { + theoreticalMoves := make([]types.Coordinate, 0, 4) + + switch p.Color { + case types.Black: + if fromSquare.Down(1) != nil { + theoreticalMoves = append(theoreticalMoves, *fromSquare.Down(1)) + } + if !p.HasMoved && fromSquare.Down(2) != nil { + theoreticalMoves = append(theoreticalMoves, *fromSquare.Down(2)) + } + + if lowerRight := fromSquare.Down(1).Right(1); lowerRight != nil { + theoreticalMoves = append(theoreticalMoves, *lowerRight) + } + if lowerLeft := fromSquare.Down(1).Left(1); lowerLeft != nil { + theoreticalMoves = append(theoreticalMoves, *lowerLeft) + } + + case types.White: + if fromSquare.Up(1) != nil { + theoreticalMoves = append(theoreticalMoves, *fromSquare.Up(1)) + } + if !p.HasMoved && fromSquare.Up(2) != nil { + theoreticalMoves = append(theoreticalMoves, *fromSquare.Up(2)) + } + + if upperRight := fromSquare.Up(1).Right(1); upperRight != nil { + theoreticalMoves = append(theoreticalMoves, *upperRight) + } + if upperLeft := fromSquare.Up(1).Left(1); upperLeft != nil { + theoreticalMoves = append(theoreticalMoves, *upperLeft) + } + } + + return theoreticalMoves +} + +func (p Pawn) filterBlockedSquares(board Board, fromSquare types.Coordinate, squaresToBeFiltered []types.Coordinate) []types.Coordinate { + var nonBlockedSquares []types.Coordinate + //order of movesToBeFiltered is important here + for _, square := range squaresToBeFiltered { + pieceAtSquare := board.getPieceAt(square) + if square.Col == fromSquare.Col { // squares ahead + if pieceAtSquare == nil { + nonBlockedSquares = append(nonBlockedSquares, square) + } + } else { //squares that pawn attacks + if pieceAtSquare != nil && pieceAtSquare.GetColor() != p.Color { + nonBlockedSquares = append(nonBlockedSquares, square) + } + } + } + return lo.Intersect(nonBlockedSquares, squaresToBeFiltered) +} diff --git a/chess/piece_interface.go b/chess/piece_interface.go new file mode 100644 index 0000000..7dd3a29 --- /dev/null +++ b/chess/piece_interface.go @@ -0,0 +1,11 @@ +package chess + +import ( + "local/m/mchess_server/types" +) + +type Piece interface { + GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate + GetColor() types.ChessColor + AfterMoveAction() +} diff --git a/chess/player.go b/chess/player.go index 220a8f3..9343815 100644 --- a/chess/player.go +++ b/chess/player.go @@ -72,7 +72,7 @@ func (p *Player) ReadMove() (types.Move, error) { return types.Move{}, err } - if !msg.IsValidMove() { + if !msg.IsValidMoveMessage() { return types.Move{}, errors.New("not a valid move") } diff --git a/chess/queen.go b/chess/queen.go new file mode 100644 index 0000000..b3d7786 --- /dev/null +++ b/chess/queen.go @@ -0,0 +1,18 @@ +package chess + +import "local/m/mchess_server/types" + +type Queen struct { + Color types.ChessColor +} + +func (Queen) AfterMoveAction() { +} + +func (q Queen) GetColor() types.ChessColor { + return q.Color +} + +func (q Queen) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { + return []types.Coordinate{} +} diff --git a/chess/rook.go b/chess/rook.go new file mode 100644 index 0000000..2d170c3 --- /dev/null +++ b/chess/rook.go @@ -0,0 +1,20 @@ +package chess + +import "local/m/mchess_server/types" + +type Rook struct { + Color types.ChessColor + HasMoved bool +} + +func (Rook) AfterMoveAction() { +} + +// GetColor implements Piece. +func (r Rook) GetColor() types.ChessColor { + return r.Color +} + +func (r Rook) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { + return []types.Coordinate{} +} diff --git a/go.mod b/go.mod index 2795063..9d716e9 100644 --- a/go.mod +++ b/go.mod @@ -25,10 +25,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/samber/lo v1.38.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.9 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.5.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index ea00b05..4539c5b 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -92,6 +94,8 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= diff --git a/types/common.go b/types/common.go index a38b767..cc4c803 100644 --- a/types/common.go +++ b/types/common.go @@ -1,10 +1,50 @@ package types +// coordinates starting at 1:1 and end at 8:8 type Coordinate struct { Col int `json:"col"` Row int `json:"row"` } +const ( + RangeLastValid = 8 + RangeFirstValid = 1 + + RangeUpperInvalid = 9 + RangeLowerInvalid = 0 +) + +func (c Coordinate) Up(number int) *Coordinate { + check := c.Row + number + if check <= RangeLastValid { + return &Coordinate{Row: check, Col: c.Col} + } + return nil +} +func (c Coordinate) Down(number int) *Coordinate { + check := c.Row - number + if check >= RangeFirstValid { + return &Coordinate{Row: check, Col: c.Col} + } + return nil +} + +// Right and left is seen from a board where row 1 is on the bottom +func (c Coordinate) Right(number int) *Coordinate { + check := c.Col + number + if check >= RangeFirstValid { + return &Coordinate{Row: c.Row, Col: check} + } + return nil +} +func (c Coordinate) Left(number int) *Coordinate { + check := c.Col - number + if check >= RangeFirstValid { + return &Coordinate{Row: c.Row, Col: check} + } + return nil +} + type Move struct { StartSquare Coordinate `json:"startSquare"` EndSquare Coordinate `json:"endSquare"` @@ -35,9 +75,3 @@ func (c ChessColor) Opposite() ChessColor { return White } } - -type Piece struct { - Class PieceClass - Color ChessColor - HasMoved bool //we need this for pawns (first move is special) and rooks+king (for castling) -}