Compare commits

...

38 Commits

Author SHA1 Message Date
Marco 464956dd47 hack to fix crashes when a user connects to a game twice using the same browser 2024-06-27 21:30:29 +02:00
Marco 88fb5b9fa7 go get -u 2024-05-21 23:59:37 +02:00
marco 8dc8e08f35 Merge pull request 'Fix/enable reconnecting to existing games' (#15) from simplify-connecting into master
Reviewed-on: #15
2024-05-21 21:55:41 +00:00
Marco 34143ed3dc remove unused channel 2024-05-21 23:44:37 +02:00
Marco fd2fb3fab6 change behavior of reading/writing from websockets 2024-05-21 23:41:37 +02:00
Marco 6f16a5c23b wip 2024-05-20 17:22:06 +02:00
Marco 636ce06836 Improve reconnection handling
1. Lobbies are only identified by their passphrases
2. Improve logging
3. Do not close an existing websocket connection for a player but ignore
   the request
2024-05-20 15:36:13 +02:00
marco b623410aff Merge pull request 'Make games rejoinable again!' (#14) from make-games-rejoinable-again into master
Reviewed-on: #14
2024-05-19 12:44:57 +00:00
Marco 75fd0cc400 go mod tidy 2024-05-15 21:17:32 +02:00
Marco cd2ab106a2 go get -u 2024-05-15 21:17:01 +02:00
Marco 90dca37d44 fix board state when reconnecting and set access headers 2024-05-15 19:45:35 +02:00
Marco 682ce8437b make games rejoinable 2024-05-15 14:28:14 +02:00
Marco b4c82d6c8f remove .vscode 2024-05-13 16:20:07 +02:00
Marco e4ad872849 introduce some simple methods to set/get a player's color 2024-05-12 15:49:27 +02:00
marco 0f71743598 Merge pull request 'Some more changes for the new game handling api and also rate limiting' (#13) from game-handler into master
Reviewed-on: #13
2024-05-12 13:48:22 +00:00
Marco 97ad45e505 Rate limit websocket connection 2024-05-12 15:42:40 +02:00
Marco 4207a0ed72 forgot files 2024-05-12 15:37:53 +02:00
Marco fc088a04fe More work for introducing a new game handler, also: ratelimiter 2024-05-12 15:36:30 +02:00
Marco 58002a1b38 Groundwork for handler that allows reconnecting 2024-05-11 22:57:47 +02:00
marco 829a80d62a Merge pull request 'Replace old websocket library with gorilla's one' (#12) from introduce-gorilla-ws into master
Reviewed-on: #12
2024-05-11 17:17:50 +00:00
Marco 42dc4e5af8 go mod tidy 2024-05-11 12:05:41 +02:00
Marco 65dc2ebd24 Replace old websocket library with the gorilla one 2024-05-11 12:03:24 +02:00
Marco d40c757776 fix bug that prevented king from moving because server thought it wanted to castling (but it was not a castling move) 2024-05-10 02:29:11 +02:00
marco a55a46e866 Merge pull request 'get-lobby-from-passphrase' (#11) from get-lobby-from-passphrase into master
Reviewed-on: #11
2024-05-09 20:49:24 +00:00
Marco d7c4f28f3a Fix endpoint for getting lobby id from passphrase
Had to add several helpers (e.g. passphrase ones) to make the endpoint
for getting lobby id work.

Moved all handler functions into handler package.

Added test for getting lobby from passphrase.
2024-05-09 22:29:48 +02:00
Marco 11d6e8c98a Introduce handler to get lobby info from passphrase 2024-05-07 23:39:05 +02:00
Marco b2ebf2cc5d Remove more words and remove duplicates 2024-05-02 12:47:18 +02:00
Marco 3b77a01dad go get -u 2024-05-01 01:15:32 +02:00
marco d50c4908c8 Merge pull request 'We introduce a pre-filtered list of (hopefully) clean words. Generating the passphrase with babbler was too slow' (#10) from clean-words into master
Reviewed-on: #10
2024-04-30 23:13:34 +00:00
Marco d7731c08eb We introduce a pre-filtered list of (hopefully) clean words. Generating the passphrase with babbler was too slow 2024-04-30 23:55:27 +02:00
Marco db754f5601 Do not remove a disconnected player from the 'player' slice since that crashes the server 2024-04-23 19:29:32 +02:00
marco 732d759f84 Merge pull request 'Prevent castling in case there is no rook on the castling side' (#8) from fix-castling-without-rook into master
Reviewed-on: #8
2024-04-16 16:20:33 +00:00
Marco 25893a5187 Prevent castling in case there is no rook on the castling side 2024-04-16 18:17:28 +02:00
marco 4153f4cc7c Merge pull request 'Fix a bug that allowed castling even if squares were attacked' (#3) from castling-through-check into master
Reviewed-on: #3
2024-04-09 19:55:40 +00:00
Marco 77e66aabdd Fix a bug that allowed castling even if squares were attacked 2024-04-09 21:48:27 +02:00
Marco 24a4f19d28 Run go get -u 2024-02-18 19:18:46 +01:00
marco 0190ff99d4 Merge pull request 'checkmate-and-stalemate-check' (#2) from checkmate-and-stalemate-check into master
Reviewed-on: #2
2024-01-17 23:07:14 +01:00
Marco af993876fd Introduce check if the game has ended (checkmate/stalemate)
Introduce checkmate check and send out 'gameEnded' message
2024-01-17 23:06:48 +01:00
35 changed files with 61805 additions and 704 deletions

15
.vscode/launch.json vendored
View File

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/main.go"
}
]
}

139
api/handler/handler.go Normal file
View File

@ -0,0 +1,139 @@
package handler
import (
"mchess_server/api"
"mchess_server/chess"
"mchess_server/lobbies"
"mchess_server/utils"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/ratelimit"
)
var mut sync.Mutex
var limiter = ratelimit.New(10)
func HostGameHandler(c *gin.Context) {
limiter.Take()
player := chess.NewPlayer(uuid.New())
u := lobbies.GetUsher()
mut.Lock()
defer mut.Unlock()
lobby := u.CreateNewPrivateLobby(player)
u.AddPlayerToLobbyAndStartGameIfFull(player, lobby)
passphrase := lobby.Passphrase.String()
info := api.PlayerInfo{
PlayerID: &player.Uuid,
Passphrase: &passphrase,
}
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, info)
}
func GetLobbyForPassphraseHandler(c *gin.Context) {
limiter.Take()
reqPassphrase := c.Param("phrase")
if reqPassphrase == "" {
c.IndentedJSON(http.StatusBadRequest, reqPassphrase)
return
}
passPhraseWithSpaces := utils.ConvertToPassphraseWithSpaces(reqPassphrase)
lobby := lobbies.GetLobbyRegistry().GetLobbyByPassphrase(passPhraseWithSpaces)
if lobby == nil {
c.IndentedJSON(http.StatusNotFound, reqPassphrase)
return
}
passphrase := api.Passphrase{
Value: (*string)(&lobby.Passphrase),
}
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, passphrase)
}
func JoinPrivateGame(c *gin.Context) {
limiter.Take()
req := api.PlayerInfo{}
err := c.ShouldBindJSON(&req)
if err != nil || req.Passphrase == nil || *req.Passphrase == "" {
c.IndentedJSON(http.StatusNotFound, req)
return
}
u := lobbies.GetUsher()
if req.PlayerID != nil { //is reconnect
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*req.Passphrase))
var found bool
if lobby != nil {
_, found = lobby.GetPlayerByUUID(*req.PlayerID)
}
if found {
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(
http.StatusOK,
api.PlayerInfo{
PlayerID: req.PlayerID,
Passphrase: req.Passphrase,
})
return
} else {
c.IndentedJSON(http.StatusNotFound, req)
return
}
}
player := chess.NewPlayer(uuid.New())
mut.Lock()
defer mut.Unlock()
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*req.Passphrase))
if lobby != nil {
u.AddPlayerToLobbyAndStartGameIfFull(player, lobby)
} else {
c.IndentedJSON(http.StatusNotFound, req)
return
}
info := api.PlayerInfo{
PlayerID: &player.Uuid,
Passphrase: req.Passphrase,
}
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, info)
}
func JoinGameHandler(c *gin.Context) {
limiter.Take()
passphrase := api.Passphrase{}
err := c.ShouldBindJSON(&passphrase)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, nil)
return
}
u := lobbies.GetUsher()
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*passphrase.Value))
if lobby == nil {
c.IndentedJSON(http.StatusNotFound, nil)
return
}
lobbyInfo := api.Passphrase{
Value: (*string)(&lobby.Passphrase),
}
c.IndentedJSON(http.StatusOK, lobbyInfo)
}

View File

@ -0,0 +1,55 @@
package handler
import (
"encoding/json"
"mchess_server/api"
"mchess_server/utils"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func Test_GetLobbyFromPassphraseHandler(t *testing.T) {
var passphraseURLParameter string
var receivedPhrase string
t.Run("host a lobby", func(t *testing.T) {
r1 := httptest.NewRecorder()
ctx1, e1 := gin.CreateTestContext(r1)
e1.GET("/api/hostPrivate", HostGameHandler)
hostGameRequest, _ := http.NewRequest("GET", "/api/hostPrivate", nil)
ctx1.Request = hostGameRequest
e1.ServeHTTP(r1, hostGameRequest)
playerInfo := api.PlayerInfo{}
err := json.Unmarshal(r1.Body.Bytes(), &playerInfo)
assert.NoError(t, err)
receivedPhrase = *playerInfo.Passphrase
passphrase := utils.NewPassphraseFromString(receivedPhrase)
passphraseURLParameter = passphrase.AsURLParam()
})
t.Run("see if the lobby can be fetched by using the passphrase", func(t *testing.T) {
r2 := httptest.NewRecorder()
ctx2, engine := gin.CreateTestContext(r2)
engine.GET("/api/getLobbyForPassphrase/:phrase", GetLobbyForPassphraseHandler)
url := "/api/getLobbyForPassphrase/" + passphraseURLParameter
getLobbyRequest, _ := http.NewRequest("GET", url, nil)
ctx2.Request = getLobbyRequest
engine.ServeHTTP(r2, getLobbyRequest)
passhrase := api.Passphrase{}
err := json.Unmarshal(r2.Body.Bytes(), &passhrase)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, r2.Code)
})
}

