Fix nurse shark and remove mid-battle choice.

This commit is contained in:
Greyson Parrelli
2026-07-24 10:12:17 -04:00
parent 094f38593e
commit 0fe0757e18
14 changed files with 84 additions and 326 deletions
+12 -40
View File
@@ -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
}