From c4b9f9287fb3c755a0f710ee1c1dde78313e9df4 Mon Sep 17 00:00:00 2001 From: minie4 Date: Thu, 6 Mar 2025 10:14:21 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20=F0=9F=9A=A7=20Start=20working?= =?UTF-8?q?=20on=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 3 + .gitignore | 6 + backend/.gitignore | 1 + backend/api/api.go | 153 +++++++++++++++++ backend/api/websocket.go | 260 +++++++++++++++++++++++++++++ backend/db/db.go | 82 +++++++++ backend/db/serializable_types.go | 71 ++++++++ backend/decks/classic.go | 180 ++++++++++++++++++++ backend/decks/decks.go | 31 ++++ backend/game/game.go | 209 +++++++++++++++++++++++ backend/go.mod | 66 ++++++++ backend/go.sum | 189 +++++++++++++++++++++ backend/main.go | 39 +++++ backend/types/types.go | 142 ++++++++++++++++ backend/types/websocket_packets.go | 113 +++++++++++++ backend/utils/utils.go | 38 +++++ docker-compose.dev.yml | 8 + 17 files changed, 1591 insertions(+) create mode 100644 .env.sample create mode 100644 backend/.gitignore create mode 100644 backend/api/api.go create mode 100644 backend/api/websocket.go create mode 100644 backend/db/db.go create mode 100644 backend/db/serializable_types.go create mode 100644 backend/decks/classic.go create mode 100644 backend/decks/decks.go create mode 100644 backend/game/game.go create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/main.go create mode 100644 backend/types/types.go create mode 100644 backend/types/websocket_packets.go create mode 100644 backend/utils/utils.go create mode 100644 docker-compose.dev.yml diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..f5b314d --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +LISTEN_HOST=0.0.0.0 +LISTEN_PORT=3000 +MONGO_URI="mongodb://127.0.0.1:27017/" \ No newline at end of file diff --git a/.gitignore b/.gitignore index aee7f33..e2a1414 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ *.ntvs* *.njsproj *.sln + +# Secrets +.env + +# MongoDB docker volume +data/ \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..3320515 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +HexDeck \ No newline at end of file diff --git a/backend/api/api.go b/backend/api/api.go new file mode 100644 index 0000000..f273bd0 --- /dev/null +++ b/backend/api/api.go @@ -0,0 +1,153 @@ +package api + +import ( + "fmt" + "log" + "log/slog" + "net/http" + "strconv" + + "github.com/HexCardGames/HexDeck/db" + "github.com/HexCardGames/HexDeck/game" + "github.com/HexCardGames/HexDeck/types" + "github.com/HexCardGames/HexDeck/utils" + "github.com/gin-gonic/gin" +) + +type ErrorReply struct { + StatusCode string + Message string +} +type StatsReply struct { + TotalGamesPlayed int + RunningGames int + OnlinePlayerCount int +} +type ImprintReply struct { + Content string +} +type CreateRoomReply struct { + JoinCode string +} +type JoinRoomRequest struct { + JoinCode string + Username string +} +type LeaveRoomRequest struct { + SessionToken string +} + +func InitApi() { + server := gin.Default() + server.SetTrustedProxies(nil) + + server.GET("/api/stats", func(c *gin.Context) { + stats := game.CalculateStats() + c.JSON(http.StatusOK, StatsReply{ + TotalGamesPlayed: db.Conn.QueryGlobalStats().GamesPlayed, + RunningGames: stats.RunningGames, + OnlinePlayerCount: stats.OnlinePlayerCount, + }) + }) + server.GET("/api/imprint", func(c *gin.Context) { + // TODO: Implement imprint endpoint + c.JSON(http.StatusOK, ImprintReply{ + Content: "Not implemented yet", + }) + }) + + server.POST("/api/room/create", func(c *gin.Context) { + request := JoinRoomRequest{} + c.BindJSON(&request) + room := game.CreateRoom() + player := game.JoinRoom(room, request.Username) + player.SetPermissionBit(types.PermissionHost) + slog.Debug("New room created", "username", player.Username, "sessionToken", player.SessionToken, "roomId", room.RoomId.Hex()) + c.JSON(http.StatusOK, player) + }) + + server.POST("/api/room/join", func(c *gin.Context) { + request := JoinRoomRequest{} + c.BindJSON(&request) + room := game.FindRoomByJoinCode(request.JoinCode) + if room == nil { + slog.Debug("Client tried joining room using an invalid joinCode", "joinCode", request.JoinCode) + c.JSON(http.StatusBadRequest, ErrorReply{ + StatusCode: "invalid_join_code", + Message: "No valid joinCode was provided", + }) + return + } + if room.GameState != types.StateLobby { + slog.Debug("Client tried joining room not in lobby state", "joinCode", request.JoinCode) + c.JSON(http.StatusBadRequest, ErrorReply{ + StatusCode: "game_already_running", + Message: "You cannot join this room as the game has already started", + }) + return + } + player := game.JoinRoom(room, request.Username) + slog.Debug("New session created", "username", player.Username, "sessionToken", player.SessionToken, "roomId", room.RoomId.Hex(), "joinCode", request.JoinCode) + c.JSON(http.StatusOK, player) + }) + + server.GET("/api/check/session", func(c *gin.Context) { + sessionToken := c.Query("sessionToken") + if sessionToken == "" { + c.JSON(http.StatusBadRequest, ErrorReply{ + StatusCode: "missing_parameter", + Message: "Parameter sessionToken is missing", + }) + } + _, player := game.FindSession(sessionToken) + if player == nil { + c.Status(401) + } else { + c.Status(200) + } + }) + + server.GET("/api/check/joinCode", func(c *gin.Context) { + joinCode := c.Query("JoinCode") + if joinCode == "" { + c.JSON(http.StatusBadRequest, ErrorReply{ + StatusCode: "missing_parameter", + Message: "Parameter JoinCode is missing", + }) + } + room := game.FindRoomByJoinCode(joinCode) + if room == nil { + c.Status(401) + } else { + c.Status(200) + } + }) + + server.POST("/api/room/leave", func(c *gin.Context) { + request := LeaveRoomRequest{} + c.BindJSON(&request) + room, player := game.FindSession(request.SessionToken) + if player == nil { + c.JSON(http.StatusBadRequest, ErrorReply{ + StatusCode: "invalid_session", + Message: "No user was found with the provided sessionToken", + }) + return + } + room.RemovePlayer(*player) + game.OnRoomUpdate(room) + c.Status(http.StatusOK) + }) + + // Handle WebSocket connections using Socket.io + wsHandler := initWS() + server.Any("/socket.io/", gin.WrapH(wsHandler)) + + listenHost := utils.Getenv("LISTEN_HOST", "0.0.0.0") + listenPort, err := strconv.Atoi(utils.Getenv("LISTEN_PORT", "3000")) + if err != nil { + log.Fatal("Value of variable PORT is not a valid integer!") + } + slog.Info(fmt.Sprintf("HexDeck server listening on http://%s:%d", listenHost, listenPort)) + server.Run(fmt.Sprintf("%s:%d", listenHost, listenPort)) +} diff --git a/backend/api/websocket.go b/backend/api/websocket.go new file mode 100644 index 0000000..60b739a --- /dev/null +++ b/backend/api/websocket.go @@ -0,0 +1,260 @@ +package api + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/HexCardGames/HexDeck/game" + "github.com/HexCardGames/HexDeck/types" + "github.com/zishang520/socket.io/v2/socket" + socketio "github.com/zishang520/socket.io/v2/socket" +) + +var io *socketio.Server + +func initWS() http.Handler { + io = socketio.NewServer(nil, nil) + + io.On("connection", func(clients ...any) { + client := clients[0].(*socket.Socket) + remoteAddr := client.Request().Request().RemoteAddr + + sessionToken, exists := client.Request().Query().Get("sessionToken") + room, player := game.FindSession(sessionToken) + if !exists || player == nil { + slog.Debug("New WebSocket connection from didn't provide a valid sessionToken -> disconnecting", "remoteAddress", remoteAddr, "sessionToken", sessionToken) + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "invalid_session", + Message: "No valid sessionToken was provided", + }) + client.Disconnect(true) + return + } + if player.Connection.IsConnected && player.Connection.Socket != nil { + slog.Debug("User already connected to WebSocket -> disconnecting old socket", "remoteAddress", remoteAddr, "sessionToken", sessionToken) + player.Connection.Socket.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "connection_from_different_socket", + Message: "User connected from a different socket", + }) + player.Connection.Socket.Disconnect(true) + } + player.Connection.Socket = client + player.Connection.IsConnected = true + player.ResetInactivity() + slog.Debug("New WebSocket connection", "username", player.Username, "remoteAddress", remoteAddr, "playerId", player.PlayerId, "sessionToken", sessionToken, "roomId", room.RoomId.Hex()) + game.OnRoomUpdate(room) + + onPlayerJoin(client, room, player) + }) + + return io.ServeHandler(nil) +} + +func unpackData(datas []any, target interface{}) bool { + if len(datas) < 1 { + slog.Warn("Unexpected length of WebSocket data; ignoring message") + return false + } + request, _ := datas[0].(string) + ok := json.Unmarshal([]byte(request), &target) + return ok != nil +} + +func verifyPlayerIsActivePlayer(room *types.Room, target *types.Player) bool { + if room.GameState != types.StateRunning { + target.Connection.Socket.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "game_not_running", + Message: "The game is not running", + }) + return false + } + + if !room.CardDeck.IsPlayerActive(target) { + target.Connection.Socket.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "player_not_active", + Message: "You can't execute this action while you are not the active player", + }) + return false + } + return true +} + +func onPlayerJoin(client *socket.Socket, room *types.Room, player *types.Player) { + client.On("disconnect", func(...any) { + player.Connection.IsConnected = false + player.Connection.Socket = nil + slog.Debug("Player disconnected from WebSocket", "username", player.Username, "remoteAddress", client.Conn().RemoteAddress(), "sessionToken", player.SessionToken, "roomId", room.RoomId.Hex()) + game.OnRoomUpdate(room) + }) + + client.On("UpdatePlayer", func(datas ...any) { + updatePlayerRequest := types.C2S_UpdatePlayer{} + unpackData(datas, &updatePlayerRequest) + if updatePlayerRequest.PlayerId != player.PlayerId && !player.HasPermissionBit(types.PermissionHost) { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "insufficient_permission", + Message: "You can't update other users unless you are host", + }) + return + } + targetPlayer := room.FindPlayer(updatePlayerRequest.PlayerId) + if targetPlayer == nil { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "invalid_player", + Message: "No player with the requested playerId was found", + }) + return + } + slog.Debug("Updating player data", "roomId", room.RoomId, "playerId", updatePlayerRequest.PlayerId, "username", targetPlayer.Username, "request", updatePlayerRequest) + + if updatePlayerRequest.Username != nil { + if room.IsUsernameAvailable(*updatePlayerRequest.Username) { + targetPlayer.Username = *updatePlayerRequest.Username + } else { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "username_taken", + Message: "The requested username is not available", + }) + } + } + if updatePlayerRequest.Permissions != nil { + targetPlayer.Permissions = *updatePlayerRequest.Permissions + } + + game.OnRoomUpdate(room) + }) + + client.On("KickPlayer", func(datas ...any) { + kickPlayerRequest := types.C2S_KickPlayer{} + unpackData(datas, &kickPlayerRequest) + if !player.HasPermissionBit(types.PermissionHost) { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "insufficient_permission", + Message: "You can't update other users unless you are host", + }) + return + } + targetPlayer := room.FindPlayer(kickPlayerRequest.PlayerId) + if targetPlayer == nil { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "invalid_player", + Message: "No player with the requested playerId was found", + }) + return + } + + if room.RemovePlayer(*targetPlayer) { + slog.Debug("Player was kicked from room", "playerId", player.PlayerId, "targetPlayerId", kickPlayerRequest.PlayerId, "roomId", room.RoomId) + if targetPlayer.Connection.IsConnected && targetPlayer.Connection.Socket != nil { + targetPlayer.Connection.Socket.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "player_kicked", + Message: "You were kicked from the room", + }) + } + } + game.OnRoomUpdate(room) + }) + + client.On("StartGame", func(datas ...any) { + if !player.HasPermissionBit(types.PermissionHost) { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "insufficient_permission", + Message: "You can't start the game unless you are host", + }) + return + } + if room.GameState != types.StateLobby { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "game_already_started", + Message: "The game has already started", + }) + return + } + game.StartGame(room) + }) + + client.On("DrawCard", func(datas ...any) { + if !verifyPlayerIsActivePlayer(room, player) { + return + } + card := room.CardDeck.DrawCard() + if card == nil { + // TODO: Handle empty card deck + return + } + game.OnPlayerStateUpdate(room, player, false) + }) + + client.On("PlayCard", func(datas ...any) { + if !verifyPlayerIsActivePlayer(room, player) { + return + } + + updatePlayerRequest := types.C2S_PlayCard{} + unpackData(datas, &updatePlayerRequest) + if updatePlayerRequest.CardIndex == nil { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "missing_parameter", + Message: "CardIndex parameter is missing", + }) + return + } + if *updatePlayerRequest.CardIndex < 0 || *updatePlayerRequest.CardIndex >= len(player.Cards) { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "invalid_card_index", + Message: "Provided CardIndex is out of bounds", + }) + return + } + card := player.Cards[*updatePlayerRequest.CardIndex] + if !room.CardDeck.PlayCard(card) { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "card_not_playable", + Message: "You can't play this card now", + }) + return + } + player.Cards = append(player.Cards[:*updatePlayerRequest.CardIndex], player.Cards[*updatePlayerRequest.CardIndex+1:]...) + game.OnPlayCard(room, player, *updatePlayerRequest.CardIndex, card) + + if len(player.Cards) == 0 { + room.Winner = &player.PlayerId + game.UpdateGameState(room, types.StateEnded) + } + }) + + client.On("UpdatePlayedCard", func(datas ...any) { + if !verifyPlayerIsActivePlayer(room, player) { + return + } + + updatePlayerRequest := types.C2S_UpdatePlayedCard{} + unpackData(datas, &updatePlayerRequest) + card := room.CardDeck.UpdatePlayedCard(updatePlayerRequest.CardData) + if card == nil { + client.Emit("Status", types.S2C_Status{ + IsError: true, + StatusCode: "card_not_updatable", + Message: "You can't update this card now", + }) + return + } + game.OnPlayedCardUpdate(room, player, card) + }) +} diff --git a/backend/db/db.go b/backend/db/db.go new file mode 100644 index 0000000..93356aa --- /dev/null +++ b/backend/db/db.go @@ -0,0 +1,82 @@ +package db + +import ( + "context" + "fmt" + "log/slog" + + "github.com/HexCardGames/HexDeck/types" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +type DatabaseConnection struct { + client *mongo.Client +} + +type GlobalStatsCollection struct { + GamesPlayed int `bson:"games_played"` +} + +func (conn *DatabaseConnection) QueryRunningRooms() []*types.Room { + res, err := conn.client.Database("hexdeck").Collection("games").Find(context.TODO(), bson.D{{Key: "gamestate", Value: bson.D{{Key: "$ne", Value: types.StateEnded}}}}) + if err != nil { + slog.Error("Loading rooms from database failed", "error", err) + return make([]*types.Room, 0) + } + + var serializableRooms []SerializableRoom + err = res.All(context.TODO(), &serializableRooms) + if err != nil { + slog.Error("Decoding rooms from database failed", "error", err) + return make([]*types.Room, 0) + } + var rooms []*types.Room = make([]*types.Room, len(serializableRooms)) + for i, serializableRoom := range serializableRooms { + room := serializableRoom.ToRoom() + rooms[i] = room + } + return rooms +} + +func (conn *DatabaseConnection) InsertRoom(room *types.Room) { + _, err := conn.client.Database("hexdeck").Collection("games").InsertOne(context.TODO(), room) + if err != nil { + slog.Error("Error while inserting room into database", "error", err) + } +} + +func (conn *DatabaseConnection) UpdateRoom(room *types.Room) { + result, err := conn.client.Database("hexdeck").Collection("games").UpdateByID(context.TODO(), room.RoomId, bson.D{{Key: "$set", Value: room}}) + if err != nil { + slog.Error("Error while updating room in database", "error", err) + } + if result.MatchedCount < 1 { + slog.Warn(fmt.Sprintf("No collections were found while trying to update room data for room '%s'", room.RoomId)) + } +} + +func (conn *DatabaseConnection) IncrementGamesPlayed() { + conn.client.Database("hexdeck").Collection("global_stats").UpdateOne(context.TODO(), bson.D{}, bson.D{ + {Key: "$inc", Value: bson.D{{Key: "games_played", Value: 1}}}, + }, options.UpdateOne().SetUpsert(true)) +} + +func (conn *DatabaseConnection) QueryGlobalStats() GlobalStatsCollection { + res := conn.client.Database("hexdeck").Collection("global_stats").FindOne(context.TODO(), bson.D{}) + var stats GlobalStatsCollection + res.Decode(&stats) + return stats +} + +func CreateDBConnection(uri string) DatabaseConnection { + client, _ := mongo.Connect(options.Client().ApplyURI(uri)) + return DatabaseConnection{client} +} + +var Conn DatabaseConnection + +func InitDB(uri string) { + Conn = CreateDBConnection(uri) +} diff --git a/backend/db/serializable_types.go b/backend/db/serializable_types.go new file mode 100644 index 0000000..a44cdca --- /dev/null +++ b/backend/db/serializable_types.go @@ -0,0 +1,71 @@ +package db + +import ( + "sync" + + "github.com/HexCardGames/HexDeck/decks" + "github.com/HexCardGames/HexDeck/types" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type SerializablePlayer struct { + PlayerId bson.ObjectID + SessionToken string + Username string + Permissions int + Cards []bson.D +} + +func (serializable *SerializablePlayer) ToPlayer(cardDeckId int) types.Player { + cards := make([]types.Card, len(serializable.Cards)) + for i, card := range serializable.Cards { + cards[i] = decks.CardFromInterface(cardDeckId, card) + } + player := types.Player{ + PlayerId: serializable.PlayerId, + SessionToken: serializable.SessionToken, + Username: serializable.Username, + Permissions: serializable.Permissions, + Connection: types.WebsocketConnection{IsConnected: false}, + Cards: cards, + } + player.ResetInactivity() + return player +} + +type SerializableRoom struct { + RoomId bson.ObjectID `bson:"_id"` + JoinCode string + GameState types.GameState + GameOptions types.GameOptions + CardDeckId int + CardDeck bson.D + Players []SerializablePlayer + OwnerId bson.ObjectID + MoveTimeout int + Winner *bson.ObjectID +} + +func (serializable SerializableRoom) ToRoom() *types.Room { + players := make([]*types.Player, len(serializable.Players)) + for i, serializablePlayer := range serializable.Players { + player := serializablePlayer.ToPlayer(serializable.CardDeckId) + players[i] = &player + } + cardDeck := decks.DeckFromInterface(serializable.CardDeckId, serializable.CardDeck) + room := &types.Room{ + RoomId: serializable.RoomId, + JoinCode: serializable.JoinCode, + GameState: serializable.GameState, + GameOptions: serializable.GameOptions, + CardDeckId: serializable.CardDeckId, + CardDeck: cardDeck, + Players: players, + PlayersMutex: &sync.Mutex{}, + OwnerId: serializable.OwnerId, + MoveTimeout: serializable.MoveTimeout, + Winner: serializable.Winner, + } + cardDeck.SetRoom(room) + return room +} diff --git a/backend/decks/classic.go b/backend/decks/classic.go new file mode 100644 index 0000000..353d1b7 --- /dev/null +++ b/backend/decks/classic.go @@ -0,0 +1,180 @@ +package decks + +import ( + "strconv" + + "github.com/HexCardGames/HexDeck/types" + "github.com/HexCardGames/HexDeck/utils" +) + +type Classic struct { + room *types.Room + CardsPlayed []*ClassicCard + CardsRemaining []*ClassicCard + DirectionReversed bool + ActivePlayer int +} + +var ClassicColors = []string{"red", "yellow", "blue", "green"} + +func (deck *Classic) Init(room *types.Room) { + deck.room = room + deck.DirectionReversed = false + deck.ActivePlayer = 0 + cards := make([]*ClassicCard, 108) + offset := 0 + for i := 0; i < 4; i++ { + color := ClassicColors[i] + for j := 0; j < 19; j++ { + cards[offset] = &ClassicCard{ + Symbol: strconv.Itoa((j + 1) % 10), + Color: color, + } + offset += 1 + } + for j := 0; j < 2; j++ { + cards[offset] = &ClassicCard{Symbol: "action:skip", Color: color} + cards[offset+1] = &ClassicCard{Symbol: "action:reverse", Color: color} + cards[offset+2] = &ClassicCard{Symbol: "action:draw_2", Color: color} + offset += 3 + } + cards[offset] = &ClassicCard{Symbol: "action:wildcard", Color: "black"} + cards[offset+1] = &ClassicCard{Symbol: "action:draw_4", Color: "black"} + offset += 2 + } + utils.ShuffleSlice(&cards) + deck.CardsRemaining = cards +} + +func (deck *Classic) SetRoom(room *types.Room) { + deck.room = room +} + +func (deck *Classic) IsEmpty() bool { + return len(deck.CardsRemaining) == 0 +} + +func (deck *Classic) getTopCard() *ClassicCard { + if len(deck.CardsPlayed) == 0 { + return nil + } + return deck.CardsPlayed[len(deck.CardsPlayed)-1] +} + +func (deck *Classic) GetTopCard() types.Card { + return deck.getTopCard() +} + +func (deck *Classic) drawCard(player *types.Player) types.Card { + if deck.IsEmpty() { + return nil + } + + card := deck.CardsRemaining[0] + deck.CardsRemaining = deck.CardsRemaining[1:] + player.Cards = append(player.Cards, card) + return card +} + +func (deck *Classic) getActivePlayer() int { + return utils.Mod(deck.ActivePlayer, len(deck.room.Players)) +} + +func (deck *Classic) DrawCard() types.Card { + // Can't draw another card before wildcard color is selected + topCard := deck.getTopCard() + if topCard != nil && topCard.Color == "black" { + return nil + } + + card := deck.drawCard(deck.room.Players[deck.getActivePlayer()]) + deck.nextPlayer() + return card +} + +func (deck *Classic) getNextPlayer() int { + direction := 1 + if deck.DirectionReversed { + direction = -1 + } + return utils.Mod((deck.ActivePlayer + direction), len(deck.room.Players)) +} + +func (deck *Classic) nextPlayer() { + deck.ActivePlayer = deck.getNextPlayer() +} + +func (deck *Classic) CanPlay(card types.Card) bool { + topCard := deck.getTopCard() + checkCard := card.(*ClassicCard) + if topCard == nil || checkCard == nil { + return topCard == nil + } + return checkCard.Color == "black" || checkCard.Color == topCard.Color || checkCard.Symbol == topCard.Symbol +} + +func (deck *Classic) PlayCard(card types.Card) bool { + if !deck.CanPlay(card) { + return false + } + deckCard := card.(*ClassicCard) + deck.CardsPlayed = append(deck.CardsPlayed, deckCard) + + if deckCard.Symbol == "action:skip" { + deck.nextPlayer() + } else if deckCard.Symbol == "action:draw_2" || deckCard.Symbol == "action:draw_4" { + targetPlayer := deck.room.Players[deck.getNextPlayer()] + amount := 2 + if deckCard.Symbol == "action:draw_4" { + amount = 4 + } + for range amount { + card := deck.drawCard(targetPlayer) + if card == nil { + // TODO: Handle empty card deck + break + } + } + } else if deckCard.Symbol == "action:reverse" { + deck.DirectionReversed = !deck.DirectionReversed + } + + if deckCard.Color != "black" { + deck.nextPlayer() + } + + return true +} + +func (deck *Classic) UpdatePlayedCard(cardData interface{}) types.Card { + topCard := deck.getTopCard() + if topCard.Color != "black" { + return nil + } + updateData, ok := cardData.(map[string]interface{}) + if !ok { + return nil + } + newColor, ok := updateData["Color"].(string) + if !ok { + return nil + } + + for _, color := range ClassicColors { + if newColor == color { + deck.nextPlayer() + topCard.Color = color + return topCard + } + } + return nil +} + +func (deck *Classic) IsPlayerActive(target *types.Player) bool { + return deck.room.Players[utils.Mod(deck.ActivePlayer, len(deck.room.Players))] == target +} + +type ClassicCard struct { + Symbol string + Color string +} diff --git a/backend/decks/decks.go b/backend/decks/decks.go new file mode 100644 index 0000000..8b61151 --- /dev/null +++ b/backend/decks/decks.go @@ -0,0 +1,31 @@ +package decks + +import ( + "github.com/HexCardGames/HexDeck/types" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func DeckFromInterface(cardDeckId int, cardDeck bson.D) types.CardDeck { + bsonBytes, _ := bson.Marshal(cardDeck) + + switch cardDeckId { + case 0: + deck := Classic{} + bson.Unmarshal(bsonBytes, &deck) + return &deck + } + + return nil +} + +func CardFromInterface(cardDeckId int, card bson.D) types.Card { + bsonBytes, _ := bson.Marshal(card) + + switch cardDeckId { + case 0: + deck := ClassicCard{} + bson.Unmarshal(bsonBytes, &deck) + return &deck + } + return nil +} diff --git a/backend/game/game.go b/backend/game/game.go new file mode 100644 index 0000000..33967b6 --- /dev/null +++ b/backend/game/game.go @@ -0,0 +1,209 @@ +package game + +import ( + "log/slog" + "math/rand/v2" + "strconv" + "sync" + + "github.com/HexCardGames/HexDeck/db" + "github.com/HexCardGames/HexDeck/decks" + "github.com/HexCardGames/HexDeck/types" + "github.com/HexCardGames/HexDeck/utils" + petname "github.com/dustinkirkland/golang-petname" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var roomsMutex sync.Mutex = sync.Mutex{} +var rooms []*types.Room = make([]*types.Room, 0) + +func GenerateJoinCode() string { + code := "" + for i := 0; i < 6; i++ { + code += strconv.Itoa(rand.IntN(10)) + } + return code +} + +func LoadRooms() { + roomsMutex.Lock() + defer roomsMutex.Unlock() + rooms = db.Conn.QueryRunningRooms() +} + +func CreateRoom() *types.Room { + newRoom := &types.Room{ + RoomId: bson.NewObjectID(), + JoinCode: GenerateJoinCode(), + GameState: types.StateLobby, + Players: make([]*types.Player, 0), + PlayersMutex: &sync.Mutex{}, + CardDeckId: 0, + } + + db.Conn.InsertRoom(newRoom) + roomsMutex.Lock() + defer roomsMutex.Unlock() + rooms = append(rooms, newRoom) + return newRoom +} + +func FindRoomByJoinCode(joinCode string) *types.Room { + for _, room := range rooms { + if room.JoinCode != joinCode { + continue + } + return room + } + return nil +} + +func FindSession(sessionToken string) (*types.Room, *types.Player) { + for _, room := range rooms { + for _, player := range room.Players { + if player.SessionToken == sessionToken { + return room, player + } + } + } + return nil, nil +} + +func JoinRoom(room *types.Room, requestedUsername string) *types.Player { + var username string + if requestedUsername != "" && room.IsUsernameAvailable(requestedUsername) { + username = requestedUsername + } else { + username = petname.Generate(2, " ") + } + + player := &types.Player{ + PlayerId: bson.NewObjectID(), + SessionToken: uuid.New().String(), + Username: username, + Permissions: 0, + Cards: make([]types.Card, 0), + Connection: types.WebsocketConnection{ + IsConnected: false, + }, + } + player.ResetInactivity() + room.AppendPlayer(player) + OnRoomUpdate(room) + return player +} + +type GameStats struct { + RunningGames int + OnlinePlayerCount int +} + +func CalculateStats() GameStats { + roomsMutex.Lock() + defer roomsMutex.Unlock() + stats := GameStats{RunningGames: 0, OnlinePlayerCount: 0} + for _, game := range rooms { + stats.RunningGames += 1 + for _, player := range game.Players { + if player.Connection.IsConnected { + stats.OnlinePlayerCount += 1 + } + } + } + return stats +} + +func UpdateGameState(room *types.Room, newState types.GameState) { + if room.GameState != types.StateEnded && newState == types.StateEnded { + db.Conn.IncrementGamesPlayed() + } + room.GameState = newState + OnRoomUpdate(room) +} + +func BroadcastInRoom(room *types.Room, topic string, data interface{}) { + for _, player := range room.Players { + if !player.Connection.IsConnected || player.Connection.Socket == nil { + continue + } + player.Connection.Socket.Emit(topic, data) + } +} + +func OnRoomUpdate(room *types.Room) { + db.Conn.UpdateRoom(room) + BroadcastInRoom(room, "RoomInfo", types.BuildRoomInfoPacket(room)) +} + +func OnPlayerStateUpdate(room *types.Room, player *types.Player, skipDBUpdate bool) { + if !skipDBUpdate { + db.Conn.UpdateRoom(room) + } + player.Connection.Socket.Emit("OwnCards", types.BuildOwnCardsPacket(room, player)) + BroadcastInRoom(room, "PlayerState", types.BuildPlayerStatePacket(room, player)) +} + +func UpdateAllPlayers(room *types.Room) { + db.Conn.UpdateRoom(room) + for _, player := range room.Players { + OnPlayerStateUpdate(room, player, true) + } +} + +func OnPlayCard(room *types.Room, player *types.Player, cardIndex int, card types.Card) { + BroadcastInRoom(room, "CardPlayed", types.BuildCardPlayedPacket(player, cardIndex, card)) + UpdateAllPlayers(room) +} + +func OnPlayedCardUpdate(room *types.Room, player *types.Player, card types.Card) { + BroadcastInRoom(room, "PlayedCardUpdate", types.BuildPlayedCardUpdatePacket(player, card)) + UpdateAllPlayers(room) +} + +func StartGame(room *types.Room) { + if room.GameState != types.StateLobby { + return + } + room.CardDeck = &decks.Classic{} + room.CardDeck.Init(room) + UpdateGameState(room, types.StateRunning) + UpdateAllPlayers(room) +} + +func TickRooms(deltaTime int) { + roomsMutex.Lock() + defer roomsMutex.Unlock() + + for i := 0; i < len(rooms); i++ { + room := rooms[i] + + hasChanged := false + room.PlayersMutex.Lock() + for j := 0; j < len(room.Players); j++ { + player := room.Players[j] + if player.Connection.IsConnected { + continue + } + if player.InactivityTimeout <= deltaTime { + slog.Debug("Removing player from room due to inactivity", "username", player.Username, "playerId", player.PlayerId.Hex(), "roomId", room.RoomId.Hex()) + hasChanged = true + room.RemovePlayerUnsafe(*player) + j-- + } + player.InactivityTimeout -= deltaTime + } + + if len(room.Players) == 0 { + slog.Debug("Ending and unloading empty room", "roomId", room.RoomId.Hex()) + UpdateGameState(room, types.StateEnded) + utils.RemoveSliceElement(&rooms, room) + i-- + } + room.PlayersMutex.Unlock() + + if hasChanged { + OnRoomUpdate(room) + } + } +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..698dbcc --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,66 @@ +module github.com/HexCardGames/HexDeck + +go 1.23.4 + +require ( + github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/zishang520/socket.io/v2 v2.3.6 + go.mongodb.org/mongo-driver/v2 v2.0.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 +) + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/bytedance/sonic v1.12.8 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.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.24.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/text v0.2.0 // 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/v2 v2.12.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.48.2 // indirect + github.com/quic-go/webtransport-go v0.0.0-20241018022711-4ac2c9250e66 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/zishang520/engine.io-go-parser v1.2.7 // indirect + github.com/zishang520/engine.io/v2 v2.2.5 // indirect + github.com/zishang520/socket.io-go-parser/v2 v2.2.3 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/arch v0.13.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.36.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..26fd22e --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,189 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= +github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +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-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +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/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +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/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +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/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= +github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= +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.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +github.com/quic-go/webtransport-go v0.0.0-20241018022711-4ac2c9250e66 h1:XymiULLvtioceJngDdwgQuyjsww8V1lqsvqaKxasAb0= +github.com/quic-go/webtransport-go v0.0.0-20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zishang520/engine.io-go-parser v1.2.7 h1:pnJr/9kOmOLBJcUQpOnRfR1q3UJAQudkeF4wLyqbtnM= +github.com/zishang520/engine.io-go-parser v1.2.7/go.mod h1:WRsjNz1Oi04dqGcvjpW0t6/B2KIuDSrTBvCZDs7r3XY= +github.com/zishang520/engine.io/v2 v2.2.5 h1:10pQBreQm0LN5CKI40VvucHfGOV/IE5FeIlrCuDEBJA= +github.com/zishang520/engine.io/v2 v2.2.5/go.mod h1:XFMmS8nhF3OOpqYk6eEgATzHCs6Bkp1LuorVSk05NrQ= +github.com/zishang520/socket.io-go-parser/v2 v2.2.3 h1:kjmpDTj/j8A67/Tpc8E0jc6mDi5pERP23tQ6YNhbj+Q= +github.com/zishang520/socket.io-go-parser/v2 v2.2.3/go.mod h1:w3il6LbFRcp7cwuaez0zTGC035UnNjNjxfO0r0SECLk= +github.com/zishang520/socket.io/v2 v2.3.6 h1:TKu7OZL7T/RLpRB3XluaXvgiG4B+6RMweMU+ia8akgo= +github.com/zishang520/socket.io/v2 v2.3.6/go.mod h1:UcJJIDwGJSVVqsUOompW0xehwU+8EoUkfyW06j3Fa+k= +go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI= +go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= +golang.org/x/arch v0.13.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..7537209 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "log/slog" + "os" + "time" + + "github.com/HexCardGames/HexDeck/api" + "github.com/HexCardGames/HexDeck/db" + "github.com/HexCardGames/HexDeck/game" + "github.com/HexCardGames/HexDeck/utils" +) + +func main() { + logHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + slog.SetDefault(slog.New(logHandler)) + + mongoUri := utils.Getenv("MONGO_URI", "") + if mongoUri == "" { + slog.Error("MONGO_URI environment variable not set!") + return + } + db.InitDB(mongoUri) + game.LoadRooms() + + roomTicker := time.NewTicker(1 * time.Second) + go func() { + for { + select { + case <-roomTicker.C: + game.TickRooms(1000) + } + } + }() + + api.InitApi() +} diff --git a/backend/types/types.go b/backend/types/types.go new file mode 100644 index 0000000..4312ee9 --- /dev/null +++ b/backend/types/types.go @@ -0,0 +1,142 @@ +package types + +import ( + "sync" + + "github.com/zishang520/socket.io/v2/socket" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type WebsocketConnection struct { + IsConnected bool + Socket *socket.Socket +} + +type Card interface { +} + +type CardDeck interface { + Init(*Room) + SetRoom(*Room) + IsEmpty() bool + DrawCard() Card + CanPlay(Card) bool + PlayCard(Card) bool + GetTopCard() Card + UpdatePlayedCard(interface{}) Card + IsPlayerActive(*Player) bool +} + +type Player struct { + PlayerId bson.ObjectID + SessionToken string + Username string + Permissions int + Cards []Card `json:"-"` + Connection WebsocketConnection `bson:"-" json:"-"` + InactivityTimeout int `bson:"-" json:"-"` +} + +func (player *Player) ResetInactivity() { + player.InactivityTimeout = 20 * 1000 +} + +func (player *Player) SetPermissionBit(bit RoomPermission) { + player.Permissions |= (1 << bit) +} + +func (player *Player) ClearPermissionBit(bit RoomPermission) { + player.Permissions &= ^(1 << bit) +} + +func (player *Player) HasPermissionBit(bit RoomPermission) bool { + return player.Permissions&(1< 0 +} + +type GameState int + +const ( + StateLobby GameState = iota + StateRunning + StateEnded +) + +type RoomPermission int + +const ( + PermissionHost RoomPermission = 0 +) + +type GameOptions struct { +} + +type Room struct { + RoomId bson.ObjectID `bson:"_id"` + JoinCode string + GameState GameState + GameOptions GameOptions + CardDeckId int + CardDeck CardDeck + Players []*Player + PlayersMutex *sync.Mutex `bson:"-"` + OwnerId bson.ObjectID + MoveTimeout int + Winner *bson.ObjectID +} + +func (room *Room) AppendPlayer(player *Player) { + room.PlayersMutex.Lock() + defer room.PlayersMutex.Unlock() + room.Players = append(room.Players, player) +} + +func (room *Room) RemovePlayer(target Player) bool { + room.PlayersMutex.Lock() + defer room.PlayersMutex.Unlock() + return room.RemovePlayerUnsafe(target) +} + +func (room *Room) FindPlayer(playerId bson.ObjectID) *Player { + room.PlayersMutex.Lock() + defer room.PlayersMutex.Unlock() + + for _, player := range room.Players { + if player.PlayerId == playerId { + return player + } + } + return nil +} + +func (room *Room) RemovePlayerUnsafe(target Player) bool { + foundHost := false + foundPlayer := false + for i := 0; i < len(room.Players); i++ { + player := room.Players[i] + if player.PlayerId == target.PlayerId { + room.Players = append(room.Players[:i], room.Players[i+1:]...) + foundPlayer = true + i-- + continue + } + if player.HasPermissionBit(PermissionHost) { + foundHost = true + } + } + if !foundPlayer { + return false + } + if !foundHost && len(room.Players) > 0 { + room.Players[0].SetPermissionBit(PermissionHost) + } + return true +} + +func (room *Room) IsUsernameAvailable(username string) bool { + for _, player := range room.Players { + if player.Username == username { + return false + } + } + return true +} diff --git a/backend/types/websocket_packets.go b/backend/types/websocket_packets.go new file mode 100644 index 0000000..f3e5ac5 --- /dev/null +++ b/backend/types/websocket_packets.go @@ -0,0 +1,113 @@ +package types + +import ( + "go.mongodb.org/mongo-driver/v2/bson" +) + +type S2C_Status struct { + IsError bool + StatusCode string + Message string +} +type S2C_PlayerInfo struct { + PlayerId bson.ObjectID + Username string + Permissions int + IsConnected bool +} +type S2C_RoomInfo struct { + RoomId bson.ObjectID `bson:"_id"` + JoinCode string + GameState GameState + GameOptions GameOptions + TopCard Card + CardDeckId int + Winner *bson.ObjectID + Players []S2C_PlayerInfo +} +type S2C_Card struct { + CanPlay bool + Card Card +} +type S2C_OwnCards struct { + Cards []S2C_Card +} +type S2C_PlayerState struct { + PlayerId bson.ObjectID + NumCards int + Active bool +} +type S2C_CardPlayed struct { + Card Card + CardIndex int + PlayedBy bson.ObjectID +} +type S2C_PlayedCardUpdate struct { + UpdatedBy bson.ObjectID + Card Card +} + +type C2S_UpdatePlayer struct { + PlayerId bson.ObjectID + Username *string + Permissions *int +} +type C2S_KickPlayer struct { + PlayerId bson.ObjectID +} +type C2S_PlayCard struct { + CardIndex *int + CardData interface{} +} +type C2S_UpdatePlayedCard struct { + CardData interface{} +} + +func BuildRoomInfoPacket(room *Room) S2C_RoomInfo { + players := make([]S2C_PlayerInfo, len(room.Players)) + for i, player := range room.Players { + players[i] = S2C_PlayerInfo{ + PlayerId: player.PlayerId, + Username: player.Username, + Permissions: player.Permissions, + IsConnected: player.Connection.IsConnected, + } + } + roomInfo := S2C_RoomInfo{ + RoomId: room.RoomId, + JoinCode: room.JoinCode, + GameState: room.GameState, + CardDeckId: room.CardDeckId, + GameOptions: room.GameOptions, + Winner: room.Winner, + Players: players, + } + + if room.CardDeck != nil { + roomInfo.TopCard = room.CardDeck.GetTopCard() + } + return roomInfo +} + +func BuildOwnCardsPacket(room *Room, player *Player) S2C_OwnCards { + cards := make([]S2C_Card, len(player.Cards)) + for i, card := range player.Cards { + cards[i] = S2C_Card{ + Card: card, + CanPlay: room.CardDeck.CanPlay(card), + } + } + return S2C_OwnCards{ + Cards: cards, + } +} + +func BuildPlayerStatePacket(room *Room, player *Player) S2C_PlayerState { + return S2C_PlayerState{PlayerId: player.PlayerId, NumCards: len(player.Cards), Active: room.CardDeck.IsPlayerActive(player)} +} +func BuildCardPlayedPacket(player *Player, cardIndex int, card Card) S2C_CardPlayed { + return S2C_CardPlayed{Card: card, CardIndex: cardIndex, PlayedBy: player.PlayerId} +} +func BuildPlayedCardUpdatePacket(player *Player, card Card) S2C_PlayedCardUpdate { + return S2C_PlayedCardUpdate{UpdatedBy: player.PlayerId, Card: card} +} diff --git a/backend/utils/utils.go b/backend/utils/utils.go new file mode 100644 index 0000000..44a5044 --- /dev/null +++ b/backend/utils/utils.go @@ -0,0 +1,38 @@ +package utils + +import ( + "os" + + "golang.org/x/exp/rand" +) + +func Getenv(key string, fallback string) string { + value, exists := os.LookupEnv(key) + if exists { + return value + } else { + return fallback + } +} + +func RemoveSliceElement[T comparable](slice *([]T), target T) bool { + for i, el := range *slice { + if el == target { + *slice = append((*slice)[:i], (*slice)[i+1:]...) + return true + } + } + return false +} + +func ShuffleSlice[T any](slice *([]T)) { + length := len(*slice) + for i := 0; i < length; i++ { + j := rand.Intn(i + 1) + (*slice)[i], (*slice)[j] = (*slice)[j], (*slice)[i] + } +} + +func Mod(a, b int) int { + return (a%b + b) % b +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..7d2e3f4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,8 @@ +services: + mongodb-dev: + user: "1000" + ports: + - 27017:27017 + volumes: + - ./data/mongodb-dev/:/data/db/ + image: mongodb/mongodb-community-server:latest