Add DEBUG-gated panel to buy any card in the shop

When the server's DEBUG env var is on, expose a card catalog endpoint
and a collapsible client panel that drops any pet or food straight into
your deck (free, off-turn). Gated server-side so it is inert in normal
play.
This commit is contained in:
Greyson Parrelli
2026-07-23 01:07:14 -04:00
parent 71fa20493a
commit b2aa7c5ca3
13 changed files with 282 additions and 7 deletions
+54
View File
@@ -519,6 +519,60 @@ func (g *Game) buildShopDecks() {
}
}
// Catalog returns one representative card for every pet and food in the game,
// tier by tier, for the debug "buy any card" panel. IDs are name-based
// placeholders (not real instances); pets use their first printed suit.
func Catalog() []Card {
var cards []Card
for tierIdx := range petTiers {
for _, t := range petTiers[tierIdx] {
suit := SuitRed
if len(t.Suits) > 0 {
suit = t.Suits[0]
}
cards = append(cards, Card{
ID: "pet-" + t.Name, Kind: KindPet, Name: t.Name, Tier: tierIdx + 1,
Power: t.Power, Suit: suit, Effects: t.Effects, EffectText: t.EffectText,
})
}
for _, f := range foodTiers[tierIdx] {
cards = append(cards, Card{
ID: "food-" + f.Name, Kind: KindFood, Name: f.Name, Tier: tierIdx + 1,
Food: f.Food, Perk: f.Perk, Effects: f.Effects, EffectText: f.EffectText,
})
}
}
return cards
}
// cardByName mints a fresh instance of the named pet or food from its
// template (pets take their first printed suit). Returns false if unknown.
func (g *Game) cardByName(name string) (Card, bool) {
for tierIdx := range petTiers {
for _, t := range petTiers[tierIdx] {
if t.Name == name {
suit := SuitRed
if len(t.Suits) > 0 {
suit = t.Suits[0]
}
return Card{
ID: g.newCardID(), Kind: KindPet, Name: t.Name, Tier: tierIdx + 1,
Power: t.Power, Suit: suit, Effects: t.Effects, EffectText: t.EffectText,
}, true
}
}
for _, f := range foodTiers[tierIdx] {
if f.Name == name {
return Card{
ID: g.newCardID(), Kind: KindFood, Name: f.Name, Tier: tierIdx + 1,
Food: f.Food, Perk: f.Perk, Effects: f.Effects, EffectText: f.EffectText,
}, true
}
}
}
return Card{}, false
}
// newApple mints an apple food card. Apples are temporary: they vanish from
// the deck after the next battle.
func (g *Game) newApple() Card {
+19
View File
@@ -430,6 +430,25 @@ func (g *Game) TradeChoose(playerID string, pick int) error {
return nil
}
// DebugGrant drops any card straight into a player's deck during the shop,
// free and off-turn — a testing aid gated behind the server's DEBUG flag, not
// a normal action. No buy effects fire.
func (g *Game) DebugGrant(playerID, name string) error {
if g.Phase != PhaseShop && g.Phase != PhaseCleanup {
return ErrWrongPhase
}
p := g.PlayerByID(playerID)
if p == nil {
return errors.New("unknown player")
}
c, ok := g.cardByName(name)
if !ok {
return fmt.Errorf("%w: no card named %q", ErrInvalidAction, name)
}
p.Deck = append(p.Deck, c)
return nil
}
// Pass forfeits the player's remaining coins and ends their shopping.
func (g *Game) Pass(playerID string) error {
p, err := g.requireShopTurn(playerID)
+30
View File
@@ -482,3 +482,33 @@ func TestViewHidesSecrets(t *testing.T) {
t.Fatal("trade options must be visible to the trader")
}
}
func TestDebugGrant(t *testing.T) {
g, p1, _ := testGame(t)
before := len(p1.Deck)
if err := g.DebugGrant(p1.ID, "Ant"); err != nil {
t.Fatal(err)
}
if len(p1.Deck) != before+1 || p1.Deck[before].Name != "Ant" || !p1.Deck[before].IsPet() {
t.Fatalf("granted card should be an Ant appended to the deck: %+v", p1.Deck)
}
if p1.Deck[before].ID == "" {
t.Fatal("granted card should get a real instance ID")
}
// Unknown card name is rejected.
if err := g.DebugGrant(p1.ID, "Nonexistent"); err == nil {
t.Fatal("unknown card name should error")
}
// Foods can be granted too.
if err := g.DebugGrant(p1.ID, "Honey"); err != nil {
t.Fatal(err)
}
if !Catalog()[0].IsPet() { // sanity on the shared catalog helper
t.Fatal("catalog should start with a pet")
}
// Not allowed outside shop/cleanup.
g.Phase = PhaseBattle
if err := g.DebugGrant(p1.ID, "Ant"); err == nil {
t.Fatal("grant should be rejected outside the shop")
}
}
+3
View File
@@ -35,6 +35,9 @@ type View struct {
Pending *PendingTrade `json:"pending,omitempty"`
Battle *BattleResult `json:"battle,omitempty"`
WinnerSeat int `json:"winnerSeat"`
// Debug is set by the server when its DEBUG flag is on, unlocking the
// client's "buy any card" panel. Not part of the pure game state.
Debug bool `json:"debug,omitempty"`
}
// ViewFor builds the state visible to the given player.