diff --git a/README.md b/README.md index d7a9f14..b05e9fa 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,8 @@ The Golden pack adds mechanics the Turtle pack doesn't have: - **Golden Retriever** — once per battle, a side that runs out of cards but still holds Trumpets fields one, its power equal to those Trumpets. - **Avocado** — a persistent set-aside token you can discard in place of gold. -- **Nurse Shark** — the one interactive battle moment: the fight pauses and - asks how many Trumpets to spend on rocks (the bot answers with a fixed - policy, so simulated rollouts stay valid). +- **Nurse Shark** — on entry it spends every Trumpet it can (up to 3), + throwing two rocks at the enemy per Trumpet spent. - **Cockatoo** — a shop-time reveal, plus Manta Ray's free first buy, Blue-Ringed Octopus' per-buy apples, and more. diff --git a/internal/ai/ai.go b/internal/ai/ai.go index a52c750..099e9e6 100644 --- a/internal/ai/ai.go +++ b/internal/ai/ai.go @@ -35,13 +35,12 @@ import ( // Action is one move the bot wants to make, mirroring the client protocol. type Action struct { - Type string // buy | buyAvocado | sell | trade | tradeChoose | pass | arrange | ready | revealChoose | battleChoose + Type string // buy | buyAvocado | sell | trade | tradeChoose | pass | arrange | ready | revealChoose Row int // buy / buyAvocado Cards []string // sell / trade Pick int // tradeChoose Order []string // arrange CardID string // revealChoose (Cockatoo): the pet to reveal - Value int // battleChoose (Nurse Shark): Trumpets to spend } // Bot is a computer player at a fixed difficulty level. @@ -86,13 +85,6 @@ func (b *Bot) Act(v *game.View, mem *Memory) *Action { return b.decideArrange(v, mem) } case game.PhaseBattle: - if v.PendingBattle != nil { - if v.PendingBattle.Seat == v.YouSeat { - // Spend as many Trumpets as allowed — more rocks is better. - return &Action{Type: "battleChoose", Value: v.PendingBattle.Max} - } - return nil - } if !me.Ready { return &Action{Type: "ready"} } @@ -134,11 +126,6 @@ func Pending(v *game.View) bool { case game.PhaseArrange: return !me.Ready case game.PhaseBattle: - // A pending mid-battle decision is owed only by the deciding seat; - // otherwise everyone owes the battle acknowledgement. - if v.PendingBattle != nil { - return v.PendingBattle.Seat == v.YouSeat - } return !me.Ready } return false diff --git a/internal/ai/ai_test.go b/internal/ai/ai_test.go index 6fefe7e..a373eab 100644 --- a/internal/ai/ai_test.go +++ b/internal/ai/ai_test.go @@ -100,8 +100,6 @@ func applyAction(g *game.Game, playerID string, a *Action) error { return g.TradeChoose(playerID, a.Pick) case "revealChoose": return g.RevealChoose(playerID, a.CardID) - case "battleChoose": - return g.BattleChoose(playerID, a.Value) case "pass": return g.Pass(playerID) case "arrange": diff --git a/internal/game/battle.go b/internal/game/battle.go index 23426d4..4d8c27c 100644 --- a/internal/game/battle.go +++ b/internal/game/battle.go @@ -287,27 +287,18 @@ func effectCount(e Effect, s *battleSide, u *BattleUnit, enemy *battleSide) int // attack manages to hurt. A clash that changes nothing ends the battle as a // stalemate. // -// resolveBattle is the orchestrator: it re-runs the (deterministic) simulation -// from the recorded dice/decision tapes, publishing either a completed result -// or a suspended one awaiting a mid-battle decision (Golden pack: Nurse Shark). +// resolveBattle is the orchestrator: it runs the (deterministic) simulation and +// publishes the completed result. func (g *Game) resolveBattle() { - g.NextCardID = g.BattleCardBase - g.battleRollCursor = 0 - g.battleDecisionCursor = 0 - res, pending := g.runBattle() + res := g.runBattle() g.Battle = res - if pending != nil { - g.PendingBattle = pending - return - } - g.PendingBattle = nil g.finalizeBattle(res) } // finalizeBattle applies the persistent effects of a completed battle: trophies, // the priority token hand-off, the result log line, and clearing the per-round -// apples-in-play bank. Deferred here (not inside runBattle) because runBattle -// may re-run several times before the battle actually completes. +// apples-in-play bank. Kept separate from runBattle, which mutates no persistent +// player state. func (g *Game) finalizeBattle(res *BattleResult) { n := len(g.Players) winner := res.WinnerSeat @@ -332,13 +323,11 @@ func (g *Game) finalizeBattle(res *BattleResult) { } } -// runBattle plays the simulation to completion or until it needs a mid-battle -// decision, returning the (partial) result and a non-nil pending in the latter -// case. It mutates no persistent player state — that is finalizeBattle's job. -func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) { +// runBattle plays the simulation to completion, returning the result. It +// mutates no persistent player state — that is finalizeBattle's job. +func (g *Game) runBattle() *BattleResult { n := len(g.Players) res := &BattleResult{Round: g.Round, WinnerSeat: -1, StackSizes: make([]int, n), Lineups: make([][]Card, n)} - var suspended *PendingBattleDecision sides := make([]*battleSide, n) emit := func(ev BattleEvent) { res.Events = append(res.Events, ev) } // pname is the owning player's display name for a seat, for log text. @@ -1190,19 +1179,10 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) { Text: fmt.Sprintf("%s shuffles an apple into %s's deck.", q.unit.Card.Name, pname(q.seat))}) } case ActionSpendRocks: - // Nurse Shark: the owner chooses how many Trumpets (0..available, - // capped at Count) to spend; each throws two rocks. This is the one - // mid-battle decision — it may suspend the whole simulation. + // Nurse Shark: spend as many Trumpets as available (up to Count) to + // throw two rocks each. s := sides[q.seat] - maxSpend := min(q.effect.count(), s.trumpets) - choice, pending := g.decideBattle(PendingBattleDecision{ - Seat: q.seat, Kind: "nurseShark", PetName: q.unit.Card.Name, - Min: 0, Max: maxSpend, Trumpets: s.trumpets, - }) - if pending != nil { - suspended = pending - break - } + choice := min(q.effect.count(), s.trumpets) if choice > 0 { s.trumpets -= choice emit(BattleEvent{Type: "trumpet", Seat: q.seat, Count: -choice, @@ -1214,14 +1194,6 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) { } } } - if suspended != nil { - break // stop mid-plays; the battle re-runs once the choice is in - } - } - // A pending decision unwinds the whole simulation; the events emitted so - // far are a valid prefix the re-run reproduces exactly. - if suspended != nil { - return res, suspended } // Battle over? A side that can no longer field a pet is out (checked // after play effects so parting shots land, using canField so a pet that @@ -1337,5 +1309,5 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) { res.Trophies = 2 } } - return res, nil + return res } diff --git a/internal/game/cards.go b/internal/game/cards.go index 6b32593..e06a221 100644 --- a/internal/game/cards.go +++ b/internal/game/cards.go @@ -185,9 +185,8 @@ const ( // ActionStripApples (play) discards every apple attached to the enemy pet // in play (Durian perk) — like ActionStripFoods but apples only. ActionStripApples EffectAction = "stripApples" - // ActionSpendRocks (play) asks the owner how many Trumpets to spend (0..Count) - // and throws twice that many rocks at the enemy (Nurse Shark). This is the - // one battle-time player decision. + // ActionSpendRocks (play) spends as many Trumpets as available (up to Count) + // and throws twice that many rocks at the enemy (Nurse Shark). ActionSpendRocks EffectAction = "spendRocks" // ActionFirstBuyFree (shop start) makes the player's first Buy this round // cost no gold, if they hold Count or fewer pets (Manta Ray). diff --git a/internal/game/game.go b/internal/game/game.go index 30baf1c..a23ad82 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -137,96 +137,28 @@ type Game struct { Log []LogEntry `json:"log,omitempty"` LogSeq int `json:"logSeq"` // last assigned entry sequence number - // --- Golden pack: resumable battle (Nurse Shark's mid-battle choice) --- - // A battle is deterministic given its decks, the recorded dice tape, and the - // recorded decisions, so it can be re-run from scratch each time a decision - // is made. BattleCardBase snapshots NextCardID at battle start so re-runs - // mint identical ephemeral card ids. - BattleDice []int `json:"battleDice,omitempty"` - BattleDecisions []int `json:"battleDecisions,omitempty"` - BattleCardBase int `json:"battleCardBase,omitempty"` - PendingBattle *PendingBattleDecision `json:"pendingBattle,omitempty"` - // RollDie overrides the rock die (faces 0,0,1,1,2,2) for tests. Nil // (including after loading from storage) means a fair random roll. RollDie func() int `json:"-"` - - // Transient per-run battle state (not serialized): the tape cursors and the - // rollout auto-decide flag. - battleRollCursor int - battleDecisionCursor int - autoBattleDecide bool } -// PendingBattleDecision is a choice a player owes mid-battle (Golden pack: Nurse -// Shark). The battle suspends until BattleChoose supplies a value in [Min, Max]. -type PendingBattleDecision struct { - Seat int `json:"seat"` - Kind string `json:"kind"` // "nurseShark" - PetName string `json:"petName"` // for the prompt - Min int `json:"min"` - Max int `json:"max"` - Trumpets int `json:"trumpets"` // the side's current Trumpet pool -} - -// battleDraw returns a random value in [0, n), replaying from the recorded -// battle tape when re-running a battle and recording fresh draws otherwise. -// This keeps re-runs (after a mid-battle decision) deterministic across every -// source of battle randomness — rock dice and Komodo's apple shuffle alike. +// battleDraw returns a random value in [0, n) for a battle's randomness — rock +// dice and Komodo's apple shuffle alike. The RollDie test override applies to +// rock dice (n == 3). func (g *Game) battleDraw(n int) int { - if g.battleRollCursor < len(g.BattleDice) { - v := g.BattleDice[g.battleRollCursor] - g.battleRollCursor++ - return v - } - var v int switch { case n <= 0: - v = 0 + return 0 case n == 3 && g.RollDie != nil: - v = g.RollDie() // test override applies to rock dice + return g.RollDie() // test override applies to rock dice default: - v = randInt(n) + return randInt(n) } - g.BattleDice = append(g.BattleDice, v) - g.battleRollCursor++ - return v } // rollRockDie rolls one rock die: 0, 1, or 2 with equal probability. func (g *Game) rollRockDie() int { return g.battleDraw(3) } -// decideBattle resolves a mid-battle decision. In rollouts it auto-picks; when -// replaying it reads the recorded decision; otherwise it signals a suspend by -// returning a non-nil pending for the caller to surface. -func (g *Game) decideBattle(pd PendingBattleDecision) (int, *PendingBattleDecision) { - if g.autoBattleDecide { - return autoBattleChoice(pd), nil - } - if g.battleDecisionCursor < len(g.BattleDecisions) { - v := clampInt(g.BattleDecisions[g.battleDecisionCursor], pd.Min, pd.Max) - g.battleDecisionCursor++ - return v, nil - } - return 0, &pd -} - -// autoBattleChoice is the fixed policy used in rollouts and by bots: for Nurse -// Shark, spend as many Trumpets as allowed (more rocks is better). -func autoBattleChoice(pd PendingBattleDecision) int { - return pd.Max -} - -func clampInt(v, lo, hi int) int { - if v < lo { - return lo - } - if v > hi { - return hi - } - return v -} - var ( ErrNotYourTurn = errors.New("not your turn") ErrWrongPhase = errors.New("action not allowed in this phase") @@ -962,14 +894,8 @@ func (g *Game) SubmitOrder(playerID string, orderedIDs []string) error { return nil } -// startBattle enters the battle phase and resolves it. It snapshots the card-id -// base and clears the dice/decision tapes so the (possibly resumable) battle -// re-runs deterministically as decisions come in. +// startBattle enters the battle phase and resolves it. func (g *Game) startBattle() { - g.BattleCardBase = g.NextCardID - g.BattleDice = nil - g.BattleDecisions = nil - g.PendingBattle = nil for _, p := range g.Players { p.Ready = false } @@ -977,29 +903,12 @@ func (g *Game) startBattle() { g.resolveBattle() } -// BattleChoose supplies a value for the pending mid-battle decision (Golden -// pack: Nurse Shark) and re-runs the battle from the recorded tape. -func (g *Game) BattleChoose(playerID string, value int) error { - if g.PendingBattle == nil { - return fmt.Errorf("%w: no battle decision pending", ErrInvalidAction) - } - if g.Players[g.PendingBattle.Seat].ID != playerID { - return ErrNotYourTurn - } - g.BattleDecisions = append(g.BattleDecisions, clampInt(value, g.PendingBattle.Min, g.PendingBattle.Max)) - g.resolveBattle() - return nil -} - // AcknowledgeBattle marks the player done reviewing the battle. When all // players acknowledge, the next round starts (or the game ends). func (g *Game) AcknowledgeBattle(playerID string) error { if g.Phase != PhaseBattle { return ErrWrongPhase } - if g.PendingBattle != nil { - return fmt.Errorf("%w: a battle decision is still pending", ErrInvalidAction) - } p := g.PlayerByID(playerID) if p == nil { return errors.New("unknown player") diff --git a/internal/game/golden45_test.go b/internal/game/golden45_test.go index 877e707..9336772 100644 --- a/internal/game/golden45_test.go +++ b/internal/game/golden45_test.go @@ -2,11 +2,11 @@ package game import "testing" -// --- Nurse Shark: the mid-battle decision channel --- +// --- Nurse Shark --- -// Nurse Shark suspends the battle to ask how many Trumpets to spend, then -// resumes and throws two rocks per Trumpet spent. -func TestNurseSharkSuspendsAndResumes(t *testing.T) { +// Nurse Shark automatically spends every available Trumpet (up to 3), throwing +// two rocks per Trumpet spent. +func TestNurseSharkSpendsAllTrumpets(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 2 } // each rock deals 2 res := forceBattle(t, g, @@ -14,25 +14,9 @@ func TestNurseSharkSuspendsAndResumes(t *testing.T) { []Card{g.pet("Big", 4), g.pet("Tank", 6)}, ) // Nyala trades with Big (banking 2 Trumpets), then Nurse Shark enters and - // the battle suspends on its choice. - if g.PendingBattle == nil { - t.Fatal("expected the battle to suspend on Nurse Shark's choice") - } - if g.PendingBattle.Seat != 0 || g.PendingBattle.Max != 2 || g.PendingBattle.Kind != "nurseShark" { - t.Fatalf("unexpected pending decision: %+v", g.PendingBattle) - } - if res.WinnerSeat != -1 { - t.Fatalf("a suspended battle has no winner yet, got %d", res.WinnerSeat) - } - // Spend both Trumpets: 4 rocks (2+2+2+2 = 8) kill the 6-power Tank. - if err := g.BattleChoose(g.Players[0].ID, 2); err != nil { - t.Fatal(err) - } - if g.PendingBattle != nil { - t.Fatalf("battle should have completed: %+v", g.PendingBattle) - } + // spends both: 4 rocks (2+2+2+2 = 8) kill the 6-power Tank. spends := 0 - for _, ev := range eventsOfType(g.Battle, "trumpet") { + for _, ev := range eventsOfType(res, "trumpet") { if ev.Count < 0 { spends++ } @@ -40,44 +24,51 @@ func TestNurseSharkSuspendsAndResumes(t *testing.T) { if spends != 1 { t.Fatalf("expected one trumpet spend, got %d", spends) } - rocks := eventsOfType(g.Battle, "rock") + rocks := eventsOfType(res, "rock") if len(rocks) != 1 || len(rocks[0].Dice) != 4 || !rocks[0].TargetDied { t.Fatalf("Nurse Shark should throw 4 rocks (2 per Trumpet) and kill Tank: %+v", rocks) } - if g.Battle.WinnerSeat != 0 { - t.Fatalf("seat 0 should win, got %d", g.Battle.WinnerSeat) + if res.WinnerSeat != 0 { + t.Fatalf("seat 0 should win, got %d", res.WinnerSeat) } } -// Choosing to spend zero Trumpets throws no rocks. -func TestNurseSharkSpendZero(t *testing.T) { +// The spend is capped at 3 Trumpets even when more are banked. +func TestNurseSharkCapsAtThree(t *testing.T) { g, _, _ := testGame(t) - g.RollDie = func() int { return 2 } - forceBattle(t, g, - []Card{g.goldenPet(t, "Nyala"), g.goldenPet(t, "Nurse Shark")}, - []Card{g.pet("Big", 4), g.pet("Tank", 6)}, + g.RollDie = func() int { return 1 } // each rock deals 1 + g.Players[0].PendingTrumpets = 5 // seed a pool larger than the cap + res := forceBattle(t, g, + []Card{g.goldenPet(t, "Nurse Shark")}, + []Card{g.pet("Wall", 20)}, ) - if g.PendingBattle == nil { - t.Fatal("expected a pending decision") + var spend *BattleEvent + for i, ev := range res.Events { + if ev.Type == "trumpet" && ev.Count < 0 { + spend = &res.Events[i] + } } - if err := g.BattleChoose(g.Players[0].ID, 0); err != nil { - t.Fatal(err) + if spend == nil || spend.Count != -3 { + t.Fatalf("Nurse Shark should spend exactly 3 Trumpets: %+v", eventsOfType(res, "trumpet")) } - if len(eventsOfType(g.Battle, "rock")) != 0 { - t.Fatal("spending zero Trumpets should throw no rocks") + rocks := eventsOfType(res, "rock") + if len(rocks) != 1 || len(rocks[0].Dice) != 6 { + t.Fatalf("spending 3 Trumpets should throw 6 rocks: %+v", rocks) } } -// With no Trumpets, Nurse Shark's choice is trivial (max 0) and the rollout -// path auto-resolves without suspending. -func TestNurseSharkAutoResolvesInRollout(t *testing.T) { +// With no Trumpets, Nurse Shark throws no rocks and the battle still resolves. +func TestNurseSharkNoTrumpets(t *testing.T) { res := SimulateBattle(1, 0, []Card{cardWithName("Nurse Shark", 3, []Effect{{Trigger: TriggerPlay, Action: ActionSpendRocks, Count: 3}})}, []Card{{ID: "x", Kind: KindPet, Name: "Foe", Power: 2}}, func() int { return 2 }, ) if res == nil || res.WinnerSeat < -1 { - t.Fatalf("rollout should complete without suspending: %+v", res) + t.Fatalf("battle should resolve: %+v", res) + } + if len(eventsOfType(res, "rock")) != 0 { + t.Fatal("with no Trumpets, Nurse Shark should throw no rocks") } } diff --git a/internal/game/sim.go b/internal/game/sim.go index 8d0e1ed..024bf5f 100644 --- a/internal/game/sim.go +++ b/internal/game/sim.go @@ -17,9 +17,6 @@ func SimulateBattle(round, prioritySeat int, deckA, deckB []Card, rollDie func() {Name: "A", Seat: 0, Deck: append([]Card(nil), deckA...)}, {Name: "B", Seat: 1, Deck: append([]Card(nil), deckB...)}, }, - // Rollouts never pause for a mid-battle decision; a fixed policy resolves - // them (Golden pack: Nurse Shark). - autoBattleDecide: true, } g.startBattle() return g.Battle diff --git a/internal/game/view.go b/internal/game/view.go index 32bbb39..06e3a83 100644 --- a/internal/game/view.go +++ b/internal/game/view.go @@ -53,11 +53,8 @@ type View struct { // see a reveal is in progress; the eligible options are only sent to the // buyer (they name the buyer's own hidden pets). PendingReveal *PendingReveal `json:"pendingReveal,omitempty"` - // PendingBattle (Golden pack: Nurse Shark) is a mid-battle decision owed by - // one seat; public since the battle replay is public. - PendingBattle *PendingBattleDecision `json:"pendingBattle,omitempty"` - Battle *BattleResult `json:"battle,omitempty"` - WinnerSeat int `json:"winnerSeat"` + Battle *BattleResult `json:"battle,omitempty"` + WinnerSeat int `json:"winnerSeat"` // Log is the shared, public event log shown across every phase. Log []LogEntry `json:"log,omitempty"` // Debug is set by the server when its DEBUG flag is on, unlocking the @@ -125,7 +122,6 @@ func (g *Game) ViewFor(playerID string) View { } v.PendingReveal = &reveal } - v.PendingBattle = g.PendingBattle // Battle results (lineups, events) are public once resolved. Keep the // battle around during the following shop phase too, so late joiners / // reconnects can still see the last result. diff --git a/internal/server/bots.go b/internal/server/bots.go index 66f6fb1..d406e3a 100644 --- a/internal/server/bots.go +++ b/internal/server/bots.go @@ -157,8 +157,6 @@ func applyBotAction(g *game.Game, playerID string, a *ai.Action) error { return g.TradeChoose(playerID, a.Pick) case "revealChoose": return g.RevealChoose(playerID, a.CardID) - case "battleChoose": - return g.BattleChoose(playerID, a.Value) case "pass": return g.Pass(playerID) case "arrange": @@ -203,9 +201,6 @@ func botFallback(g *game.Game, playerID string) error { } return g.SubmitOrder(playerID, ids) case game.PhaseBattle: - if g.PendingBattle != nil && g.Players[g.PendingBattle.Seat].ID == playerID { - return g.BattleChoose(playerID, g.PendingBattle.Max) - } return g.AcknowledgeBattle(playerID) } return game.ErrInvalidAction diff --git a/internal/server/ws.go b/internal/server/ws.go index c3f37e8..9efdc1a 100644 --- a/internal/server/ws.go +++ b/internal/server/ws.go @@ -32,7 +32,6 @@ type clientMessage struct { Difficulty string `json:"difficulty"` // addBot Target string `json:"target"` // removePlayer (player ID) Card string `json:"card"` // revealChoose (Cockatoo): pet card id - Value int `json:"value"` // battleChoose (Nurse Shark): Trumpets to spend } type serverMessage struct { @@ -152,8 +151,6 @@ func (s *Server) apply(r *room, c *client, msg clientMessage) { err = g.TradeChoose(c.playerID, msg.Pick) case "revealChoose": err = g.RevealChoose(c.playerID, msg.Card) - case "battleChoose": - err = g.BattleChoose(c.playerID, msg.Value) case "pass": err = g.Pass(c.playerID) case "arrange": diff --git a/web/src/components/BattlePhase.tsx b/web/src/components/BattlePhase.tsx index 50cc08c..4a2f0f7 100644 --- a/web/src/components/BattlePhase.tsx +++ b/web/src/components/BattlePhase.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react' import type { Dispatch, SetStateAction } from 'react' import { createPortal } from 'react-dom' -import type { BattleEvent, Card, ClientMessage, GameView, PendingBattleDecision } from '../types' +import type { BattleEvent, Card, ClientMessage, GameView } from '../types' import { CardView } from './CardView' import { DiceRoll, ROLL_MS } from './DiceRoll' @@ -553,94 +553,37 @@ export function BattlePhase({ view, send, step, setStep }: Props) { ) })()} - {done && view.pendingBattle ? ( - - ) : ( - done && ( -
-
- {draw ? 'Draw!' : won ? 'Victory!' : 'Defeat…'} -
- {!draw && ( -
- {view.players[battle.winnerSeat]?.name} wins{' '} - {'🏆'.repeat(battle.trophies)} -
- )} - {draw &&
No trophies awarded
} - {acked ? ( -

Waiting for opponent…

- ) : ( - - )} + {done && ( +
+
+ {draw ? 'Draw!' : won ? 'Victory!' : 'Defeat…'}
- ) + {!draw && ( +
+ {view.players[battle.winnerSeat]?.name} wins{' '} + {'🏆'.repeat(battle.trophies)} +
+ )} + {draw &&
No trophies awarded
} + {acked ? ( +

Waiting for opponent…

+ ) : ( + + )} +
)}
) } -// BattleDecision is the mid-battle prompt (Golden pack: Nurse Shark). The -// deciding player picks how many Trumpets to spend; the other player waits. -function BattleDecision({ - pd, - youSeat, - oppName, - send, -}: { - pd: PendingBattleDecision - youSeat: number - oppName: string - send: (msg: ClientMessage) => void -}) { - const [sent, setSent] = useState(false) - // Reset when a fresh decision arrives (e.g. a second Nurse Shark). - useEffect(() => setSent(false), [pd.seat, pd.trumpets, pd.max]) - if (pd.seat !== youSeat) { - return ( -
-