83
api/handler/websocket.go Normal file
View File

@ -0,0 +1,83 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"log"
"mchess_server/api"
"mchess_server/utils"
"net/http"
"mchess_server/lobbies"
"github.com/gin-gonic/gin"
gorillaws "github.com/gorilla/websocket"
)
var upgrader = gorillaws.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func RegisterWebSocketConnection(c *gin.Context) {
limiter.Take()
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println(err)
return
}
go waitForAndHandlePlayerID(c, conn)
}
func waitForAndHandlePlayerID(ctx context.Context, conn *gorillaws.Conn) {
limiter.Take()
msgType, msg, err := conn.ReadMessage()
if err != nil {
errorMessage := fmt.Sprintf("Reading from websocket connection did not work: %s", err)
log.Println(errorMessage)
conn.Close()
return
}
log.Println("read from websocket endpoint: ", msgType, string(msg), err)
var info api.PlayerInfo
err = json.Unmarshal(msg, &info)
if err != nil {
errorMessage := fmt.Sprintf("Unmarshaling message did not work: %s", err)
log.Println(errorMessage)
conn.WriteMessage(msgType, []byte(errorMessage))
conn.Close()
return
}
lobby := lobbies.GetLobbyRegistry().GetLobbyByPassphrase(utils.NewPassphraseFromString(*info.Passphrase))
if lobby == nil {
conn.WriteMessage(msgType, []byte("lobby not found"))
conn.Close()
return
}
player, found := lobby.GetPlayerByUUID(*info.PlayerID)
if !found {
conn.WriteMessage(msgType, []byte("player not found"))
conn.Close()
return
}
lobby.Game.SetWebsocketConnectionFor(ctx, player, conn)
log.Println("player after setting connection: ")
log.Println("id: ", player.Uuid)
log.Println("color: ", player.GetColor())
log.Println("Connection: ", player.Conn.ID)
}
func ConnectWsForGame(c *gin.Context) {
limiter.Take()
}

View File

@ -21,6 +21,7 @@ const (
MoveMessage MessageType = "move"
InvalidMoveMessage MessageType = "invalidMove"
ColorDetermined MessageType = "colorDetermined"
GameEnded MessageType = "gameEnded"
)
func (m WebsocketMessage) IsValid() bool {

5
api/passphrase.go Normal file
View File

@ -0,0 +1,5 @@
package api
type Passphrase struct {
Value *string `json:"value,omitempty"`
}

View File

@ -4,6 +4,5 @@ import "github.com/google/uuid"
type PlayerInfo struct {
PlayerID *uuid.UUID `json:"playerID,omitempty"`
LobbyID *uuid.UUID `json:"lobbyID,omitempty"`
Passphrase *string `json:"passphrase,omitempty"`
}

View File

@ -9,14 +9,14 @@ type Bishop struct {
}
func (b Bishop) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return b.GetAllNonBlockedMoves(board, fromSquare)
return b.GetAllNonBlockedSquares(board, fromSquare)
}
func (b Bishop) GetColor() types.ChessColor {
return b.Color
}
func (b Bishop) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate {
func (b Bishop) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return board.GetNonBlockedDiagonals(fromSquare)
}

View File

@ -57,7 +57,7 @@ func (b *Board) Init() {
b.position[types.Coordinate{Row: 8, Col: 8}] = Rook{Color: types.Black}
}
func (b *Board) CheckAndPlay(move types.Move) (bool, Violation) {
func (b *Board) CheckAndPlay(move *types.Move) (bool, Violation) {
tempBoard := b.getCopyOfBoard()
//Check start square of move
@ -80,13 +80,13 @@ func (b *Board) CheckAndPlay(move types.Move) (bool, Violation) {
}
}
wasSpecialMove, err := tempBoard.handleSpecialMove(move)
if err != nil {
return false, InvalidMove
wasSpecialMove, violation := tempBoard.handleSpecialMove(*move)
if violation != "" {
return false, violation
}
if !wasSpecialMove {
allMovesExceptBlocked := pieceAtStartSquare.GetAllNonBlockedMoves(tempBoard, move.StartSquare)
allMovesExceptBlocked := pieceAtStartSquare.GetAllNonBlockedSquares(tempBoard, move.StartSquare)
legal := lo.Contains(allMovesExceptBlocked, move.EndSquare)
if !legal {
return false, InvalidMove
@ -109,7 +109,7 @@ func (b *Board) CheckAndPlay(move types.Move) (bool, Violation) {
b.position = tempBoard.position
b.history = tempBoard.history
b.colorToMove = b.colorToMove.Opposite()
b.appendMoveToHistory(move)
b.appendMoveToHistory(*move)
pieceAtStartSquare.AfterMoveAction(b, move.StartSquare)
@ -188,19 +188,82 @@ func (p Position) getCopyOfPosition() Position {
return position
}
func (b *Board) handleSpecialMove(move types.Move) (bool, error) {
func (b *Board) handleSpecialMove(move types.Move) (bool, Violation) {
var was bool
var err error
var violation Violation
var pieceAtStartSquare = b.getPieceAt(move.StartSquare)
switch piece := pieceAtStartSquare.(type) {
case Pawn:
was, err = piece.HandlePossiblePromotion(b, move)
was, violation = piece.HandlePossiblePromotion(b, move)
if !was {
was, err = piece.HandleEnPassant(b, move, b.getLastMove())
was, violation = piece.HandleEnPassant(b, move, b.getLastMove())
}
case King:
was, err = piece.HandleCastling(b, move)
was, violation = piece.HandleCastling(b, move)
}
return was, err
return was, violation
}
type GameEndedReason string
const (
NoReason GameEndedReason = "noReason"
WhiteIsCheckmated GameEndedReason = "whiteIsCheckmated"
BlackIsCheckmated GameEndedReason = "blackIsCheckmated"
StalemateReason GameEndedReason = "stalemate"
)
func (r GameEndedReason) String() string {
return string(r)
}
func (b *Board) HasGameEnded(lastMove types.Move) (bool, GameEndedReason) {
checkForColor := lastMove.ColorMoved.Opposite()
if checkmate := b.isColorCheckmated(checkForColor); checkmate {
switch checkForColor {
case types.White:
return true, WhiteIsCheckmated
case types.Black:
return true, BlackIsCheckmated
}
}
if b.isStalemate() {
return true, StalemateReason
}
return false, NoReason
}
func (b *Board) isColorCheckmated(color types.ChessColor) bool {
inCheck, _ := b.isKingOfMovingColorInCheck(color)
if !inCheck {
return false
}
copyOfBoard := b.getCopyOfBoard()
var movesToCheck []types.Move
for startSquare, piece := range b.position {
if piece.GetColor() == color {
for _, endSquare := range piece.GetAllNonBlockedSquares(*b, startSquare) {
move := types.Move{StartSquare: startSquare, EndSquare: endSquare}
movesToCheck = append(movesToCheck, move)
}
}
}
for _, move := range movesToCheck {
valid, _ := copyOfBoard.CheckAndPlay(&move)
if valid {
return false
}
}
return true
}
func (b *Board) isStalemate() bool {
return false
}

View File

@ -0,0 +1,329 @@
package chess
import (
"mchess_server/types"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Board_WhiteCastlesLeft(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 1, Row: 1}] = Rook{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 3, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.True(t, valid)
}
func Test_Board_WhiteCastlesRight(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 8, Row: 1}] = Rook{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 7, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.True(t, valid)
}
func Test_Board_BlackCastlesLeft(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 1, Row: 8}] = Rook{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.True(t, valid)
}
func Test_Board_BlackCastlesRight(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 8, Row: 8}] = Rook{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 7, Row: 8},
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.True(t, valid)
}
func Test_Board_WhiteCastlesLeft_QueenCoversSquareInBetween(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 1, Row: 1}] = Rook{Color: types.White}
board.position[types.Coordinate{Col: 4, Row: 5}] = Queen{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 3, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_WhiteCastlesRight_QueenCoversSquareInBetween(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 8, Row: 1}] = Rook{Color: types.White}
board.position[types.Coordinate{Col: 6, Row: 5}] = Queen{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 7, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_WhiteCastlesLeft_KingInCheck(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 1, Row: 1}] = Rook{Color: types.White}
board.position[types.Coordinate{Col: 5, Row: 5}] = Queen{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 3, Row: 1},
ColorMoved: types.White,
}
valid, violation := board.CheckAndPlay(&castling)
assert.False(t, valid)
assert.Equal(t, CastlingWhileKingInCheck, violation)
}
func Test_Board_WhiteCastlesLeft_QueenCoversKingsSquare(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 1, Row: 1}] = Rook{Color: types.White}
board.position[types.Coordinate{Col: 3, Row: 5}] = Queen{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 3, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_BlackCastlesLeft_QueenCoversSquareInBetween(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 1, Row: 8}] = Rook{Color: types.Black}
board.position[types.Coordinate{Col: 4, Row: 4}] = Queen{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_BlackCastlesRight_QueenCoversSquareInBetween(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 8, Row: 8}] = Rook{Color: types.Black}
board.position[types.Coordinate{Col: 6, Row: 4}] = Queen{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 7, Row: 8},
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_BlackCastlesLeft_KingInCheck(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 1, Row: 8}] = Rook{Color: types.Black}
board.position[types.Coordinate{Col: 5, Row: 4}] = Queen{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
ColorMoved: types.Black,
}
valid, violation := board.CheckAndPlay(&castling)
assert.False(t, valid)
assert.Equal(t, CastlingWhileKingInCheck, violation)
}
func Test_Board_BlackCastlesLeft_QueenCoversKingsSquare(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 1, Row: 8}] = Rook{Color: types.Black}
board.position[types.Coordinate{Col: 3, Row: 4}] = Queen{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_WhiteCastlesLeft_NoRook(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 3, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_WhiteCastlesRight_NoRook(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 5, Row: 1}] = King{Color: types.White}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 1},
EndSquare: types.Coordinate{Col: 7, Row: 1},
ColorMoved: types.White,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_BlackCastlesLeft_NoRook(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
kingSquare := types.Coordinate{Col: 5, Row: 8}
kingDestinationSquare := types.Coordinate{Col: 3, Row: 8}
board.position[kingSquare] = King{Color: types.Black}
castling := types.Move{
StartSquare: kingSquare,
EndSquare: kingDestinationSquare,
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
assert.Equal(t, nil, board.getPieceAt(kingDestinationSquare))
}
func Test_Board_BlackCastlesRight_NoRook(t *testing.T) {
board := newBoard()
board.colorToMove = types.Black
board.position[types.Coordinate{Col: 5, Row: 8}] = King{Color: types.Black}
castling := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 7, Row: 8},
ColorMoved: types.Black,
}
valid, _ := board.CheckAndPlay(&castling)
assert.False(t, valid)
}
func Test_Board_fixBugThatPreventsCastling(t *testing.T) {
board := newBoard()
board.Init()
//white move
valid, violation := board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 2}, EndSquare: types.Coordinate{Col: 5, Row: 3}})
assert.True(t, valid)
assert.Empty(t, violation)
//black move
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 7}, EndSquare: types.Coordinate{Col: 5, Row: 6}})
assert.True(t, valid)
assert.Empty(t, violation)
//white move
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 4, Row: 2}, EndSquare: types.Coordinate{Col: 4, Row: 3}})
assert.True(t, valid)
assert.Empty(t, violation)
//black move
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 6, Row: 7}, EndSquare: types.Coordinate{Col: 6, Row: 6}})
assert.True(t, valid)
assert.Empty(t, violation)
//queen moves to check the king
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 4, Row: 1}, EndSquare: types.Coordinate{Col: 8, Row: 5}})
assert.True(t, valid)
assert.Empty(t, violation)
//this moves should be valid but it was not because of a bug
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 8}, EndSquare: types.Coordinate{Col: 5, Row: 7}})
assert.True(t, valid)
assert.Empty(t, violation)
}

