This commit is contained in:
pixii
2025-03-06 10:25:16 +01:00
17 changed files with 1591 additions and 0 deletions

3
.env.sample Normal file
View 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
View File

@ -5,3 +5,9 @@
*.ntvs*
*.njsproj
*.sln
# Secrets
.env
# MongoDB docker volume
data/

1
backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
HexDeck

153
backend/api/api.go Normal file
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

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