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 ? (
-
Waiting for opponent…
- ) : ( - - )} + {done && ( +Waiting for opponent…
+ ) : ( + + )} +{oppName} is deciding {pd.petName}…
-