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 } }