diff --git a/api/move.go b/api/move.go index dd66bca..14db174 100644 --- a/api/move.go +++ b/api/move.go @@ -2,7 +2,7 @@ package api import ( "encoding/json" - "local/m/mchess_server/types" + "mchess_server/types" ) type WebsocketMessage struct { diff --git a/chess/bishop.go b/chess/bishop.go index 6610a15..6fa5bbb 100644 --- a/chess/bishop.go +++ b/chess/bishop.go @@ -1,18 +1,15 @@ package chess -import "local/m/mchess_server/types" +import "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 { +func (b Bishop) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { return []types.Coordinate{} } diff --git a/chess/board.go b/chess/board.go index 5e073d7..3e2b2d3 100644 --- a/chess/board.go +++ b/chess/board.go @@ -1,7 +1,7 @@ package chess import ( - "local/m/mchess_server/types" + "mchess_server/types" "github.com/samber/lo" ) @@ -14,11 +14,11 @@ func (b Board) Init() { for i := 1; i <= 8; i++ { coord.Row = 2 coord.Col = i - b[coord] = Pawn{Color: types.White, HasMoved: false} + b[coord] = Pawn{Color: types.White} coord.Row = 7 coord.Col = i - b[coord] = Pawn{Color: types.Black, HasMoved: false} + b[coord] = Pawn{Color: types.Black} } b[types.Coordinate{Row: 1, Col: 1}] = Rook{Color: types.White} @@ -41,39 +41,58 @@ func (b Board) Init() { } func (b Board) CheckMove(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) if pieceAtStartSquare == nil { return false, "no piece at start square" } movingColor := pieceAtStartSquare.GetColor() + //Check end square of move 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. + var wasPromotionMove bool + // var piece types.PieceShortName + switch pieceAtStartSquare.(type) { + case Pawn: + wasPromotionMove, _ = tempBoard.handlePossiblePromotion(move, movingColor) + } - legal := lo.Contains(pieceAtStartSquare.GetAllLegalAndIllegalMoves(b, move.StartSquare), move.EndSquare) - if !legal { - return false, "not a legal square" + if !wasPromotionMove { + // 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) + legal := lo.Contains(allMovesExceptBlocked, move.EndSquare) + if !legal { + return false, "not a legal square" + } + + //We play the move on the temporary board + delete(tempBoard, move.StartSquare) + tempBoard[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 := b.getSquareOfPiece(King{Color: movingColor}) + ownKingCoordinate := tempBoard.getSquareOfPiece(King{Color: movingColor}) if ownKingCoordinate == nil { return false, string(movingColor) + " king not found" } - kingIsAttacked := b.isSquareAttacked(*ownKingCoordinate, movingColor.Opposite()) + kingIsAttacked := tempBoard.isSquareAttacked(*ownKingCoordinate, movingColor.Opposite()) if kingIsAttacked { return false, "king is attacked after move" } - //Check for checkmate + //Check for checkmat&e //Is every square that the king can move to attacked? And can no other //piece block? -> checkmate @@ -86,11 +105,9 @@ 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 + //We play the move on the real board + b = tempBoard - pieceAtStartSquare.AfterMoveAction() return true, "" } @@ -107,7 +124,7 @@ func (b Board) isSquareAttacked(square types.Coordinate, byColor types.ChessColo var attackedSquares []types.Coordinate for square, piece := range b { - attackedSquares = append(attackedSquares, piece.GetAllLegalAndIllegalMoves(b, square)...) + attackedSquares = append(attackedSquares, piece.GetAllMovesButBlocked(b, square)...) } return lo.Contains(attackedSquares, square) @@ -115,10 +132,51 @@ func (b Board) isSquareAttacked(square types.Coordinate, byColor types.ChessColo func (b Board) getPieceAt(coord types.Coordinate) Piece { piece, found := b[coord] - if !found { return nil } return piece } + +func (b Board) handlePossiblePromotion(move types.Move, color types.ChessColor) (bool, types.PieceShortName) { + var isPromotionMove bool + var promotionToPiece types.PieceShortName + + messageContainsPromotion := move.IsPromotionMove() + + if messageContainsPromotion { + promotionToPiece = *move.PromotionToPiece + } + + switch color { + case types.White: + if move.StartSquare.Row == types.RangeLastValid-1 && + move.EndSquare.Row == types.RangeLastValid { + isPromotionMove = true + } + + case types.Black: + if move.StartSquare.Row == types.RangeFirstValid+1 && + move.EndSquare.Row == types.RangeFirstValid { + isPromotionMove = true + } + } + + if isPromotionMove { + delete(b, move.StartSquare) + b[move.EndSquare] = GetPieceForShortName(promotionToPiece) + } + + return isPromotionMove, promotionToPiece +} + +func (b Board) getCopyOfBoard() Board { + board := make(map[types.Coordinate]Piece) + + for coord, piece := range b { + board[coord] = piece + } + + return board +} diff --git a/chess/board_test.go b/chess/board_test.go new file mode 100644 index 0000000..dfd1f68 --- /dev/null +++ b/chess/board_test.go @@ -0,0 +1,71 @@ +package chess + +import ( + "mchess_server/types" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_CheckMove_validPawnMove(t *testing.T) { + var board = make(Board) + + 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} + + move := types.Move{ + StartSquare: types.Coordinate{Col: 1, Row: 1}, + EndSquare: types.Coordinate{Col: 1, Row: 2}, + } + + good, _ := board.CheckMove(move) + assert.True(t, good) +} + +func Test_CheckMove_invalidPawnMoves(t *testing.T) { + var board = make(Board) + + 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} + + move := types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 5}, + EndSquare: types.Coordinate{Col: 2, Row: 6}, + } + + 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) + 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) + }) +} + +func Test_CheckMove_validPromotion(t *testing.T) { + var board Board = make(Board) + + board[types.Coordinate{Col: 1, Row: 7}] = Pawn{Color: types.White} + board[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White} + + board[types.Coordinate{Col: 8, Row: 7}] = King{Color: types.Black} + + shortName := types.Queen + move := types.Move{ + StartSquare: types.Coordinate{Col: 1, Row: 7}, + EndSquare: types.Coordinate{Col: 1, Row: 8}, + PromotionToPiece: &shortName, + } + + good, reason := board.CheckMove(move) + + assert.Empty(t, reason) + assert.True(t, good) +} diff --git a/chess/game.go b/chess/game.go index 00c99df..67e3a91 100644 --- a/chess/game.go +++ b/chess/game.go @@ -1,8 +1,8 @@ package chess import ( - "local/m/mchess_server/api" - "local/m/mchess_server/types" + "mchess_server/api" + "mchess_server/types" "log" "time" diff --git a/chess/king.go b/chess/king.go index b9daaec..e4f3c82 100644 --- a/chess/king.go +++ b/chess/king.go @@ -1,22 +1,15 @@ package chess -import "local/m/mchess_server/types" +import "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 { +func (k King) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { return []types.Coordinate{} } diff --git a/chess/knight.go b/chess/knight.go index fcbd4d2..f3e53c2 100644 --- a/chess/knight.go +++ b/chess/knight.go @@ -1,19 +1,19 @@ package chess -import "local/m/mchess_server/types" +import "mchess_server/types" type Knight struct { Color types.ChessColor } // AfterMoveAction implements Piece. -func (Knight) AfterMoveAction() { +func (k Knight) AfterMoveAction() { } func (k Knight) GetColor() types.ChessColor { return k.Color } -func (k Knight) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (k Knight) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { return []types.Coordinate{} } diff --git a/chess/pawn.go b/chess/pawn.go index e85b3c5..c50290d 100644 --- a/chess/pawn.go +++ b/chess/pawn.go @@ -1,21 +1,16 @@ package chess import ( - "local/m/mchess_server/types" + "mchess_server/types" "github.com/samber/lo" ) type Pawn struct { - Color types.ChessColor - HasMoved bool + Color types.ChessColor } -func (p Pawn) AfterMoveAction() { - p.HasMoved = true -} - -func (p Pawn) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (p Pawn) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { theoreticalSquares := p.getAllMoves(fromSquare) legalSquares := p.filterBlockedSquares(board, fromSquare, theoreticalSquares) @@ -31,34 +26,38 @@ func (p Pawn) getAllMoves(fromSquare types.Coordinate) []types.Coordinate { switch p.Color { case types.Black: - if fromSquare.Down(1) != nil { + firstMove := fromSquare.Row == types.RangeLastValid-1 + + if fromSquare.Down(1) != nil { theoreticalMoves = append(theoreticalMoves, *fromSquare.Down(1)) } - if !p.HasMoved && fromSquare.Down(2) != nil { + if firstMove && 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) - } + 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: + firstMove := fromSquare.Row == types.RangeFirstValid+1 + if fromSquare.Up(1) != nil { theoreticalMoves = append(theoreticalMoves, *fromSquare.Up(1)) } - if !p.HasMoved && fromSquare.Up(2) != nil { + if firstMove && 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) - } + 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 diff --git a/chess/piece_interface.go b/chess/piece_interface.go index 7dd3a29..2f8ed80 100644 --- a/chess/piece_interface.go +++ b/chess/piece_interface.go @@ -1,11 +1,30 @@ package chess import ( - "local/m/mchess_server/types" + "mchess_server/types" ) type Piece interface { - GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate + GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate GetColor() types.ChessColor - AfterMoveAction() +} + +func GetPieceForShortName(name types.PieceShortName) Piece { + var piece Piece + + switch name { + case 'p': + piece = Pawn{} + case 'q': + piece = Queen{} + case 'k': + piece = King{} + case 'b': + piece = Bishop{} + case 'r': + piece = Rook{} + case 'n': + piece = Knight{} + } + return piece } diff --git a/chess/player.go b/chess/player.go index 9343815..61fa2cf 100644 --- a/chess/player.go +++ b/chess/player.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "errors" - "local/m/mchess_server/api" - "local/m/mchess_server/types" + "mchess_server/api" + "mchess_server/types" "log" "time" diff --git a/chess/queen.go b/chess/queen.go index b3d7786..c47ebd7 100644 --- a/chess/queen.go +++ b/chess/queen.go @@ -1,18 +1,15 @@ package chess -import "local/m/mchess_server/types" +import "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 { +func (q Queen) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { return []types.Coordinate{} } diff --git a/chess/rook.go b/chess/rook.go index 2d170c3..5d65601 100644 --- a/chess/rook.go +++ b/chess/rook.go @@ -1,13 +1,12 @@ package chess -import "local/m/mchess_server/types" +import "mchess_server/types" type Rook struct { Color types.ChessColor - HasMoved bool } -func (Rook) AfterMoveAction() { +func (r Rook) AfterMoveAction() { } // GetColor implements Piece. @@ -15,6 +14,6 @@ func (r Rook) GetColor() types.ChessColor { return r.Color } -func (r Rook) GetAllLegalAndIllegalMoves(board Board, fromSquare types.Coordinate) []types.Coordinate { +func (r Rook) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { return []types.Coordinate{} } diff --git a/go.mod b/go.mod index 9d716e9..ba7720c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module local/m/mchess_server +module mchess_server go 1.20 @@ -12,6 +12,7 @@ require ( require ( github.com/bytedance/sonic v1.8.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -25,7 +26,10 @@ 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/pmezard/go-difflib v1.0.0 // indirect github.com/samber/lo v1.38.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.4 // 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 diff --git a/go.sum b/go.sum index 4539c5b..181c285 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,7 @@ 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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -83,6 +84,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= diff --git a/lobby_registry/lobby.go b/lobby_registry/lobby.go index 810b34b..3edb0a8 100644 --- a/lobby_registry/lobby.go +++ b/lobby_registry/lobby.go @@ -1,7 +1,7 @@ package lobby_registry import ( - "local/m/mchess_server/chess" + "mchess_server/chess" "github.com/google/uuid" ) @@ -22,10 +22,10 @@ func NewEmptyLobbyWithUUID(uuid uuid.UUID) *Lobby { } } -func (w *Lobby) AddPlayerAndStartGameIfFull(player *chess.Player) { - w.Game.AddPlayersToGame(player) - if w.IsFull() { - go w.Game.Handle() +func (l *Lobby) AddPlayerAndStartGameIfFull(player *chess.Player) { + l.Game.AddPlayersToGame(player) + if l.IsFull() { + go l.Game.Handle() } } diff --git a/main.go b/main.go index 1b6cb7d..4f91fa8 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "fmt" - "local/m/mchess_server/api" - "local/m/mchess_server/chess" - lobbies "local/m/mchess_server/lobby_registry" - "local/m/mchess_server/usher" + "mchess_server/api" + "mchess_server/chess" + lobbies "mchess_server/lobby_registry" + "mchess_server/usher" "log" "net/http" "os" diff --git a/types/common.go b/types/common.go index cc4c803..674d4e6 100644 --- a/types/common.go +++ b/types/common.go @@ -45,22 +45,6 @@ func (c Coordinate) Left(number int) *Coordinate { return nil } -type Move struct { - StartSquare Coordinate `json:"startSquare"` - EndSquare Coordinate `json:"endSquare"` -} - -type PieceClass string - -const ( - Pawn PieceClass = "pawn" - Rook PieceClass = "rook" - Knight PieceClass = "knight" - Bishop PieceClass = "bishop" - Queen PieceClass = "queen" - King PieceClass = "king" -) - type ChessColor string const ( diff --git a/types/move.go b/types/move.go new file mode 100644 index 0000000..6423efc --- /dev/null +++ b/types/move.go @@ -0,0 +1,14 @@ +package types + +type Move struct { + StartSquare Coordinate `json:"startSquare"` + EndSquare Coordinate `json:"endSquare"` + PromotionToPiece *PieceShortName `json:"promotionToPiece,omitempty"` +} + +func (m Move) IsPromotionMove() bool { + if m.PromotionToPiece != nil { + return true + } + return false +} diff --git a/types/shortname.go b/types/shortname.go new file mode 100644 index 0000000..27d42f4 --- /dev/null +++ b/types/shortname.go @@ -0,0 +1,12 @@ +package types + +type PieceShortName rune + +const ( + Pawn PieceShortName = 'p' + Rook PieceShortName = 'r' + Knight PieceShortName = 'n' + Bishop PieceShortName = 'b' + Queen PieceShortName = 'q' + King PieceShortName = 'k' +) diff --git a/usher/usher.go b/usher/usher.go index 57e81a5..6a90b64 100644 --- a/usher/usher.go +++ b/usher/usher.go @@ -1,8 +1,8 @@ package usher import ( - "local/m/mchess_server/chess" - lobbies "local/m/mchess_server/lobby_registry" + "mchess_server/chess" + lobbies "mchess_server/lobby_registry" ) type Usher struct {