View File

@ -20,7 +20,7 @@ func Test_CheckMove_validPawnMove(t *testing.T) {
EndSquare: types.Coordinate{Col: 1, Row: 3},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.True(t, good)
//we take the pawn
@ -28,7 +28,7 @@ func Test_CheckMove_validPawnMove(t *testing.T) {
StartSquare: types.Coordinate{Col: 2, Row: 4},
EndSquare: types.Coordinate{Col: 1, Row: 3},
}
good, _ = board.CheckAndPlay(secondMove)
good, _ = board.CheckAndPlay(&secondMove)
assert.True(t, good)
}
@ -45,7 +45,7 @@ func Test_CheckMove_enPassant(t *testing.T) {
EndSquare: types.Coordinate{Col: 5, Row: 4},
}
good, reason := board.CheckAndPlay(move)
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}])
@ -54,7 +54,7 @@ func Test_CheckMove_enPassant(t *testing.T) {
StartSquare: types.Coordinate{Col: 6, Row: 4},
EndSquare: types.Coordinate{Col: 5, Row: 3},
}
good, reason = board.CheckAndPlay(newMove)
good, reason = board.CheckAndPlay(&newMove)
assert.True(t, good)
assert.Empty(t, reason)
// the black pawn is on its correct square
@ -77,7 +77,7 @@ func Test_CheckMove_invalidPawnMoves(t *testing.T) {
StartSquare: types.Coordinate{Col: 2, Row: 5},
EndSquare: types.Coordinate{Col: 2, Row: 6},
}
legalMove, _ := board.CheckAndPlay(move)
legalMove, _ := board.CheckAndPlay(&move)
assert.False(t, legalMove)
})
@ -93,7 +93,7 @@ func Test_CheckMove_invalidPawnMoves(t *testing.T) {
StartSquare: types.Coordinate{Col: 2, Row: 5},
EndSquare: types.Coordinate{Col: 3, Row: 5},
}
legal, _ := board.CheckAndPlay(move)
legal, _ := board.CheckAndPlay(&move)
assert.False(t, legal)
assert.Equal(t, boardBeforeMove, board)
@ -102,7 +102,7 @@ func Test_CheckMove_invalidPawnMoves(t *testing.T) {
StartSquare: types.Coordinate{Col: 2, Row: 5},
EndSquare: types.Coordinate{Col: 1, Row: 5},
}
legal, _ = board.CheckAndPlay(move)
legal, _ = board.CheckAndPlay(&move)
assert.False(t, legal)
assert.Equal(t, boardBeforeMove, board)
@ -111,7 +111,7 @@ func Test_CheckMove_invalidPawnMoves(t *testing.T) {
StartSquare: types.Coordinate{Col: 2, Row: 5},
EndSquare: types.Coordinate{Col: 6, Row: 5},
}
legal, _ = board.CheckAndPlay(move)
legal, _ = board.CheckAndPlay(&move)
assert.False(t, legal)
assert.Equal(t, boardBeforeMove, board)
@ -130,7 +130,7 @@ func Test_CheckMove_invalidPawnMoves(t *testing.T) {
StartSquare: types.Coordinate{Col: 2, Row: 6},
EndSquare: types.Coordinate{Col: 2, Row: 7},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.False(t, good)
assert.Equal(t, boardBeforeMove, board)
@ -151,7 +151,7 @@ func Test_CheckMove_validPromotion(t *testing.T) {
EndSquare: types.Coordinate{Col: 1, Row: 8},
PromotionToPiece: &shortName,
}
good, reason := board.CheckAndPlay(move)
good, reason := board.CheckAndPlay(&move)
assert.Empty(t, reason)
assert.True(t, good)
@ -179,13 +179,13 @@ func Test_CheckMove_HistoryWorks(t *testing.T) {
EndSquare: types.Coordinate{Col: 1, Row: 4},
}
good, _ := board.CheckAndPlay(firstMove)
good, _ := board.CheckAndPlay(&firstMove)
assert.True(t, good)
good, _ = board.CheckAndPlay(secondMove)
good, _ = board.CheckAndPlay(&secondMove)
assert.True(t, good)
good, _ = board.CheckAndPlay(thirdMove)
good, _ = board.CheckAndPlay(&thirdMove)
assert.True(t, good)
expectedHistory := []types.Move{
@ -221,13 +221,13 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 8, Row: 8}] = Rook{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 7, Row: 8},
}
good, reason := board.CheckAndPlay(move)
good, reason := board.CheckAndPlay(&move)
assert.True(t, good)
assert.Empty(t, reason)
@ -246,13 +246,13 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 8, Row: 8}] = Rook{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
}
good, reason := board.CheckAndPlay(move)
good, reason := board.CheckAndPlay(&move)
assert.True(t, good)
assert.Empty(t, reason)
@ -272,25 +272,25 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 8, Row: 8}] = Rook{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
//Move black
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 8}, EndSquare: types.Coordinate{Col: 4, Row: 8}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 8}, EndSquare: types.Coordinate{Col: 4, Row: 8}})
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 2}, EndSquare: types.Coordinate{Col: 5, Row: 1}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 2}, EndSquare: types.Coordinate{Col: 5, Row: 1}})
//Move black back
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 4, Row: 8}, EndSquare: types.Coordinate{Col: 5, Row: 8}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 4, Row: 8}, EndSquare: types.Coordinate{Col: 5, Row: 8}})
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.False(t, good)
})
@ -304,13 +304,13 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 7, Row: 8}] = Bishop{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 7, Row: 8},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.False(t, good)
})
@ -324,13 +324,13 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 4, Row: 8}] = Bishop{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.False(t, good)
})
@ -344,13 +344,13 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 3, Row: 8}] = Bishop{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.False(t, good)
})
@ -364,13 +364,88 @@ func Test_Promotion_BlackKing(t *testing.T) {
board.position[types.Coordinate{Col: 2, Row: 8}] = Bishop{Color: types.Black}
//Make dummy move for white
board.CheckAndPlay(types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 1}, EndSquare: types.Coordinate{Col: 5, Row: 2}})
move := types.Move{
StartSquare: types.Coordinate{Col: 5, Row: 8},
EndSquare: types.Coordinate{Col: 3, Row: 8},
}
good, _ := board.CheckAndPlay(move)
good, _ := board.CheckAndPlay(&move)
assert.False(t, good)
})
}
func Test_Board_HasGameEnded(t *testing.T) {
board := newBoard()
t.Run("no checkmate yet", func(t *testing.T) {
board.position[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 7, Row: 6}] = Queen{Color: types.White}
board.position[types.Coordinate{Col: 7, Row: 7}] = Queen{Color: types.White}
board.position[types.Coordinate{Col: 2, Row: 8}] = King{Color: types.Black}
whiteMove := types.Move{
StartSquare: types.Coordinate{Col: 7, Row: 6},
EndSquare: types.Coordinate{Col: 8, Row: 6},
ColorMoved: types.White,
}
valid, violation := board.CheckAndPlay(&whiteMove)
assert.True(t, valid)
assert.Empty(t, violation)
gameEnded, reason := board.HasGameEnded(whiteMove)
assert.False(t, gameEnded)
assert.Equal(t, NoReason, reason)
})
t.Run("checkmate move is made", func(t *testing.T) {
blackMove := types.Move{
StartSquare: types.Coordinate{Col: 2, Row: 8},
EndSquare: types.Coordinate{Col: 1, Row: 8},
ColorMoved: types.Black,
}
valid, violation := board.CheckAndPlay(&blackMove)
assert.True(t, valid)
assert.Empty(t, violation)
checkmateMove := types.Move{
StartSquare: types.Coordinate{Col: 8, Row: 6},
EndSquare: types.Coordinate{Col: 8, Row: 8},
ColorMoved: types.White,
}
valid, violation = board.CheckAndPlay(&checkmateMove)
assert.True(t, valid)
assert.Empty(t, violation)
gameEnded, reason := board.HasGameEnded(checkmateMove)
assert.True(t, gameEnded)
assert.Equal(t, BlackIsCheckmated, reason)
})
}
func Test_Board_HasGameEnded_RookBlocks(t *testing.T) {
board := newBoard()
board.position[types.Coordinate{Col: 1, Row: 1}] = King{Color: types.White}
board.position[types.Coordinate{Col: 8, Row: 7}] = Queen{Color: types.White}
board.position[types.Coordinate{Col: 7, Row: 7}] = Queen{Color: types.White}
board.position[types.Coordinate{Col: 1, Row: 8}] = King{Color: types.Black}
board.position[types.Coordinate{Col: 2, Row: 2}] = Rook{Color: types.Black}
whiteMove := types.Move{
StartSquare: types.Coordinate{Col: 8, Row: 7},
EndSquare: types.Coordinate{Col: 8, Row: 8},
ColorMoved: types.White,
}
valid, violation := board.CheckAndPlay(&whiteMove)
assert.True(t, valid)
assert.Empty(t, violation)
gameEnded, reason := board.HasGameEnded(whiteMove)
assert.False(t, gameEnded)
assert.Equal(t, NoReason, reason)
}

