diff --git a/internal/game/game.go b/internal/game/game.go index 076eb81..0133d12 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "slices" + "strings" ) // Tunable rules. The engine supports any player count >= 2; MinPlayers / @@ -369,6 +370,17 @@ func (g *Game) sellCards(p *Player, cardIDs []string) error { return nil } +// hasBuyEffect reports whether a card carries any buy-triggered ability, which +// means acquiring it does something visible to everyone. +func hasBuyEffect(c Card) bool { + for _, e := range c.Effects { + if e.Trigger == TriggerBuy { + return true + } + } + return false +} + // TradeStart spends one coin and three same-suit pets from the player's deck // to reveal the top two cards of the next tier's deck. The player must then // call TradeChoose before anything else happens. @@ -412,7 +424,14 @@ func (g *Game) TradeStart(playerID string, cardIDs []string) error { } p.Coins-- p.TripledThisRound = true - g.logf(p.Seat, "🔄", "%s traded in 3 %s pets for a tier %d pick.", p.Name, suit, nextTier) + // The discarded trio is public — everyone sees what was given up — even + // though the pet ultimately chosen stays secret (see TradeChoose). + names := make([]string, len(traded)) + for i, c := range traded { + names[i] = c.Name + } + g.logf(p.Seat, "🔄", "%s traded in %s (%s) for a tier %d pick.", + p.Name, strings.Join(names, ", "), suit, nextTier) g.Pending = &PendingTrade{ PlayerID: playerID, Tier: nextTier, @@ -440,7 +459,15 @@ func (g *Game) TradeChoose(playerID string, pick int) error { tierIdx := g.Pending.Tier - 1 g.ShopDecks[tierIdx] = append(g.ShopDecks[tierIdx], other) g.Pending = nil - g.logf(p.Seat, "🔄", "%s picked %s from the trade.", p.Name, chosen.Name) + // The pick is secret — opponents never saw the two revealed options and + // can't see the deck. But a pet with a Buy ability performs it publicly, so + // we have to reveal that pet (its effect log names it anyway). + if hasBuyEffect(chosen) { + g.logf(p.Seat, "🔄", "%s's trade pick is %s %s — its buy ability triggers.", + p.Name, article(chosen.Name), chosen.Name) + } else { + g.logf(p.Seat, "🔄", "%s keeps their trade pick hidden.", p.Name) + } // Pets obtained via the Triple action trigger their Buy effects. g.applyShopTrigger(p, chosen, TriggerBuy) g.advanceShopTurn() diff --git a/internal/game/game_test.go b/internal/game/game_test.go index 3857b69..f1547d4 100644 --- a/internal/game/game_test.go +++ b/internal/game/game_test.go @@ -2,6 +2,7 @@ package game import ( "slices" + "strings" "testing" ) @@ -225,6 +226,54 @@ func TestTradeTriggersTripleAndBuyEffects(t *testing.T) { } } +// A triple makes the three discarded pets public, but keeps the chosen pet +// secret — unless it has a Buy ability, which performs publicly. +func TestTradeVisibilityHidesPickUnlessBuyEffect(t *testing.T) { + logContains := func(g *Game, sub string) bool { + for _, e := range g.Log { + if strings.Contains(e.Text, sub) { + return true + } + } + return false + } + trade := func(g *Game, pick Card) *Game { + p := current(g) + var ids []string + for range 3 { + c := g.pet("Newt", 1) + c.Suit = SuitBlue + p.Deck = append(p.Deck, c) + ids = append(ids, c.ID) + } + if err := g.TradeStart(p.ID, ids); err != nil { + t.Fatal(err) + } + g.Pending.Options[0] = pick + if err := g.TradeChoose(p.ID, 0); err != nil { + t.Fatal(err) + } + return g + } + + // Discarded trio is named; an effect-less pick stays hidden. + g, _, _ := testGame(t) + trade(g, g.pet("SecretPet", 3)) + if !logContains(g, "Newt") { + t.Fatal("the discarded pets should be public in the log") + } + if logContains(g, "SecretPet") { + t.Fatal("a pick with no buy ability must stay hidden") + } + + // A pick with a Buy ability (Otter) is revealed. + g2, _, _ := testGame(t) + trade(g2, g2.realPet(t, "Otter")) + if !logContains(g2, "Otter") { + t.Fatal("a pick with a buy ability should be revealed in the log") + } +} + func TestBuyWormAddsTwoApples(t *testing.T) { g, _, _ := testGame(t) p := current(g)