Pieces are now an interface and starting to enforce rules.

This commit is contained in:
Marco 2023-06-20 23:53:54 +02:00
parent d5b8bdf630
commit 7f206b15fe
14 changed files with 329 additions and 65 deletions

View File

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

18
chess/bishop.go Normal file
View File

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

View File

@ -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"
}
ownKingCoordinate := b.getSquareOfPiece(King{Color: movingColor})
if ownKingCoordinate == nil {
return false, string(movingColor) + " king not found"
}
b.isSquareAttacked(*oppKingCoordinate, movingColor.Opposite())
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
var attackedSquares []types.Coordinate
//get every legal move of color to check if this square is attacked
return attacked
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
}

View File

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

22
chess/king.go Normal file
View File

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

19
chess/knight.go Normal file
View File

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

83
chess/pawn.go Normal file
View File

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

11
chess/piece_interface.go Normal file
View File

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

View File

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

18
chess/queen.go Normal file
View File

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

20
chess/rook.go Normal file
View File

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

2
go.mod
View File

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

4
go.sum
View File

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

View File

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