View File

@ -8,8 +8,7 @@ import (
"mchess_server/types"
"github.com/google/uuid"
"github.com/samber/lo"
"nhooyr.io/websocket"
gorillaws "github.com/gorilla/websocket"
)
type Game struct {
@ -27,6 +26,7 @@ const (
PlayerToMove
CheckMove
CheckPlayerChange
GameEnded
)
func NewGame() *Game {
@ -58,8 +58,8 @@ func (game Game) GetPlayer2() *Player {
}
func (game *Game) prepare() {
game.players[0].color = types.White
game.players[1].color = types.Black
game.players[0].SetColor(types.White)
game.players[1].SetColor(types.Black)
game.currentTurnPlayer = game.GetPlayer1()
@ -85,6 +85,7 @@ func (game *Game) Handle() {
defer game.killGame()
var receivedMove types.Move
var gameEndReason GameEndedReason
var err error
for {
@ -98,29 +99,31 @@ func (game *Game) Handle() {
game.gameState = PlayerToMove
case PlayerToMove:
log.Println("with ", game.currentTurnPlayer.GetPlayerColor(), " to move")
log.Println("with ", game.currentTurnPlayer.GetColor(), " to move")
receivedMove, err = game.currentTurnPlayer.ReadMove()
if err != nil {
log.Println("Error while reading message:", err)
return
}
log.Println("Player ", game.currentTurnPlayer, " moved:\n", receivedMove)
log.Println("Player ", game.currentTurnPlayer.color.String(), " moved:\n", receivedMove)
game.gameState = CheckMove
case CheckMove:
valid, ruleViolation := game.board.CheckAndPlay(receivedMove)
valid, ruleViolation := game.board.CheckAndPlay(&receivedMove)
if valid {
game.gameState = CheckPlayerChange
} else {
if !valid {
invalidMoveMessage, err := api.GetInvalidMoveMessage(receivedMove, ruleViolation.String())
if err != nil {
log.Println("Error marshalling 'colorDetermined' message for player 1", err)
return
}
game.currentTurnPlayer.writeMessage(invalidMoveMessage)
game.currentTurnPlayer.writeMessage(string(invalidMoveMessage))
game.gameState = PlayerToMove
continue
}
game.gameState = CheckPlayerChange
case CheckPlayerChange:
if game.currentTurnPlayer.Uuid == game.players[0].Uuid {
game.currentTurnPlayer = game.players[1]
@ -134,7 +137,17 @@ func (game *Game) Handle() {
return
}
if gameEnded, reason := game.board.HasGameEnded(receivedMove); gameEnded {
gameEndReason = reason
game.gameState = GameEnded
continue
}
game.gameState = PlayerToMove
case GameEnded:
game.broadcastGameEnd(gameEndReason)
return
}
log.Println("GameState = ", game.gameState)
}
@ -144,6 +157,14 @@ func (game *Game) AddPlayersToGame(player *Player) {
game.players = append(game.players, player)
}
func (game *Game) AreBothPlayersConnected() bool {
if len(game.GetPlayers()) < 2 {
return false
}
return game.players[0].hasWebsocketConnection() && game.players[1].hasWebsocketConnection()
}
func (game *Game) killGame() {
log.Println("Game should be killed")
}
@ -160,35 +181,49 @@ func (game Game) notifyPlayersAboutGameStart() error {
return err
}
game.GetPlayer1().writeMessage(colorDeterminedPlayer1)
game.GetPlayer1().writeMessage(string(colorDeterminedPlayer1))
game.GetPlayer1().SendBoardState(types.Move{}, game.board.PGN(), types.White)
game.GetPlayer2().writeMessage(colorDeterminedPlayer2)
game.GetPlayer2().writeMessage(string(colorDeterminedPlayer2))
game.GetPlayer2().SendBoardState(types.Move{}, game.board.PGN(), types.White)
return nil
}
func (game Game) broadcastMove(move types.Move) error {
err := game.GetPlayer1().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.color)
log.Println("broadcast move")
err := game.GetPlayer1().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.GetColor())
if err != nil {
return err
}
err = game.GetPlayer2().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.color)
err = game.GetPlayer2().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.GetColor())
if err != nil {
return err
}
return nil
}
func (game *Game) playerDisconnected(p *Player) {
log.Println(string(p.color), " disconnected")
playerStillInGame := lo.Filter(game.players, func(player *Player, _ int) bool {
return player.color != p.color
})
game.players = playerStillInGame
func (game Game) broadcastGameEnd(reason GameEndedReason) error {
err := game.GetPlayer1().SendGameEnded(reason)
if err != nil {
return err
}
err = game.GetPlayer2().SendGameEnded(reason)
if err != nil {
return err
}
return nil
}
func (game *Game) SetWebsocketConnectionFor(ctx context.Context, p *Player, ws *websocket.Conn) {
p.SetWebsocketConnectionAndSendBoardState(ctx, ws, game.board.PGN(), game.board.colorToMove)
func (game *Game) playerDisconnected(p *Player) {
}
func (game *Game) SetWebsocketConnectionFor(ctx context.Context, p *Player, ws *gorillaws.Conn) {
p.SetWebsocketConnectionAndSendBoardState(ctx, ws, &game.board)
}
func (game *Game) SendBoardStateTo(p *Player) {
p.SendBoardState(game.board.getLastMove(), game.board.PGN(), game.board.colorToMove)
}

View File

@ -9,14 +9,14 @@ type King struct {
}
func (k King) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return k.GetAllNonBlockedMoves(board, fromSquare)
return k.GetAllNonBlockedSquares(board, fromSquare)
}
func (k King) GetColor() types.ChessColor {
return k.Color
}
func (k King) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate {
func (k King) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return board.GetNonBlockedKingMoves(fromSquare)
}
@ -29,25 +29,26 @@ func (k King) AfterMoveAction(board *Board, fromSquare types.Coordinate) {
}
}
func (k King) HandleCastling(board *Board, move types.Move) (bool, error) {
func (k King) HandleCastling(board *Board, move types.Move) (bool, Violation) {
if !k.isMoveCastlingMove(board, move) {
return false, ""
}
if k.hadMovedBefore(board) {
return false, nil
return false, ""
}
valid, dir := k.movedOnValidCastlingSquare(board, move)
if board.isSquareAttacked(move.StartSquare, k.Color.Opposite()) {
return false, CastlingWhileKingInCheck
}
valid, dir := k.isCastlingAllowed(board, move)
if !valid {
return false, nil
}
switch k.Color {
case types.White:
board.state.WhiteKingMoved = true
case types.Black:
board.state.BlackKingMoved = true
return false, ""
}
k.playCastlingOnBoard(board, move, dir)
return true, nil
return true, ""
}
func (k King) hadMovedBefore(b *Board) bool {
@ -64,52 +65,95 @@ func (k King) hadMovedBefore(b *Board) bool {
type CastlingDirection string
const (
NoDirection CastlingDirection = ""
CastlingRight CastlingDirection = "right"
CastlingLeft CastlingDirection = "left"
)
func (k King) movedOnValidCastlingSquare(b *Board, move types.Move) (bool, CastlingDirection) {
func (k King) isMoveCastlingMove(b *Board, move types.Move) bool {
var destinationSquareForKingRight types.Coordinate
var destinationSquareForKingLeft types.Coordinate
switch k.Color {
case types.White:
destinationSquareForKingRight = types.Coordinate{Col: 7, Row: 1}
destinationSquareForKingLeft = types.Coordinate{Col: 3, Row: 1}
case types.Black:
destinationSquareForKingRight = types.Coordinate{Col: 7, Row: 8}
destinationSquareForKingLeft = types.Coordinate{Col: 3, Row: 8}
}
if move.EndSquare == destinationSquareForKingRight ||
move.EndSquare == destinationSquareForKingLeft {
return true
}
return false
}
func (k King) isCastlingAllowed(b *Board, move types.Move) (bool, CastlingDirection) {
var valid = false
switch k.Color {
case types.White:
castlingSquareRight := types.Coordinate{Col: 7, Row: 1}
castlingSquareLeft := types.Coordinate{Col: 3, Row: 1}
destinationSquareForKingRight := types.Coordinate{Col: 7, Row: 1}
destinationSquareForKingLeft := types.Coordinate{Col: 3, Row: 1}
if move.EndSquare == castlingSquareRight &&
b.getPieceAt(castlingSquareRight) == nil &&
b.getPieceAt(*castlingSquareRight.Left(1)) == nil &&
if move.EndSquare == destinationSquareForKingRight &&
b.getPieceAt(destinationSquareForKingRight) == nil &&
b.getPieceAt(*destinationSquareForKingRight.Left(1)) == nil &&
!b.isSquareAttacked(destinationSquareForKingRight, types.Black) &&
!b.isSquareAttacked(*destinationSquareForKingRight.Left(1), types.Black) &&
!b.state.WhiteHRookMoved {
return true, CastlingRight
_, ok := b.getPieceAt(types.Coordinate{Col: 8, Row: 1}).(Rook)
if ok {
return true, CastlingRight
}
}
if move.EndSquare == castlingSquareLeft &&
b.getPieceAt(castlingSquareLeft) == nil &&
b.getPieceAt(*castlingSquareLeft.Right(1)) == nil &&
b.getPieceAt(*castlingSquareLeft.Left(1)) == nil &&
if move.EndSquare == destinationSquareForKingLeft &&
b.getPieceAt(destinationSquareForKingLeft) == nil &&
b.getPieceAt(*destinationSquareForKingLeft.Right(1)) == nil &&
b.getPieceAt(*destinationSquareForKingLeft.Left(1)) == nil &&
!b.isSquareAttacked(destinationSquareForKingLeft, types.Black) &&
!b.isSquareAttacked(*destinationSquareForKingLeft.Right(1), types.Black) &&
!b.state.WhiteARookMoved {
return true, CastlingLeft
_, ok := b.getPieceAt(types.Coordinate{Col: 1, Row: 1}).(Rook)
if ok {
return true, CastlingLeft
}
}
case types.Black:
castlingSquareRight := types.Coordinate{Col: 7, Row: 8}
castlingSquareLeft := types.Coordinate{Col: 3, Row: 8}
destinationSquareForKingRight := types.Coordinate{Col: 7, Row: 8}
destinationSquareForKingLeft := types.Coordinate{Col: 3, Row: 8}
if move.EndSquare == castlingSquareRight &&
b.getPieceAt(castlingSquareRight) == nil &&
b.getPieceAt(*castlingSquareRight.Left(1)) == nil &&
if move.EndSquare == destinationSquareForKingRight &&
b.getPieceAt(destinationSquareForKingRight) == nil &&
b.getPieceAt(*destinationSquareForKingRight.Left(1)) == nil &&
!b.isSquareAttacked(destinationSquareForKingRight, types.White) &&
!b.isSquareAttacked(*destinationSquareForKingRight.Left(1), types.White) &&
!b.state.BlackHRookMoved {
return true, CastlingRight
_, ok := b.getPieceAt(types.Coordinate{Col: 8, Row: 8}).(Rook)
if ok {
return true, CastlingRight
}
}
if move.EndSquare == castlingSquareLeft &&
b.getPieceAt(castlingSquareLeft) == nil &&
b.getPieceAt(*castlingSquareLeft.Right(1)) == nil &&
b.getPieceAt(*castlingSquareLeft.Left(1)) == nil &&
if move.EndSquare == destinationSquareForKingLeft &&
b.getPieceAt(destinationSquareForKingLeft) == nil &&
b.getPieceAt(*destinationSquareForKingLeft.Right(1)) == nil &&
b.getPieceAt(*destinationSquareForKingLeft.Left(1)) == nil &&
!b.isSquareAttacked(destinationSquareForKingLeft, types.White) &&
!b.isSquareAttacked(*destinationSquareForKingLeft.Right(1), types.White) &&
!b.state.BlackARookMoved {
return true, CastlingLeft
_, ok := b.getPieceAt(types.Coordinate{Col: 1, Row: 8}).(Rook)
if ok {
return true, CastlingLeft
}
}
}
return valid, CastlingRight
return valid, NoDirection
}
func (k King) playCastlingOnBoard(board *Board, move types.Move, dir CastlingDirection) {

View File

@ -9,14 +9,14 @@ type Knight struct {
}
func (n Knight) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return n.GetAllNonBlockedMoves(board, fromSquare)
return n.GetAllNonBlockedSquares(board, fromSquare)
}
func (n Knight) GetColor() types.ChessColor {
return n.Color
}
func (n Knight) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate {
func (n Knight) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return board.GetNonBlockedKnightMoves(fromSquare)
}

View File

@ -12,7 +12,7 @@ type Pawn struct {
func (p Pawn) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
attackingMoves := make([]types.Coordinate, 0, 2)
allMoves := p.GetAllNonBlockedMoves(board, fromSquare)
allMoves := p.GetAllNonBlockedSquares(board, fromSquare)
for _, move := range allMoves {
if move.Col != fromSquare.Col {
attackingMoves = append(attackingMoves, move)
@ -21,7 +21,7 @@ func (p Pawn) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []
return attackingMoves
}
func (p Pawn) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate {
func (p Pawn) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
theoreticalSquares := p.getAllMoves(fromSquare)
legalSquares := p.filterBlockedSquares(board, fromSquare, theoreticalSquares)
@ -32,7 +32,7 @@ func (p Pawn) GetColor() types.ChessColor {
return p.Color
}
func (p *Pawn) HandlePossiblePromotion(b *Board, move types.Move) (bool, error) {
func (p *Pawn) HandlePossiblePromotion(b *Board, move types.Move) (bool, Violation) {
var isPromotionMove bool
var promotionToPiece types.PieceShortName
@ -62,14 +62,14 @@ func (p *Pawn) HandlePossiblePromotion(b *Board, move types.Move) (bool, error)
b.position[move.EndSquare] = GetPieceForShortName(promotionToPiece)
}
return isPromotionMove, nil
return isPromotionMove, ""
}
func (p *Pawn) HandleEnPassant(b *Board, move, lastMove types.Move) (bool, error) {
func (p *Pawn) HandleEnPassant(b *Board, move, lastMove types.Move) (bool, Violation) {
var wasEnPassant bool
if lastMove.PieceMoved.ToCommon() != types.PawnShortName {
return false, nil
return false, ""
}
switch move.ColorMoved {
@ -127,7 +127,7 @@ func (p *Pawn) HandleEnPassant(b *Board, move, lastMove types.Move) (bool, error
b.position[move.EndSquare] = GetPieceForShortName(move.PieceMoved)
}
return wasEnPassant, nil
return wasEnPassant, ""
}
func (p Pawn) getAllMoves(fromSquare types.Coordinate) []types.Coordinate {
theoreticalMoves := make([]types.Coordinate, 0, 4)

View File

@ -6,7 +6,7 @@ import (
)
type Piece interface {
GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate
GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate
GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate
GetColor() types.ChessColor
AfterMoveAction(board *Board, fromSquare types.Coordinate)

View File

@ -10,7 +10,7 @@ import (
"mchess_server/types"
"github.com/google/uuid"
"nhooyr.io/websocket"
gorillaws "github.com/gorilla/websocket"
)
type Player struct {
@ -34,18 +34,27 @@ func (p Player) hasWebsocketConnection() bool {
return p.Conn.HasWebsocketConnection()
}
func (p *Player) SetWebsocketConnection(ctx context.Context, ws *websocket.Conn) {
func (p *Player) SetWebsocketConnection(ctx context.Context, ws *gorillaws.Conn) {
p.Conn.SetWebsocketConnection(ws)
p.Conn.SetForColor(p.color)
}
func (p *Player) SetWebsocketConnectionAndSendBoardState(
ctx context.Context,
ws *websocket.Conn,
boardPosition string,
turnColor types.ChessColor,
ws *gorillaws.Conn,
board *Board,
) {
p.SetWebsocketConnection(ctx, ws)
p.SendBoardState(types.Move{}, boardPosition, turnColor)
p.SendBoardState(board.getLastMove(), board.PGN(), board.colorToMove)
}
func (p *Player) SetColor(color types.ChessColor) {
p.color = color
p.Conn.SetForColor(p.color)
}
func (p *Player) GetColor() types.ChessColor {
return p.color
}
func (p *Player) SetDisconnectCallback(cb func(*Player)) {
@ -63,8 +72,8 @@ func (p *Player) IsInGame() bool {
}
func (p *Player) SendBoardState(move types.Move, boardPosition string, turnColor types.ChessColor) error {
var pColor = p.color
if p.color == "" { // we default to white if we do not know the color yet
var pColor = p.GetColor()
if p.GetColor() == "" { // we default to white if we do not know the color yet
pColor = types.White
}
@ -80,11 +89,8 @@ func (p *Player) SendBoardState(move types.Move, boardPosition string, turnColor
return err
}
err = p.writeMessage(messageToSend)
if err != nil {
log.Println("Error during message writing:", err)
return err
}
p.writeMessage(string(messageToSend))
return nil
}
@ -99,26 +105,35 @@ func (p *Player) SendMoveAndPosition(move types.Move, boardPosition string) erro
return err
}
err = p.writeMessage(messageToSend)
if err != nil {
log.Println("Error during message writing:", err)
return err
}
p.writeMessage(string(messageToSend))
return nil
}
func (p *Player) writeMessage(msg []byte) error {
return p.Conn.Write(msg)
func (p *Player) SendGameEnded(reason GameEndedReason) error {
reasonToSend := reason.String()
messageToSend, err := json.Marshal(api.WebsocketMessage{
Type: api.GameEnded,
Reason: &reasonToSend,
})
if err != nil {
log.Println("Error while marshalling: ", err)
return err
}
p.writeMessage(string(messageToSend))
return nil
}
func (p *Player) writeMessage(msg string) {
p.Conn.Write(msg)
}
func (p *Player) ReadMove() (types.Move, error) {
receivedMessage, err := p.readMessage()
if err != nil {
return types.Move{}, err
}
receivedMessage := p.readMessage()
var msg api.WebsocketMessage
err = json.Unmarshal(receivedMessage, &msg)
err := json.Unmarshal(receivedMessage, &msg)
if err != nil {
return types.Move{}, err
}
@ -130,13 +145,9 @@ func (p *Player) ReadMove() (types.Move, error) {
return *msg.Move, nil
}
func (p *Player) readMessage() ([]byte, error) {
msg, err := p.Conn.Read()
log.Printf("Reading message: %s from player %s", string(msg), p.Uuid.String())
func (p *Player) readMessage() []byte {
msg := p.Conn.Read()
log.Printf("Reading message from %s: %s", p.color.String(), string(msg))
return msg, err
}
func (p Player) GetPlayerColor() string {
return string(p.color)
return msg
}

View File

@ -9,14 +9,14 @@ type Queen struct {
}
func (q Queen) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return q.GetAllNonBlockedMoves(board, fromSquare)
return q.GetAllNonBlockedSquares(board, fromSquare)
}
func (q Queen) GetColor() types.ChessColor {
return q.Color
}
func (q Queen) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate {
func (q Queen) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
squares := board.GetNonBlockedRowAndColumn(fromSquare)
squares = append(squares, board.GetNonBlockedDiagonals(fromSquare)...)
return squares

View File

@ -9,14 +9,14 @@ type Rook struct {
}
func (r Rook) GetAllAttackedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return r.GetAllNonBlockedMoves(board, fromSquare)
return r.GetAllNonBlockedSquares(board, fromSquare)
}
func (r Rook) GetColor() types.ChessColor {
return r.Color
}
func (r Rook) GetAllNonBlockedMoves(board Board, fromSquare types.Coordinate) []types.Coordinate {
func (r Rook) GetAllNonBlockedSquares(board Board, fromSquare types.Coordinate) []types.Coordinate {
return board.GetNonBlockedRowAndColumn(fromSquare)
}

View File

@ -14,7 +14,7 @@ func Test_Rook_GetNonBlockedSquares(t *testing.T) {
rook := Rook{Color: types.Black}
board.position[types.Coordinate{Col: 5, Row: 5}] = rook
squares := rook.GetAllNonBlockedMoves(board, types.Coordinate{Col: 5, Row: 5})
squares := rook.GetAllNonBlockedSquares(board, types.Coordinate{Col: 5, Row: 5})
assert.Len(t, squares, 14)
})
@ -28,7 +28,7 @@ func Test_Rook_GetNonBlockedSquares(t *testing.T) {
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.GetAllNonBlockedMoves(board, rookCoordinate)
squares := rook.GetAllNonBlockedSquares(board, rookCoordinate)
squaresOnLeft := lo.Filter(squares, func(square types.Coordinate, _ int) bool {
return square.Row == rookCoordinate.Row && square.Col < rookCoordinate.Col

View File

@ -3,12 +3,15 @@ package chess
type Violation string
var (
InvalidMove Violation = "invalid move"
NoPieceAtStartSquare Violation = "no piece at start square"
WrongColorMoved Violation = "wrong color moved"
TargetSquareIsOccupied Violation = "target square is occupied"
KingInCheck Violation = "king would be in check after move"
SomethingWentWrong Violation = "something went wrong"
InvalidMove Violation = "invalid move"
NoPieceAtStartSquare Violation = "no piece at start square"
WrongColorMoved Violation = "wrong color moved"
TargetSquareIsOccupied Violation = "target square is occupied"
KingInCheck Violation = "king would be in check after move"
SomethingWentWrong Violation = "something went wrong"
CastlingThroughCheck Violation = "king would move through check"
CastlingWhileKingInCheck Violation = "king cannot castle while in check"
CastlingKingMovedBefore Violation = "king cannot caslte because he moved before"
)
func (v Violation) String() string {

View File

@ -50,7 +50,7 @@ func (b *MessageBuffer) Insert(msg string) {
b.cond.Broadcast()
}
func (b *MessageBuffer) Get() (string, error) {
func (b *MessageBuffer) Get() string {
b.cond.L.Lock()
defer b.cond.L.Unlock()
@ -69,7 +69,7 @@ func (b *MessageBuffer) Get() (string, error) {
}
b.getIndex = b.incrementAndWrapIndex(b.getIndex)
return msg.content, nil
return msg.content
}
func (b MessageBuffer) incrementAndWrapIndex(index int) int {

View File

@ -66,8 +66,7 @@ func Test_MessageBuffer_GetWaitsForFirstData(t *testing.T) {
buf.Insert("delayed-message")
}()
msg, err := buf.Get()
assert.NoError(t, err)
msg := buf.Get()
endTime := time.Now()
@ -79,8 +78,7 @@ func Test_MessageBuffer_GetWaitsForNewData(t *testing.T) {
buf := newMessageBuffer(2)
buf.Insert("message-1")
msg, err := buf.Get()
assert.NoError(t, err)
msg := buf.Get()
assert.Equal(t, "message-1", msg)
go func() {
@ -89,8 +87,7 @@ func Test_MessageBuffer_GetWaitsForNewData(t *testing.T) {
buf.Insert("delayed-message")
}()
msg, err = buf.Get()
assert.NoError(t, err)
msg = buf.Get()
assert.Equal(t, "delayed-message", msg)
}
@ -117,8 +114,7 @@ func Test_MessageBuffer_IndexesAreCorrectAfterOverwritingOldData(t *testing.T) {
},
buf.messages)
msg, err := buf.Get()
assert.NoError(t, err)
msg := buf.Get()
assert.Equal(t, "message-2", msg)
}
@ -126,13 +122,11 @@ func Test_MessageBuffer_GetWaitsForNewDataIfOldOneWasAlreadyGotten(t *testing.T)
buf := newMessageBuffer(2)
buf.Insert(message1)
msg, err := buf.Get()
assert.NoError(t, err)
msg := buf.Get()
assert.Equal(t, message1, msg)
buf.Insert(message2)
msg, err = buf.Get()
assert.NoError(t, err)
msg = buf.Get()
assert.Equal(t, message2, msg)
go func() {
@ -140,8 +134,7 @@ func Test_MessageBuffer_GetWaitsForNewDataIfOldOneWasAlreadyGotten(t *testing.T)
buf.Insert(message3)
}()
msg, err = buf.Get()
assert.NoError(t, err)
msg = buf.Get()
assert.Equal(t, message3, msg)
}
@ -157,9 +150,8 @@ func Test_MessageBuffer_InsertCatchesUpWithRead(t *testing.T) {
buf.Insert(message6)
buf.Insert(message7)
msg, err := buf.Get()
msg := buf.Get()
assert.NoError(t, err)
assert.Equal(t, message3, msg)
}
@ -172,7 +164,7 @@ func Test_MessageBuffer_FuckShitUp(t *testing.T) {
var readMsg = make([]string, 0)
go func() {
for i := 0; i < size*10; i++ {
msg, _ := buf.Get()
msg := buf.Get()
if msg == "99" {
break
}

View File

@ -3,22 +3,38 @@ package connection
import (
"context"
"log"
"mchess_server/types"
"sync"
"nhooyr.io/websocket"
"github.com/google/uuid"
gorillaws "github.com/gorilla/websocket"
)
type Connection struct {
ws *websocket.Conn
wsConnectionEstablished chan bool
ctx context.Context
buffer MessageBuffer
disconnectCallback func()
ID uuid.UUID
ws *gorillaws.Conn
ctx context.Context
rxBuffer *MessageBuffer
txBuffer *MessageBuffer
disconnectCallback func()
forColor types.ChessColor
/* this is a hack since a user using the same
browser might get the same object of type Connection in two instances of
the same browser. Then if Close() gets called for both of these instances
the first Close() will set ws=nil, and the seconds one will dereference this
nil pointer. The correct way to fix this, is to make sure, two instances
of the same browser do not get the same connection instance.
At the moment, the usage of localStorage 'identifies' an already connected
player and reuses the old Connection struct instance */
closingMutex sync.Mutex
}
func NewConnection(options ...func(*Connection)) *Connection {
connection := Connection{
buffer: *newMessageBuffer(100),
wsConnectionEstablished: make(chan bool),
ID: uuid.New(),
rxBuffer: newMessageBuffer(100),
txBuffer: newMessageBuffer(100),
}
for _, option := range options {
@ -28,7 +44,7 @@ func NewConnection(options ...func(*Connection)) *Connection {
return &connection
}
func WithWebsocket(ws *websocket.Conn) func(*Connection) {
func WithWebsocket(ws *gorillaws.Conn) func(*Connection) {
return func(c *Connection) {
c.ws = ws
}
@ -48,6 +64,10 @@ func WithDisconnectCallback(cb func()) func(*Connection) {
}
}
func (conn *Connection) SetForColor(color types.ChessColor) {
conn.forColor = color
}
func (conn *Connection) SetDisconnectCallback(cb func()) {
conn.disconnectCallback = cb
}
@ -56,53 +76,81 @@ func (conn *Connection) HasWebsocketConnection() bool {
return conn.ws != nil
}
func (conn *Connection) SetWebsocketConnection(ws *websocket.Conn) {
func (conn *Connection) readFromRxBuffer() {
for {
_, msg, err := conn.ws.ReadMessage()
if err != nil {
conn.logConnection("while reading from websocket: %w", "ERROR:", err.Error())
conn.Close("")
return
}
conn.rxBuffer.Insert(string(msg))
}
}
func (conn *Connection) writeTxBuffer() {
for {
msg := conn.txBuffer.Get()
if conn.ws == nil {
return
}
err := conn.ws.WriteMessage(gorillaws.TextMessage, []byte(msg))
if err != nil {
conn.logConnection("while writing to websocket: %w", "ERROR:", err.Error())
return
}
}
}
func (conn *Connection) SetWebsocketConnection(ws *gorillaws.Conn) {
if ws == nil {
conn.logConnection("ERROR: setting ws = null")
return
}
conn.ws = ws
select {
case conn.wsConnectionEstablished <- true:
default:
}
go conn.readFromRxBuffer()
go conn.writeTxBuffer()
go func() {
for {
_, msg, err := conn.ws.Read(conn.ctx)
if err != nil {
log.Println("while reading from websocket: %w", err)
if conn.disconnectCallback != nil {
conn.disconnectCallback()
}
return
}
conn.buffer.Insert(string(msg))
}
}()
defer conn.logConnection("websocket connection set")
}
func (conn *Connection) Write(msg []byte) error {
if conn.ws == nil { //if ws is not yet set, we wait for it
<-conn.wsConnectionEstablished
}
log.Printf("Writing message: %s", string(msg))
return conn.ws.Write(conn.ctx, websocket.MessageText, msg)
func (conn *Connection) Write(msg string) {
conn.logConnection("Writing message: ", string(msg))
conn.txBuffer.Insert(msg)
}
func (conn *Connection) Read() ([]byte, error) {
msg, err := conn.buffer.Get()
if err != nil {
conn.ws = nil
return nil, err // Tell game-handler that connection was lost
}
func (conn *Connection) Read() []byte {
msg := conn.rxBuffer.Get()
return []byte(msg), err
return []byte(msg)
}
func (conn *Connection) Close(msg string) {
conn.ws.Close(websocket.StatusCode(400), msg)
/* This is a hack, do not try this at home.
See more details at the definition of Connection */
conn.closingMutex.Lock()
defer conn.closingMutex.Unlock()
if conn == nil || conn.ws == nil {
return
}
conn.logConnection("closing websocket connection")
conn.ws.WriteMessage(gorillaws.TextMessage, []byte(msg))
conn.ws.Close()
conn.ws = nil
conn.txBuffer.Insert("we do this to make txBuffer.Get() return")
}
func (con *Connection) logConnection(v ...string) {
a := ""
for _, s := range v {
a += s + ", "
}
log.Println(a, "on connection ", con.ID, ", for color ", con.forColor.String())
}

52
go.mod
View File

@ -1,46 +1,46 @@
module mchess_server
go 1.21.1
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.5.0
github.com/samber/lo v1.39.0
github.com/stretchr/testify v1.8.4
github.com/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1
nhooyr.io/websocket v1.8.10
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/samber/lo v1.41.0
github.com/stretchr/testify v1.9.0
)
require github.com/benbjohnson/clock v1.3.5 // indirect
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/bytedance/sonic v1.11.9 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // 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
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gorilla/websocket v1.5.3
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.27.10 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
go.uber.org/ratelimit v0.3.1
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

213
go.sum
View File

@ -1,65 +1,57 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -67,126 +59,67 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.41.0 h1:vZEqVQNmX/ykONfla8tiW6cdoLZxb3+LCZ63B8z/2eE=
github.com/samber/lo v1.41.0/go.mod h1:w7R6fO7h2lrnx/s0bWcZ55vXJI89p5UPM6+kyDL373E=
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=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/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/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1 h1:j8whCiEmvLCXI3scVn+YnklCU8mwJ9ZJ4/DGAKqQbRE=
github.com/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1/go.mod h1:O5hBrCGqzfb+8WyY8ico2AyQau7XQwAfEQeEQ5/5V9E=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,4 +1,4 @@
package lobby_registry
package lobbies
import (
"mchess_server/chess"
@ -8,17 +8,15 @@ import (
)
type Lobby struct {
Uuid uuid.UUID
Game *chess.Game
PlayerJoined chan bool
Passphrase utils.Passphrase
Uuid uuid.UUID
Game *chess.Game
Passphrase utils.Passphrase
}
func NewEmptyLobbyWithUUID(uuid uuid.UUID) *Lobby {
return &Lobby{
Uuid: uuid,
Game: chess.NewGame(),
PlayerJoined: make(chan bool),
Uuid: uuid,
Game: chess.NewGame(),
}
}
@ -31,12 +29,16 @@ func newEmptyLobbyWithPassphrase() *Lobby {
func (l *Lobby) AddPlayerAndStartGameIfFull(player *chess.Player) {
l.Game.AddPlayersToGame(player)
if l.IsFull() {
if l.ContainsTwoPlayers() {
l.Game.StartHandling()
}
}
func (w *Lobby) IsFull() bool {
func (w *Lobby) AreBothPlayersConnected() bool {
return w.Game.AreBothPlayersConnected()
}
func (w *Lobby) ContainsTwoPlayers() bool {
return len(w.Game.GetPlayers()) == 2
}

View File

@ -1,4 +1,4 @@
package lobby_registry
package lobbies
import (
"mchess_server/utils"
@ -7,7 +7,7 @@ import (
)
type LobbyRegistry struct {
lobbies map[uuid.UUID]*Lobby
lobbies map[utils.Passphrase]*Lobby
}
var instance *LobbyRegistry
@ -21,7 +21,7 @@ func GetLobbyRegistry() *LobbyRegistry {
}
func newLobbyRegistry() *LobbyRegistry {
return &LobbyRegistry{lobbies: make(map[uuid.UUID]*Lobby)}
return &LobbyRegistry{lobbies: make(map[utils.Passphrase]*Lobby)}
}
func (r *LobbyRegistry) CreateNewPrivateLobby() *Lobby {
@ -32,7 +32,7 @@ func (r *LobbyRegistry) CreateNewPrivateLobby() *Lobby {
func (r *LobbyRegistry) GetLobbyForPlayer() *Lobby {
for _, lobby := range r.lobbies {
if !lobby.IsFull() {
if !lobby.ContainsTwoPlayers() {
return lobby
}
}
@ -42,10 +42,6 @@ func (r *LobbyRegistry) GetLobbyForPlayer() *Lobby {
return newLobby
}
func (r *LobbyRegistry) GetLobbyByUUID(uuid uuid.UUID) *Lobby {
return r.lobbies[uuid]
}
func (r *LobbyRegistry) GetLobbyByPassphrase(p utils.Passphrase) *Lobby {
for _, lobby := range r.lobbies {
if lobby.Passphrase == p {
@ -56,6 +52,6 @@ func (r *LobbyRegistry) GetLobbyByPassphrase(p utils.Passphrase) *Lobby {
}
func (r *LobbyRegistry) addNewLobby(lobby *Lobby) uuid.UUID {
r.lobbies[lobby.Uuid] = lobby
r.lobbies[lobby.Passphrase] = lobby
return lobby.Uuid
}

38
lobbies/usher.go Normal file
View File

@ -0,0 +1,38 @@
package lobbies
import (
"mchess_server/chess"
"mchess_server/utils"
)
type Usher struct {
}
var usherInstance *Usher
func newUsher() *Usher {
return &Usher{}
}
func GetUsher() *Usher {
if usherInstance == nil {
usherInstance = newUsher()
}
return usherInstance
}
func (u *Usher) WelcomeNewPlayer(player *chess.Player) *Lobby {
return GetLobbyRegistry().GetLobbyForPlayer()
}
func (u *Usher) CreateNewPrivateLobby(player *chess.Player) *Lobby {
return GetLobbyRegistry().CreateNewPrivateLobby()
}
func (u *Usher) FindExistingPrivateLobby(p utils.Passphrase) *Lobby {
return GetLobbyRegistry().GetLobbyByPassphrase(p)
}
func (u *Usher) AddPlayerToLobbyAndStartGameIfFull(player *chess.Player, lobby *Lobby) {
lobby.AddPlayerAndStartGameIfFull(player)
}

162
main.go
View File

@ -1,30 +1,19 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"mchess_server/api"
"mchess_server/chess"
lobbies "mchess_server/lobby_registry"
"mchess_server/usher"
"mchess_server/utils"
"net/http"
"sync"
"mchess_server/api/handler"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"nhooyr.io/websocket"
)
var cert_path = "/etc/letsencrypt/live/chess.sw-gross.de/"
var cert_file = cert_path + "fullchain.pem"
var key_file = cert_path + "privkey.pem"
var mut sync.Mutex
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
var debugMode bool
debugModeLong := flag.Bool("debug", false, "activates debug mode")
@ -35,10 +24,13 @@ func main() {
}
router := gin.Default()
router.GET("/api/random", registerForRandomGame)
router.GET("/api/hostPrivate", hostPrivateGame)
router.POST("/api/joinPrivate", joinPrivateGame)
router.GET("/api/ws", registerWebSocketConnection)
router.GET("/api/hostPrivate", handler.HostGameHandler)
router.POST("/api/joinPrivate", handler.JoinPrivateGame)
router.GET("/api/ws", handler.RegisterWebSocketConnection)
router.GET("/api/getLobbyForPassphrase/:phrase", handler.GetLobbyForPassphraseHandler)
router.GET("/api/registerWsForGame/:id", handler.ConnectWsForGame)
router.POST("/api/joinGame/:id", handler.JoinGameHandler)
if debugMode {
log.Println("Starting service WITHOUT TLS")
@ -50,139 +42,3 @@ func main() {
log.Fatal(router.RunTLS("chess.sw-gross.de:9999", cert_file, key_file))
}
}
func registerForRandomGame(c *gin.Context) {
player := chess.NewPlayer(uuid.New())
usher := usher.GetUsher()
mut.Lock()
defer mut.Unlock()
lobby := usher.WelcomeNewPlayer(player)
usher.AddPlayerToLobbyAndStartGameIfFull(player, lobby)
info := api.PlayerInfo{
PlayerID: &player.Uuid,
LobbyID: &lobby.Uuid,
}
log.Println("responding with info ", info)
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, info)
}
func hostPrivateGame(c *gin.Context) {
player := chess.NewPlayer(uuid.New())
u := usher.GetUsher()
mut.Lock()
defer mut.Unlock()
lobby := u.CreateNewPrivateLobby(player)
u.AddPlayerToLobbyAndStartGameIfFull(player, lobby)
passphrase := lobby.Passphrase.String()
info := api.PlayerInfo{
PlayerID: &player.Uuid,
LobbyID: &lobby.Uuid,
Passphrase: &passphrase,
}
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, info)
}
func joinPrivateGame(c *gin.Context) {
req := api.PlayerInfo{}
log.Println(c.Request.Body)
err := c.ShouldBindJSON(&req)
if err != nil || req.Passphrase == nil || *req.Passphrase == "" {
c.IndentedJSON(http.StatusNotFound, req)
}
u := usher.GetUsher()
if req.Passphrase != nil &&
*req.Passphrase != "" &&
req.PlayerID != nil &&
req.LobbyID != nil { //is reconnect
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*req.Passphrase))
_, found := lobby.GetPlayerByUUID(*req.PlayerID)
if found {
c.IndentedJSON(
http.StatusOK,
api.PlayerInfo{
PlayerID: req.PlayerID,
LobbyID: req.LobbyID,
Passphrase: req.Passphrase,
})
return
} else {
c.IndentedJSON(http.StatusNotFound, req)
}
}
player := chess.NewPlayer(uuid.New())
mut.Lock()
defer mut.Unlock()
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*req.Passphrase))
if lobby != nil {
u.AddPlayerToLobbyAndStartGameIfFull(player, lobby)
} else {
c.IndentedJSON(http.StatusNotFound, req)
return
}
info := api.PlayerInfo{
PlayerID: &player.Uuid,
LobbyID: &lobby.Uuid,
Passphrase: req.Passphrase,
}
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, info)
}
func registerWebSocketConnection(c *gin.Context) {
webSocketConn, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{OriginPatterns: []string{"chess.sw-gross.de", "localhost:*"}})
if err != nil {
log.Println(err)
return
}
go waitForAndHandlePlayerID(c, webSocketConn)
}
func waitForAndHandlePlayerID(ctx context.Context, conn *websocket.Conn) {
msgType, msg, err := conn.Read(ctx)
if err != nil {
errorMessage := fmt.Sprintf("Reading from websocket connection did not work: %s", err)
log.Println(errorMessage)
conn.Close(websocket.StatusCode(400), errorMessage)
return
}
log.Println("read from websocket endpoint: ", msgType, string(msg), err)
var info api.PlayerInfo
err = json.Unmarshal(msg, &info)
if err != nil {
errorMessage := fmt.Sprintf("Unmarshaling message did not work: %s", err)
log.Println(errorMessage)
conn.Close(websocket.StatusCode(400), errorMessage)
return
}
lobby := lobbies.GetLobbyRegistry().GetLobbyByUUID(*info.LobbyID)
if lobby == nil {
conn.Close(websocket.StatusCode(400), "lobby not found")
return
}
player, found := lobby.GetPlayerByUUID(*info.PlayerID)
if !found {
conn.Close(websocket.StatusCode(400), "player not found")
return
}
if player.Conn.HasWebsocketConnection() {
player.Conn.Close("closing existing connection")
}
lobby.Game.SetWebsocketConnectionFor(ctx, player, conn)
log.Println("player after setting connection: ", player)
}

View File

@ -3,8 +3,9 @@ package types
type ChessColor string
const (
White ChessColor = "white"
Black ChessColor = "black"
NoColor ChessColor = "no_color"
White ChessColor = "white"
Black ChessColor = "black"
)
func (c ChessColor) Opposite() ChessColor {
@ -15,6 +16,10 @@ func (c ChessColor) Opposite() ChessColor {
}
}
func (c ChessColor) String() string {
return string(c)
}
type AdditionalState struct {
BlackKingMoved bool
WhiteKingMoved bool

View File

@ -1,45 +0,0 @@
package usher
import (
"mchess_server/chess"
lobbies "mchess_server/lobby_registry"
"mchess_server/utils"
)
type Usher struct {
}
var instance *Usher
func newUsher() *Usher {
return &Usher{}
}
func GetUsher() *Usher {
if instance == nil {
instance = newUsher()
}
return instance
}
func (u *Usher) WelcomeNewPlayer(player *chess.Player) *lobbies.Lobby {
lobby := lobbies.GetLobbyRegistry().GetLobbyForPlayer()
return lobby
}
func (u *Usher) CreateNewPrivateLobby(player *chess.Player) *lobbies.Lobby {
lobby := lobbies.GetLobbyRegistry().CreateNewPrivateLobby()
return lobby
}
func (u *Usher) FindExistingPrivateLobby(p utils.Passphrase) *lobbies.Lobby {
lobby := lobbies.GetLobbyRegistry().GetLobbyByPassphrase(p)
if lobby == nil || lobby.IsFull() {
return nil
}
return lobby
}
func (u *Usher) AddPlayerToLobbyAndStartGameIfFull(player *chess.Player, lobby *lobbies.Lobby) {
lobby.AddPlayerAndStartGameIfFull(player)
}

60458
utils/clean-words.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,101 +2,56 @@ package utils
import (
"strings"
"github.com/tjarratt/babble"
"unicode"
)
type Passphrase string
func NewPassphrase() Passphrase {
var phrase string
var retries int
var word string
var words = 2
var words = 3
//TODO make sure passphrases are unique
for words > 0 {
retries = 20
for {
word = getCleanWord()
if isAccecpable(word) {
phrase = phrase + word + " "
break
}
if retries == 1 { //this is our last try, we take any word
phrase = phrase + word + " "
}
retries -= 1
}
word = getCleanWord()
phrase = phrase + word + " "
words -= 1
}
return Passphrase(strings.TrimSpace(phrase))
}
func NewPassphraseFromString(s string) Passphrase {
return Passphrase(s)
}
func (p Passphrase) AsURLParam() string {
var result string
phraseAsString := p.String()
segments := strings.Split(phraseAsString, " ")
for _, segment := range segments {
runes := []rune(segment)
runes[0] = unicode.ToUpper(runes[0])
result += string(runes)
}
return result
}
func ConvertToPassphraseWithSpaces(s string) Passphrase {
result := ""
for _, rune := range s {
if unicode.IsUpper(rune) {
result += " "
}
result += strings.ToLower(string(rune))
}
return NewPassphraseFromString(strings.Trim(result, " "))
}
func (p Passphrase) String() string {
return string(p)
}
func isAccecpable(s string) bool {
l := len(s)
if l > 8 || l < 3 {
return false
}
for _, rune := range s {
if !isEnglishLetter(rune) {
return false
}
}
return true
}
func isProfanity(s string) bool {
contains := []string{
"nigg",
"fag",
"ass",
"bitch",
"rape",
"ass",
"scrot",
"rect",
}
startsWith := []string{
"spic",
"chin",
"cunt",
}
for _, word := range contains {
if strings.Contains(s, word) {
return true
}
}
for _, word := range startsWith {
if strings.HasPrefix(s, word) {
return true
}
}
return false
}
func isEnglishLetter(r rune) bool {
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') {
return false
}
return true
}
func getCleanWord() string {
var word string
babbler := babble.NewBabbler()
babbler.Count = 1
for {
word = babbler.Babble()
if !isProfanity(word) {
return word
}
}
}

36
utils/passphrase_test.go Normal file
View File

@ -0,0 +1,36 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_onlyUniqueWords(t *testing.T) {
wordsDistribution := make(map[string]int)
for _, word := range CleanWords {
wordsDistribution[word] += 1
}
for _, v := range wordsDistribution {
assert.Equal(t, 1, v)
}
assert.Equal(t, numberOfCleanWords, len(wordsDistribution))
}
func Test_Passphrase_AsURLParam(t *testing.T) {
phrase := NewPassphraseFromString("this is a Test phrase")
asParams := phrase.AsURLParam()
assert.Equal(t, "ThisIsATestPhrase", asParams)
}
func Test_Passphrase_ConvertToPassphraseWithSpaces(t *testing.T) {
fromURL := "ThisIsATestPhraseWithManyWords"
phrase := ConvertToPassphraseWithSpaces(fromURL)
assert.Equal(t, NewPassphraseFromString("this is a test phrase with many words"), phrase)
}