diff --git a/backend/api/websocket.go b/backend/api/websocket.go
index 5a75abd..c18c2de 100644
--- a/backend/api/websocket.go
+++ b/backend/api/websocket.go
@@ -95,6 +95,36 @@ func onPlayerJoin(client *socketio.Socket, room *types.Room, player *types.Playe
game.OnRoomUpdate(room)
})
+ client.On("SetCardDeck", func(datas ...any) {
+ setCardDeckRequest := types.C2S_SetCardDeck{}
+ unpackData(datas, &setCardDeckRequest)
+
+ if room.GameState != types.StateLobby {
+ client.Emit("Status", types.S2C_Status{
+ IsError: true,
+ StatusCode: "game_already_running",
+ Message: "You can't change the card deck while the game is running",
+ })
+ return
+ }
+ if !player.HasPermissionBit(types.PermissionHost) {
+ client.Emit("Status", types.S2C_Status{
+ IsError: true,
+ StatusCode: "insufficient_permission",
+ Message: "You can't change the card deck unless you are host",
+ })
+ return
+ }
+ if !game.SetCardDeck(room, setCardDeckRequest.CardDeckId) {
+ client.Emit("Status", types.S2C_Status{
+ IsError: true,
+ StatusCode: "invalid_card_deck",
+ Message: "No card deck exists with this ID",
+ })
+ return
+ }
+ })
+
client.On("UpdatePlayer", func(datas ...any) {
player.Mutex.Lock()
defer player.Mutex.Unlock()
@@ -248,7 +278,7 @@ func onPlayerJoin(client *socketio.Socket, room *types.Room, player *types.Playe
return
}
card := player.Cards[*updatePlayerRequest.CardIndex]
- if !room.CardDeck.PlayCard(card) {
+ if !room.CardDeck.CanPlay(card) {
client.Emit("Status", types.S2C_Status{
IsError: true,
StatusCode: "card_not_playable",
@@ -257,6 +287,9 @@ func onPlayerJoin(client *socketio.Socket, room *types.Room, player *types.Playe
return
}
player.Cards = append(player.Cards[:*updatePlayerRequest.CardIndex], player.Cards[*updatePlayerRequest.CardIndex+1:]...)
+ if !room.CardDeck.PlayCard(card) {
+ slog.Error("Cannot play card after checking", "roomId", room.RoomId.Hex(), "playerId", player.PlayerId.Hex())
+ }
game.OnPlayCard(room, player, *updatePlayerRequest.CardIndex, card)
if len(player.Cards) == 0 {
diff --git a/backend/decks/decks.go b/backend/decks/decks.go
index 8b61151..0cb27f8 100644
--- a/backend/decks/decks.go
+++ b/backend/decks/decks.go
@@ -13,6 +13,10 @@ func DeckFromInterface(cardDeckId int, cardDeck bson.D) types.CardDeck {
deck := Classic{}
bson.Unmarshal(bsonBytes, &deck)
return &deck
+ case 1:
+ deck := HexV1{}
+ bson.Unmarshal(bsonBytes, &deck)
+ return &deck
}
return nil
@@ -26,6 +30,10 @@ func CardFromInterface(cardDeckId int, card bson.D) types.Card {
deck := ClassicCard{}
bson.Unmarshal(bsonBytes, &deck)
return &deck
+ case 1:
+ deck := HexV1Card{}
+ bson.Unmarshal(bsonBytes, &deck)
+ return &deck
}
return nil
}
diff --git a/backend/decks/hexv1.go b/backend/decks/hexv1.go
new file mode 100644
index 0000000..fab58ac
--- /dev/null
+++ b/backend/decks/hexv1.go
@@ -0,0 +1,205 @@
+package decks
+
+import (
+ "fmt"
+ "math/rand/v2"
+
+ "github.com/HexCardGames/HexDeck/types"
+ "github.com/HexCardGames/HexDeck/utils"
+)
+
+type HexV1 struct {
+ room *types.Room
+ CardsPlayed []*HexV1Card
+ PlayerOrder []int
+ ActiveIndex int
+}
+
+type HexV1Card struct {
+ Symbol string
+ Color string
+ NumericValue int
+}
+
+var HexV1Colors = []string{"blue", "green", "yellow", "purple"}
+var HexV1ActionCards = []string{"shuffle", "skip", "draw", "swap"}
+
+func (deck *HexV1) Init(room *types.Room) {
+ deck.room = room
+ deck.PlayerOrder = make([]int, len(room.Players))
+ deck.ActiveIndex = 0
+
+ deck.room.PlayersMutex.Lock()
+ defer deck.room.PlayersMutex.Unlock()
+ for i, player := range deck.room.Players {
+ deck.PlayerOrder[i] = i
+ player.Mutex.Lock()
+ defer player.Mutex.Unlock()
+ deck.drawMany(player, 8)
+ }
+}
+
+func (deck *HexV1) SetRoom(room *types.Room) {
+ deck.room = room
+}
+
+func (deck *HexV1) IsEmpty() bool {
+ return false
+}
+
+func (deck *HexV1) getTopCard() *HexV1Card {
+ if len(deck.CardsPlayed) == 0 {
+ return nil
+ }
+ return deck.CardsPlayed[len(deck.CardsPlayed)-1]
+}
+
+func (deck *HexV1) GetTopCard() types.Card {
+ return deck.getTopCard()
+}
+
+func (deck *HexV1) generateCard() *HexV1Card {
+ cardType := rand.IntN(16 + len(HexV1ActionCards))
+ cardColor := HexV1Colors[rand.IntN(len(HexV1Colors))]
+ if cardType < 16 {
+ return &HexV1Card{
+ Symbol: fmt.Sprintf("%x", cardType),
+ Color: cardColor,
+ NumericValue: cardType,
+ }
+ }
+ cardSymbol := HexV1ActionCards[cardType-16]
+ if rand.IntN(100) <= 10 {
+ cardColor = "rainbow"
+ }
+ return &HexV1Card{
+ Symbol: "action:" + cardSymbol,
+ Color: cardColor,
+ NumericValue: 3,
+ }
+}
+
+func (deck *HexV1) drawCard(player *types.Player) types.Card {
+ card := deck.generateCard()
+ player.Cards = append(player.Cards, card)
+ return card
+}
+
+func (deck *HexV1) drawMany(player *types.Player, cards int) {
+ for i := 0; i < cards; i++ {
+ deck.drawCard(player)
+ }
+}
+
+func (deck *HexV1) DrawCard() types.Card {
+ // Can't draw another card before wildcard color is selected
+ topCard := deck.getTopCard()
+ if topCard != nil && topCard.Color == "rainbow" {
+ return nil
+ }
+
+ card := deck.drawCard(deck.getPlayer(deck.ActiveIndex))
+ deck.nextPlayer()
+ return card
+}
+
+func (deck *HexV1) getNextValidIndex(index int) int {
+ if len(deck.room.Players) == 0 || len(deck.PlayerOrder) == 0 {
+ return -1
+ }
+ checkIndex := utils.Mod(index, len(deck.PlayerOrder))
+ for deck.PlayerOrder[checkIndex] >= len(deck.room.Players) {
+ checkIndex = utils.Mod(checkIndex+1, len(deck.PlayerOrder))
+ }
+ return checkIndex
+}
+
+func (deck *HexV1) getPlayer(index int) *types.Player {
+ playerIndex := deck.getNextValidIndex(index)
+ if playerIndex == -1 {
+ return nil
+ }
+ return deck.room.Players[deck.PlayerOrder[playerIndex]]
+}
+
+func (deck *HexV1) getNextPlayerIndex() int {
+ return deck.getNextValidIndex(deck.ActiveIndex + 1)
+}
+
+func (deck *HexV1) nextPlayer() {
+ deck.ActiveIndex = deck.getNextPlayerIndex()
+}
+
+func (deck *HexV1) IsPlayerActive(target *types.Player) bool {
+ return deck.getPlayer(deck.ActiveIndex) == target
+}
+
+func (deck *HexV1) CanPlay(card types.Card) bool {
+ topCard := deck.getTopCard()
+ checkCard := card.(*HexV1Card)
+ if topCard == nil || checkCard == nil {
+ return topCard == nil
+ }
+ return topCard.Color != "rainbow" && (checkCard.Color == "rainbow" || checkCard.Color == topCard.Color || checkCard.Symbol == topCard.Symbol)
+}
+
+func (deck *HexV1) PlayCard(card types.Card) bool {
+ if !deck.CanPlay(card) {
+ return false
+ }
+ deckCard := card.(*HexV1Card)
+ targetPlayer := deck.getPlayer(deck.ActiveIndex)
+ nextPlayer := deck.getPlayer(deck.getNextPlayerIndex())
+
+ if deckCard.Symbol == "action:skip" && deckCard.Color != "rainbow" {
+ deck.nextPlayer()
+ } else if deckCard.Symbol == "action:draw" {
+ amount := 3
+ topCard := deck.getTopCard()
+ if topCard != nil {
+ amount = topCard.NumericValue
+ }
+ deck.drawMany(nextPlayer, amount)
+ } else if deckCard.Symbol == "action:shuffle" {
+ utils.ShuffleSlice(&deck.PlayerOrder)
+ } else if deckCard.Symbol == "action:swap" {
+ p1Cards := targetPlayer.Cards
+ p2Cards := nextPlayer.Cards
+ targetPlayer.Cards = p2Cards
+ nextPlayer.Cards = p1Cards
+ }
+
+ if deckCard.Color != "rainbow" {
+ deck.nextPlayer()
+ }
+
+ deck.CardsPlayed = append(deck.CardsPlayed, deckCard)
+ return true
+}
+
+func (deck *HexV1) UpdatePlayedCard(cardData interface{}) types.Card {
+ topCard := deck.getTopCard()
+ if topCard.Color != "rainbow" {
+ return nil
+ }
+ updateData, ok := cardData.(map[string]interface{})
+ if !ok {
+ return nil
+ }
+ newColor, ok := updateData["Color"].(string)
+ if !ok {
+ return nil
+ }
+
+ for _, color := range HexV1Colors {
+ if newColor == color {
+ deck.nextPlayer()
+ if topCard.Symbol == "action:skip" {
+ deck.nextPlayer()
+ }
+ topCard.Color = color
+ return topCard
+ }
+ }
+ return nil
+}
diff --git a/backend/game/game.go b/backend/game/game.go
index 9792ea0..f72ae18 100644
--- a/backend/game/game.go
+++ b/backend/game/game.go
@@ -39,7 +39,7 @@ func CreateRoom() *types.Room {
GameState: types.StateLobby,
Players: make([]*types.Player, 0),
PlayersMutex: &sync.Mutex{},
- CardDeckId: 0,
+ CardDeckId: 1,
}
db.Conn.InsertRoom(newRoom)
@@ -123,6 +123,24 @@ func UpdateGameState(room *types.Room, newState types.GameState) {
OnRoomUpdate(room)
}
+func SetCardDeck(room *types.Room, id int) bool {
+ if id < 0 || id > 1 {
+ return false
+ }
+ room.CardDeckId = id
+ OnRoomUpdate(room)
+ return true
+}
+
+func CreateCardDeckObj(room *types.Room) {
+ switch room.CardDeckId {
+ case 0:
+ room.CardDeck = &decks.Classic{}
+ case 1:
+ room.CardDeck = &decks.HexV1{}
+ }
+}
+
func BroadcastInRoom(room *types.Room, topic string, data interface{}) {
for _, player := range room.Players {
if !player.Connection.IsConnected || player.Connection.Socket == nil {
@@ -179,7 +197,7 @@ func StartGame(room *types.Room) {
if room.GameState != types.StateLobby {
return
}
- room.CardDeck = &decks.Classic{}
+ CreateCardDeckObj(room)
room.CardDeck.Init(room)
UpdateGameState(room, types.StateRunning)
UpdateAllPlayers(room)
diff --git a/backend/types/websocket_packets.go b/backend/types/websocket_packets.go
index 77b649f..16f917e 100644
--- a/backend/types/websocket_packets.go
+++ b/backend/types/websocket_packets.go
@@ -47,6 +47,9 @@ type S2C_PlayedCardUpdate struct {
Card Card
}
+type C2S_SetCardDeck struct {
+ CardDeckId int
+}
type C2S_UpdatePlayer struct {
PlayerId bson.ObjectID
Username *string
diff --git a/backend/utils/utils.go b/backend/utils/utils.go
index 44a5044..8a2c6da 100644
--- a/backend/utils/utils.go
+++ b/backend/utils/utils.go
@@ -34,5 +34,8 @@ func ShuffleSlice[T any](slice *([]T)) {
}
func Mod(a, b int) int {
+ if b == 0 {
+ return 0
+ }
return (a%b + b) % b
}
diff --git a/frontend/src/components/CardDeckShowcase.svelte b/frontend/src/components/CardDeckShowcase.svelte
new file mode 100644
index 0000000..f2197a8
--- /dev/null
+++ b/frontend/src/components/CardDeckShowcase.svelte
@@ -0,0 +1,29 @@
+
+
+