Fix nurse shark and remove mid-battle choice.
This commit is contained in:
+7
-98
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user