mirror of
https://github.com/HexCardGames/HexDeck.git
synced 2025-09-03 18:48:38 +02:00
🎉 🚧 Start working on backend
This commit is contained in:
3
.env.sample
Normal file
3
.env.sample
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
LISTEN_HOST=0.0.0.0
|
||||||
|
LISTEN_PORT=3000
|
||||||
|
MONGO_URI="mongodb://127.0.0.1:27017/"
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -5,3 +5,9 @@
|
|||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
|
||||||
|
# MongoDB docker volume
|
||||||
|
data/
|
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
HexDeck
|
153
backend/api/api.go
Normal file
153
backend/api/api.go
Normal file
@ -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))
|
||||||
|
}
|
260
backend/api/websocket.go
Normal file
260
backend/api/websocket.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
82
backend/db/db.go
Normal file
82
backend/db/db.go
Normal file
@ -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)
|
||||||
|
}
|
71
backend/db/serializable_types.go
Normal file
71
backend/db/serializable_types.go
Normal file
@ -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
|
||||||
|
}
|
180
backend/decks/classic.go
Normal file
180
backend/decks/classic.go
Normal file
@ -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
|
||||||
|
}
|
31
backend/decks/decks.go
Normal file
31
backend/decks/decks.go
Normal file
@ -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
|
||||||
|
}
|
209
backend/game/game.go
Normal file
209
backend/game/game.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
backend/go.mod
Normal file
66
backend/go.mod
Normal file
@ -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
|
||||||
|
)
|
189
backend/go.sum
Normal file
189
backend/go.sum
Normal file
@ -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=
|
39
backend/main.go
Normal file
39
backend/main.go
Normal file
@ -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()
|
||||||
|
}
|
142
backend/types/types.go
Normal file
142
backend/types/types.go
Normal file
@ -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<<bit) > 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
|
||||||
|
}
|
113
backend/types/websocket_packets.go
Normal file
113
backend/types/websocket_packets.go
Normal file
@ -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}
|
||||||
|
}
|
38
backend/utils/utils.go
Normal file
38
backend/utils/utils.go
Normal file
@ -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
|
||||||
|
}
|
8
docker-compose.dev.yml
Normal file
8
docker-compose.dev.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
mongodb-dev:
|
||||||
|
user: "1000"
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
volumes:
|
||||||
|
- ./data/mongodb-dev/:/data/db/
|
||||||
|
image: mongodb/mongodb-community-server:latest
|
Reference in New Issue
Block a user