{oppName} is deciding {pd.petName}…

-
- ) - } - return ( -
-
{pd.petName}
-
- Spend Trumpets to throw 2 🪨 each — you hold {pd.trumpets} 🎺 -
-
- {Array.from({ length: pd.max + 1 }, (_, n) => ( - - ))} -
-
- ) -} - // clashDamageTaken computes how much damage a seat's pet took in the clash // at event index `idx` (its damage total there minus its total beforehand). function clashDamageTaken(events: BattleEvent[], idx: number, seat: number): number { diff --git a/web/src/styles.css b/web/src/styles.css index 808b9fa..cda9598 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1410,19 +1410,6 @@ h3 { color: var(--gold); } -/* Mid-battle decision panel (Golden pack: Nurse Shark). */ -.battle-decision-options { - display: flex; - gap: 10px; - flex-wrap: wrap; - justify-content: center; - margin-top: 12px; -} - -.battle-decision-options .btn { - font-family: var(--font-display); -} - /* Set-aside Avocado tokens (Golden pack): a slim tray with a toggle pill. */ .avocado-zone { background: rgba(0, 0, 0, 0.24); diff --git a/web/src/types.ts b/web/src/types.ts index 53c0b2d..ec61843 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -48,16 +48,6 @@ export interface PendingReveal { options?: string[] // eligible pet card ids (buyer only) } -// Nurse Shark (Golden pack): a mid-battle Trumpet-spend choice. -export interface PendingBattleDecision { - seat: number - kind: string - petName: string - min: number - max: number - trumpets: number -} - export interface BattleEvent { type: | 'prep' @@ -140,7 +130,6 @@ export interface GameView { players: PlayerView[] pending?: PendingTrade pendingReveal?: PendingReveal - pendingBattle?: PendingBattleDecision battle?: BattleResult winnerSeat: number log?: LogEntry[] @@ -158,7 +147,6 @@ export type ClientMessage = | { type: 'trade'; cards: string[] } | { type: 'tradeChoose'; pick: number } | { type: 'revealChoose'; card: string } - | { type: 'battleChoose'; value: number } | { type: 'pass' } | { type: 'arrange'; order: string[] } | { type: 'ready' }