Compare commits

...

29 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
25 changed files with 726 additions and 1923 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()
}

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

@ -296,3 +296,34 @@ func Test_Board_BlackCastlesRight_NoRook(t *testing.T) {
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

@ -8,7 +8,7 @@ import (
"mchess_server/types"
"github.com/google/uuid"
"nhooyr.io/websocket"
gorillaws "github.com/gorilla/websocket"
)
type Game struct {
@ -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()
@ -99,13 +99,13 @@ 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:
@ -117,7 +117,7 @@ func (game *Game) Handle() {
log.Println("Error marshalling 'colorDetermined' message for player 1", err)
return
}
game.currentTurnPlayer.writeMessage(invalidMoveMessage)
game.currentTurnPlayer.writeMessage(string(invalidMoveMessage))
game.gameState = PlayerToMove
continue
}
@ -157,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")
}
@ -173,21 +181,22 @@ 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
}
@ -211,6 +220,10 @@ func (game Game) broadcastGameEnd(reason GameEndedReason) error {
func (game *Game) playerDisconnected(p *Player) {
}
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) 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

@ -30,6 +30,10 @@ func (k King) AfterMoveAction(board *Board, fromSquare types.Coordinate) {
}
func (k King) HandleCastling(board *Board, move types.Move) (bool, Violation) {
if !k.isMoveCastlingMove(board, move) {
return false, ""
}
if k.hadMovedBefore(board) {
return false, ""
}
@ -66,6 +70,28 @@ const (
CastlingLeft CastlingDirection = "left"
)
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

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,11 +105,8 @@ 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
}
@ -117,26 +120,20 @@ func (p *Player) SendGameEnded(reason GameEndedReason) error {
log.Println("Error while marshalling: ", err)
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) 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
}
@ -148,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

@ -11,6 +11,7 @@ var (
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 // TODO: 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())
}

41
go.mod
View File

@ -3,41 +3,44 @@ module mchess_server
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/samber/lo v1.39.0
github.com/samber/lo v1.41.0
github.com/stretchr/testify v1.9.0
nhooyr.io/websocket v1.8.11
)
require github.com/benbjohnson/clock v1.3.5 // indirect
require (
github.com/bytedance/sonic v1.11.3 // 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.19.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.7 // 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/pelletier/go-toml/v2 v2.2.0 // 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.7.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.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
)

101
go.sum
View File

@ -1,43 +1,54 @@
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.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
github.com/bytedance/sonic v1.11.3/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/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.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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/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.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.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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
@ -48,12 +59,14 @@ 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/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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.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=
@ -70,29 +83,43 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
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.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.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/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.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
nhooyr.io/websocket v1.8.11/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

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

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ package utils
import (
"strings"
"unicode"
)
type Passphrase string
@ -20,6 +21,37 @@ func NewPassphrase() Passphrase {
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)
}

View File

@ -1,16 +1,36 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Passphrase(t *testing.T) {
for run := range 100 {
phrase := NewPassphrase()
fmt.Println(run, ": ", phrase)
assert.NotEmpty(t, phrase)
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)
}