From 3d4fee26310c3e16ad2ae5128e5c2f9bb449b5ea Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 26 Jun 2023 23:58:40 +0200 Subject: [PATCH] Refactor a itsy bitsy tiny bit and add rook moves. --- chess/board.go | 90 +-------------------------- chess/board_test.go | 18 ++++-- chess/free_squares.go | 41 +++++++++++++ chess/pawn.go | 97 ++++++++++++++++++++++++++++++ chess/rook.go | 5 +- chess/rook_test.go | 72 ++++++++++++++++++++++ types/common.go | 43 ------------- types/coordinate.go | 127 +++++++++++++++++++++++++++++++++++++++ types/coordinate_test.go | 25 ++++++++ 9 files changed, 381 insertions(+), 137 deletions(-) create mode 100644 chess/free_squares.go create mode 100644 chess/rook_test.go create mode 100644 types/coordinate.go create mode 100644 types/coordinate_test.go diff --git a/chess/board.go b/chess/board.go index 105b377..3d2a375 100644 --- a/chess/board.go +++ b/chess/board.go @@ -183,97 +183,13 @@ func (b *Board) handleSpecialMove(move types.Move) bool { switch pieceAtStartSquare.(type) { case Pawn: - was = b.handlePossiblePromotion(move) + pawn := pieceAtStartSquare.(Pawn) + was = pawn.HandlePossiblePromotion(b, move) if !was { - was = b.handleEnPassant(move, b.getLastMove()) + was = pawn.HandleEnPassant(b, 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 move.ColorMoved { - 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.position, move.StartSquare) - b.position[move.EndSquare] = GetPieceForShortName(promotionToPiece, move.ColorMoved) - } - - return isPromotionMove -} - -func (b *Board) handleEnPassant(move, lastMove types.Move) bool { - var wasEnPassant bool - - if lastMove.PieceMoved != types.PawnShortName { - return false - } - - 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 e9d7238..b6012aa 100644 --- a/chess/board_test.go +++ b/chess/board_test.go @@ -117,10 +117,20 @@ func Test_CheckMove_invalidPawnMoves(t *testing.T) { 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) - // }) + t.Run("king of moving color is in check after move", func(t *testing.T) { + var board = newBoard() + + board.position[types.Coordinate{Col: 1, Row: 6}] = King{Color: types.White} + board.position[types.Coordinate{Col: 2, Row: 6}] = Pawn{Color: types.White} + board.position[types.Coordinate{Col: 8, Row: 6}] = King{Color: types.Black} + + move := types.Move{ + StartSquare: types.Coordinate{Col: 2, Row: 6}, + EndSquare: types.Coordinate{Col: 2, Row: 7}, + } + good, _ := board.CheckAndPlay(move) + assert.False(t, good) + }) } func Test_CheckMove_validPromotion(t *testing.T) { diff --git a/chess/free_squares.go b/chess/free_squares.go new file mode 100644 index 0000000..a124e0c --- /dev/null +++ b/chess/free_squares.go @@ -0,0 +1,41 @@ +package chess + +import "mchess_server/types" + +func (b *Board) GetNonBlockedRowAndColumn(fromSquare types.Coordinate) []types.Coordinate { + squaresLeft := fromSquare.GetAllSquaresLeft() + squaresRight := fromSquare.GetAllSquaresRight() + squaresAbove := fromSquare.GetAllSquaresAbove() + squaresBelow := fromSquare.GetAllSquaresBelow() + nonBlocked := []types.Coordinate{} + + nonBlocked = append(nonBlocked, b.getNonBlocked(squaresLeft, fromSquare)...) + nonBlocked = append(nonBlocked, b.getNonBlocked(squaresRight, fromSquare)...) + nonBlocked = append(nonBlocked, b.getNonBlocked(squaresAbove, fromSquare)...) + nonBlocked = append(nonBlocked, b.getNonBlocked(squaresBelow, fromSquare)...) + + return nonBlocked +} + +func (b *Board) getNonBlocked( + squaresToCheck []types.Coordinate, + sourceSquare types.Coordinate, +) []types.Coordinate { + pieceOnSourceSquare := b.getPieceAt(sourceSquare) + nonBlocked := []types.Coordinate{} + + for _, square := range squaresToCheck { + piece := b.getPieceAt(square) + if piece != nil { + if piece.GetColor() == pieceOnSourceSquare.GetColor() { + break + } + // if there is an opposite colored piece we append it but + // stop appending the squares behind it + nonBlocked = append(nonBlocked, square) + break + } + nonBlocked = append(nonBlocked, square) + } + return nonBlocked +} diff --git a/chess/pawn.go b/chess/pawn.go index c50290d..bf09b8c 100644 --- a/chess/pawn.go +++ b/chess/pawn.go @@ -21,6 +21,102 @@ func (p Pawn) GetColor() types.ChessColor { return p.Color } +func (p *Pawn) HandlePossiblePromotion(b *Board ,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 move.ColorMoved { + 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.position, move.StartSquare) + b.position[move.EndSquare] = GetPieceForShortName(promotionToPiece, move.ColorMoved) + } + + return isPromotionMove +} + +func (p *Pawn) HandleEnPassant(b *Board, move, lastMove types.Move) bool { + var wasEnPassant bool + + if lastMove.PieceMoved != types.PawnShortName { + return false + } + + switch move.ColorMoved { + case types.White: + if lastMove.StartSquare.Row != 7 || lastMove.EndSquare.Row != 5 { + wasEnPassant = false + break + } + if move.StartSquare.Row != 5 { + wasEnPassant = false + break + } + if move.EndSquare.Row != 6 { + wasEnPassant = false + break + } + if move.StartSquare.Col == lastMove.EndSquare.Col+1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + break + } + if move.StartSquare.Col == lastMove.EndSquare.Col-1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + break + } + case types.Black: + if lastMove.StartSquare.Row != 2 || lastMove.EndSquare.Row != 4 { + wasEnPassant = false + break + } + if move.StartSquare.Row != 4 { + wasEnPassant = false + break + } + if move.EndSquare.Row != 3 { + wasEnPassant = false + break + } + if move.StartSquare.Col == lastMove.EndSquare.Col+1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + break + } + if move.StartSquare.Col == lastMove.EndSquare.Col-1 && + move.EndSquare.Col == lastMove.EndSquare.Col { + wasEnPassant = true + break + } + } + + if wasEnPassant { //play the move + delete(b.position, lastMove.EndSquare) + b.position[move.EndSquare] = GetPieceForShortName(move.PieceMoved, move.ColorMoved) + } + + return wasEnPassant +} func (p Pawn) getAllMoves(fromSquare types.Coordinate) []types.Coordinate { theoreticalMoves := make([]types.Coordinate, 0, 4) @@ -80,3 +176,4 @@ func (p Pawn) filterBlockedSquares(board Board, fromSquare types.Coordinate, squ } return lo.Intersect(nonBlockedSquares, squaresToBeFiltered) } + diff --git a/chess/rook.go b/chess/rook.go index 5d65601..e9533ee 100644 --- a/chess/rook.go +++ b/chess/rook.go @@ -3,17 +3,16 @@ package chess import "mchess_server/types" type Rook struct { - Color types.ChessColor + Color types.ChessColor } func (r Rook) AfterMoveAction() { } -// GetColor implements Piece. func (r Rook) GetColor() types.ChessColor { return r.Color } func (r Rook) GetAllMovesButBlocked(board Board, fromSquare types.Coordinate) []types.Coordinate { - return []types.Coordinate{} + return board.GetNonBlockedRowAndColumn(fromSquare) } diff --git a/chess/rook_test.go b/chess/rook_test.go new file mode 100644 index 0000000..9fbe142 --- /dev/null +++ b/chess/rook_test.go @@ -0,0 +1,72 @@ +package chess + +import ( + "mchess_server/types" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +func Test_Rook_GetNonBlockedSquares(t *testing.T) { + t.Run("free row and column", func(t *testing.T) { + board := newBoard() + rook := Rook{Color: types.Black} + + board.position[types.Coordinate{Col: 5, Row: 5}] = rook + squares := rook.GetAllMovesButBlocked(board, types.Coordinate{Col: 5, Row: 5}) + assert.Len(t, squares, 14) + assert.Equal(t, types.Coordinate{Col: 4, Row: 5}, squares[0]) + }) + + t.Run("free row and column", func(t *testing.T) { + board := newBoard() + rook := Rook{Color: types.Black} + rookCoordinate := types.Coordinate{Col: 5, Row: 5} + + board.position[rookCoordinate] = rook + board.position[types.Coordinate{Col: 3, Row: 5}] = Pawn{Color: types.White} + 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.GetAllMovesButBlocked(board, types.Coordinate{Col: 5, Row: 5}) + + squaresOnLeft := lo.Filter(squares, func(square types.Coordinate, _ int) bool { + return square.Row == rookCoordinate.Row && square.Col < rookCoordinate.Col + }) + assert.Equal(t, + []types.Coordinate{ + {Col: 4, Row: 5}, + {Col: 3, Row: 5}, + }, + squaresOnLeft) + + squaresOnRight := lo.Filter(squares, func(square types.Coordinate, _ int) bool { + return square.Row == rookCoordinate.Row && square.Col > rookCoordinate.Col + }) + assert.Equal(t, + []types.Coordinate{}, + squaresOnRight) + + squaresAbove := lo.Filter(squares, func(square types.Coordinate, _ int) bool { + return square.Col == rookCoordinate.Col && square.Row > rookCoordinate.Row + }) + assert.Equal(t, + []types.Coordinate{ + {Col: 5, Row: 6}, + }, + squaresAbove) + + squaresBelow := lo.Filter(squares, func(square types.Coordinate, _ int) bool { + return square.Col == rookCoordinate.Col && square.Row < rookCoordinate.Row + }) + assert.Equal(t, + []types.Coordinate{ + {Col: 5, Row: 4}, + {Col: 5, Row: 3}, + {Col: 5, Row: 2}, + {Col: 5, Row: 1}, + }, + squaresBelow) + }) +} diff --git a/types/common.go b/types/common.go index 674d4e6..e0b2145 100644 --- a/types/common.go +++ b/types/common.go @@ -1,49 +1,6 @@ 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 ChessColor string diff --git a/types/coordinate.go b/types/coordinate.go new file mode 100644 index 0000000..9cb428e --- /dev/null +++ b/types/coordinate.go @@ -0,0 +1,127 @@ +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 <= RangeLastValid { + 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 +} + +func (c Coordinate) GetAllSquaresLeft() []Coordinate { + squares := []Coordinate{} + i := 1 + for { + if square := c.Left(i); square != nil { + squares = append(squares, *square) + i += 1 + } else { + return squares + } + } +} + +func (c Coordinate) GetAllSquaresRight() []Coordinate { + squares := []Coordinate{} + i := 1 + for { + if square := c.Right(i); square != nil { + squares = append(squares, *square) + i += 1 + } else { + return squares + } + } +} + +func (c Coordinate) GetAllSquaresAbove() []Coordinate { + squares := []Coordinate{} + i := 1 + for { + if square := c.Up(i); square != nil { + squares = append(squares, *square) + i += 1 + } else { + return squares + } + } +} + +func (c Coordinate) GetAllSquaresBelow() []Coordinate { + squares := []Coordinate{} + i := 1 + for { + if square := c.Down(i); square != nil { + squares = append(squares, *square) + i += 1 + } else { + return squares + } + } +} +func (c Coordinate) GetSquaresOnRow() []Coordinate { + rowToReturn := []Coordinate{} + leftMostSquareOnRow := Coordinate{Col: RangeFirstValid, Row: c.Row} + index := 0 + for { + squareToAppend := leftMostSquareOnRow.Right(index) + if squareToAppend == nil { + break + } + rowToReturn = append(rowToReturn, *squareToAppend) + index = index + 1 + } + return rowToReturn +} + +func (c Coordinate) GetSquaresOnColumn() []Coordinate { + columnToReturn := []Coordinate{} + bottomSquareOnColumn := Coordinate{Col: c.Col, Row: RangeFirstValid} + index := 0 + for { + squareToAppend := bottomSquareOnColumn.Up(index) + if squareToAppend == nil { + break + } + columnToReturn = append(columnToReturn, *squareToAppend) + index = index + 1 + } + return columnToReturn +} diff --git a/types/coordinate_test.go b/types/coordinate_test.go new file mode 100644 index 0000000..ebd0433 --- /dev/null +++ b/types/coordinate_test.go @@ -0,0 +1,25 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Coordinate_GetSquaresOfRow(t *testing.T) { + t.Run("get row for a coordinate", func(t *testing.T) { + startSquare := Coordinate{Col: 4, Row: 2} + row := startSquare.GetSquaresOnRow() + assert.Len(t, row, 8) + assert.Equal(t,row[0].Col, 1) + assert.Equal(t,row[7].Col, 8) + }) + + t.Run("get column for a coordinate", func(t *testing.T) { + startSquare := Coordinate{Col: 4, Row: 2} + column := startSquare.GetSquaresOnColumn() + assert.Len(t, column, 8) + assert.Equal(t,column[0].Row, 1) + assert.Equal(t,column[7].Row, 8) + }) +}