Implement en passant and much more!

This commit is contained in:
Marco 2023-06-26 00:51:20 +02:00
parent 9793c37582
commit f79e5be008
6 changed files with 343 additions and 101 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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'
)