Files
super-auto-pets-board-game/internal/game/battle.go
T
2026-07-22 23:07:29 -04:00

123 lines
3.9 KiB
Go

package game
// BattleUnit is a pet on the battle line with its attached foods applied.
// Power (attack) is unaffected by damage; a unit dies when Damage >= Power.
type BattleUnit struct {
Card Card `json:"card"`
Foods []Card `json:"foods"`
Bonus int `json:"bonus"` // total power added by foods
Damage int `json:"damage"` // damage markers accumulated this battle
}
func (u *BattleUnit) Power() int { return u.Card.Power + u.Bonus }
func (u *BattleUnit) Alive() bool { return u.Damage < u.Power() }
// BattleEvent is one step of the battle, in order, for clients to animate.
type BattleEvent struct {
Type string `json:"type"` // "clash"
// Indexes into the initial lineups (per seat) of the two front pets.
Units []int `json:"units"` // one entry per seat
// Damage each front pet has accumulated after the clash, per seat.
Damage []int `json:"damage"`
// Whether each front pet died in the clash, per seat.
Died []bool `json:"died"`
}
// BattleResult is the full, public record of one round's battle.
type BattleResult struct {
Round int `json:"round"`
Lineups [][]BattleUnit `json:"lineups"` // initial lineups per seat
WastedFood [][]Card `json:"wastedFoods"` // foods with no pet beneath them, per seat
Events []BattleEvent `json:"events"`
WinnerSeat int `json:"winnerSeat"` // -1 = draw
Trophies int `json:"trophies"` // awarded to the winner
}
// buildLineup walks a deck top-to-bottom, attaching each run of foods to the
// next pet below it. Foods after the last pet affect nothing and are wasted.
func buildLineup(deck []Card) (units []BattleUnit, wasted []Card) {
var pendingFoods []Card
for _, c := range deck {
if c.IsFood() {
pendingFoods = append(pendingFoods, c)
continue
}
u := BattleUnit{Card: c, Foods: pendingFoods}
for _, f := range pendingFoods {
if f.Food == FoodApple {
u.Bonus++
}
}
pendingFoods = nil
units = append(units, u)
}
return units, pendingFoods
}
// resolveBattle simulates the battle from the players' arranged decks,
// records the event log, awards trophies, and moves to PhaseBattle.
//
// Combat: the two front pets deal their full Power to each other
// simultaneously as damage markers. A pet with Damage >= Power dies. Since
// remaining health never exceeds Power, at least one pet dies every clash,
// so the loop always terminates (until effects say otherwise).
func (g *Game) resolveBattle() {
res := &BattleResult{
Round: g.Round,
Lineups: make([][]BattleUnit, len(g.Players)),
WastedFood: make([][]Card, len(g.Players)),
WinnerSeat: -1,
}
live := make([][]BattleUnit, len(g.Players)) // working copies
front := make([]int, len(g.Players)) // index of each seat's front pet
for _, p := range g.Players {
units, wasted := buildLineup(p.Deck)
res.Lineups[p.Seat] = units
res.WastedFood[p.Seat] = wasted
live[p.Seat] = append([]BattleUnit(nil), units...)
}
// Two-player combat. Effects and >2 player battle formats come later;
// the surrounding state (lineups, events) is already per-seat.
a, b := 0, 1
for front[a] < len(live[a]) && front[b] < len(live[b]) {
ua, ub := &live[a][front[a]], &live[b][front[b]]
ua.Damage += ub.Power()
ub.Damage += ua.Power()
ev := BattleEvent{
Type: "clash",
Units: []int{front[a], front[b]},
Damage: []int{ua.Damage, ub.Damage},
Died: []bool{!ua.Alive(), !ub.Alive()},
}
res.Events = append(res.Events, ev)
if !ua.Alive() {
front[a]++
}
if !ub.Alive() {
front[b]++
}
}
trophies := 1
if g.Round == MaxRounds {
trophies = 2
}
switch {
case front[a] < len(live[a]):
res.WinnerSeat = a
case front[b] < len(live[b]):
res.WinnerSeat = b
}
if res.WinnerSeat >= 0 {
res.Trophies = trophies
g.Players[res.WinnerSeat].Trophies += trophies
}
g.Battle = res
g.Phase = PhaseBattle
for _, p := range g.Players {
p.Ready = false
}
}