resolve.gno
12.38 Kb · 426 lines
1package engine
2
3// Result is how a round left the match.
4type Result int
5
6const (
7 Ongoing Result = iota
8 AWins
9 BWins
10 Draw
11)
12
13// Outcome is the state after a round, and the match result once it has one.
14type Outcome struct {
15 State State
16 Result Result
17}
18
19const (
20 lastRound = 3
21 maxPlanSize = beatsPerRound
22)
23
24// slot is the card of one plan occupying one beat.
25type slot struct {
26 index int // position of the card in the plan
27 lands bool // the card's last beat, where its effects resolve
28 active bool // lands, or a Parry card, which acts on every beat it occupies
29}
30
31func (rs *Ruleset) slotsOf(p Plan) [beatsPerRound]slot {
32 var out [beatsPerRound]slot
33 beat := 0
34 for i, id := range p {
35 c := &rs.Cards[id]
36 for k := 0; k < c.Cost; k++ {
37 lands := k == c.Cost-1
38 out[beat] = slot{index: i, lands: lands, active: lands || c.Parry > 0}
39 beat++
40 }
41 }
42 return out
43}
44
45// take applies damage through Block and returns what Block absorbed.
46func (v *Vitals) take(damage int) int {
47 absorbed := damage
48 if v.Block < absorbed {
49 absorbed = v.Block
50 }
51 v.Block -= absorbed
52 v.HP -= damage - absorbed
53 return absorbed
54}
55
56// gain applies a Block-phase effect and returns the change after the cap.
57func (v *Vitals) gain(rs *Ruleset, e Effect) int {
58 switch e.Kind {
59 case GainBlock, BlockPerVulnerable:
60 n := e.N
61 if e.Kind == BlockPerVulnerable {
62 n *= 1 + v.Vulnerable
63 }
64 before := v.Block
65 v.Block = capped(v.Block+n, rs.BlockCap)
66 return v.Block - before
67 case GainCounter:
68 before := v.Counter
69 v.Counter = capped(v.Counter+e.N, rs.CounterCap)
70 return v.Counter - before
71 }
72 return 0
73}
74
75func (rs *Ruleset) gainAll(v *Vitals, effects []Effect, tr *tracer, side Side, card CardID) {
76 for _, e := range effects {
77 n := v.gain(rs, e)
78 switch e.Kind {
79 case GainBlock, BlockPerVulnerable:
80 tr.emit(BlockGained, side, card, n, 0, false)
81 case GainCounter:
82 tr.emit(CounterGained, side, card, n, 0, false)
83 }
84 }
85}
86
87func movesVulnerable(effects []Effect) bool {
88 for _, e := range effects {
89 if e.Kind == MoveVulnerable {
90 return true
91 }
92 }
93 return false
94}
95
96// moveVulnerable puts all of each mover's Vulnerable on the other gnome; movers[side] is
97// nil when that side moves nothing. Both amounts are read before either changes, so two
98// moves on one beat swap the tokens. The receiver's cap applies.
99func (rs *Ruleset) moveVulnerable(f *[2]Vitals, movers [2]*Card, cards [2]CardID, tr *tracer, beat int) {
100 var held [2]int
101 for side := 0; side < 2; side++ {
102 if movers[side] != nil {
103 held[side], f[side].Vulnerable = f[side].Vulnerable, 0
104 }
105 }
106 for side := 0; side < 2; side++ {
107 if movers[side] == nil {
108 continue
109 }
110 o := 1 - side
111 f[o].Vulnerable = capped(f[o].Vulnerable+held[side], rs.VulnerableCap)
112 tr.at(beat, BlockPhase, movers[side].Tier, 1)
113 tr.emit(VulnerableMoved, Side(side), cards[side], held[side], 0, false)
114 }
115}
116
117func capped(v, limit int) int {
118 if v > limit {
119 return limit
120 }
121 return v
122}
123
124// Resolve plays round s.Round. Both plans must be legal for their side in s
125// (LegalPlans); an illegal plan is a caller error.
126func (rs *Ruleset) Resolve(s State, a, b Plan) Outcome {
127 return rs.resolve(s, a, b, nil)
128}
129
130func (rs *Ruleset) resolve(s State, a, b Plan, events *[]Event) Outcome {
131 plans := [2]Plan{a, b}
132 next := s
133 next.Round = s.Round + 1
134 tr := tracer{events: events}
135 var f [2]Vitals
136 var slots [2][beatsPerRound]slot
137 var gone [2][maxPlanSize]bool // cancelled, or a Parry that used up its cancels
138 var parried [2][maxPlanSize]int // cancels made by each Parry card this round
139 var hardest [2]int // the most HP one opponent hit took from each gnome this round
140 for side := 0; side < 2; side++ {
141 g := s.Gnomes[side]
142 f[side] = Vitals{HP: g.HP, Vulnerable: g.Vulnerable, Strength: g.Strength}
143 slots[side] = rs.slotsOf(plans[side])
144 for _, id := range plans[side] {
145 if rs.Cards[id].Special {
146 next.Gnomes[side].Spent |= uint64(1) << uint(id)
147 }
148 }
149 }
150
151 for beat := 0; beat < beatsPerRound; beat++ {
152 var cur [2]*Card // nil once cancelled or ended
153 var sl [2]slot
154 var cards [2]CardID
155 for side := 0; side < 2; side++ {
156 sl[side] = slots[side][beat]
157 cards[side] = plans[side][sl[side].index]
158 if !gone[side][sl[side].index] {
159 cur[side] = &rs.Cards[cards[side]]
160 }
161 }
162
163 // Interrupt phase
164 var cancelledNow [2]*Card
165 var didCancel [2]bool
166 for tier := Quick; tier <= Slow; tier++ {
167 var hit, parryDone [2]bool
168 for side := 0; side < 2; side++ {
169 o := 1 - side
170 me, op := cur[side], cur[o]
171 if me == nil || op == nil || me.Tier != tier {
172 continue
173 }
174 switch {
175 case me.Interrupt && sl[side].lands && !sl[o].active:
176 hit[o], didCancel[side] = true, true
177 case me.Parry > 0 && sl[side].active && sl[o].lands && op.Attack():
178 hit[o], didCancel[side] = true, true
179 parried[side][sl[side].index]++
180 parryDone[side] = parried[side][sl[side].index] >= me.Parry
181 }
182 }
183 tr.at(beat+1, InterruptPhase, tier, 1)
184 for side := 0; side < 2; side++ {
185 if hit[side] {
186 cancelledNow[side] = cur[side]
187 gone[side][sl[side].index] = true
188 cur[side] = nil
189 tr.emit(Cancelled, Side(side), cards[side], 0, 0, false)
190 }
191 if parryDone[side] && cur[side] != nil {
192 // Its later beats stay empty; this beat's effects still resolve.
193 gone[side][sl[side].index] = true
194 tr.emit(ParryEnded, Side(side), cards[side], 0, 0, false)
195 }
196 }
197 tr.close(&f)
198 }
199
200 facts := beatFacts{didCancel: didCancel}
201 for side := 0; side < 2; side++ {
202 o := 1 - side
203 facts.attacked[side] = cur[o] != nil && sl[o].lands && cur[o].Attack()
204 }
205
206 // Block phase
207 var movers [2]*Card
208 for side := 0; side < 2; side++ {
209 if cur[side] != nil && sl[side].lands {
210 tr.at(beat+1, BlockPhase, cur[side].Tier, 1)
211 rs.gainAll(&f[side], cur[side].Block, &tr, Side(side), cards[side])
212 if movesVulnerable(cur[side].Block) {
213 movers[side] = cur[side]
214 }
215 }
216 if cancelledNow[side] != nil {
217 tr.at(beat+1, BlockPhase, cancelledNow[side].Tier, 1)
218 rs.gainAll(&f[side], cancelledNow[side].OnCancel, &tr, Side(side), cards[side])
219 }
220 }
221 rs.moveVulnerable(&f, movers, cards, &tr, beat+1)
222 tr.close(&f)
223
224 // Damage phase
225 for tier := Quick; tier <= Slow; tier++ {
226 var effects [2][]Effect
227 for side := 0; side < 2; side++ {
228 switch c := cancelledNow[side]; {
229 case cur[side] != nil && sl[side].lands && cur[side].Tier == tier:
230 effects[side] = cur[side].Damage
231 case c != nil && c.Tier == tier:
232 effects[side] = c.CancelDamage
233 }
234 }
235 tr.at(beat+1, DamagePhase, tier, 0)
236 if result := rs.resolveDamageTier(&f, &hardest, effects, cards, facts, &tr); result != Ongoing {
237 return Outcome{State: rs.settle(next, f), Result: result}
238 }
239 }
240 }
241
242 next = rs.settle(next, f)
243 if s.Round < lastRound {
244 return Outcome{State: next, Result: Ongoing}
245 }
246 return Outcome{State: next, Result: byHP(next.Gnomes[SideA].HP, next.Gnomes[SideB].HP)}
247}
248
249// settle copies the round's carried values into the next state.
250func (rs *Ruleset) settle(next State, f [2]Vitals) State {
251 for side := 0; side < 2; side++ {
252 next.Gnomes[side].HP = f[side].HP
253 next.Gnomes[side].Vulnerable = f[side].Vulnerable
254 next.Gnomes[side].Strength = f[side].Strength
255 if rs.StrengthLastsTheRound {
256 next.Gnomes[side].Strength = 0
257 }
258 if rs.VulnerableLastsTheRound {
259 next.Gnomes[side].Vulnerable = 0
260 }
261 }
262 return next
263}
264
265func byHP(a, b int) Result {
266 switch {
267 case a > b:
268 return AWins
269 case b > a:
270 return BWins
271 }
272 return Draw
273}
274
275// beatFacts are what a beat's Damage phase reads from its Interrupt phase.
276type beatFacts struct {
277 didCancel [2]bool // the gnome's card cancelled an opponent card this beat
278 attacked [2]bool // an opponent attack lands on the gnome this beat, not cancelled
279}
280
281// resolveDamageTier resolves both gnomes' effects of one tier in steps: step k applies
282// effect k of each card together, every value read at the start of the step. A gnome
283// takes the opponent's hit before the Counter damage its own hit triggered. hardest
284// carries each gnome's hardest hit taken this round across tiers and beats. It returns
285// the knockout result as soon as a step leaves a gnome at 0 HP or less.
286func (rs *Ruleset) resolveDamageTier(f *[2]Vitals, hardest *[2]int, effects [2][]Effect, cards [2]CardID, facts beatFacts, tr *tracer) Result {
287 var hitsTotal, hitsDone [2]int
288 var doubled, doubleDecided [2]bool
289 for side := 0; side < 2; side++ {
290 for _, e := range effects[side] {
291 if e.Kind.isHit() && !skipsHit(e, facts, side) {
292 hitsTotal[side]++
293 }
294 }
295 }
296 steps := len(effects[0])
297 if len(effects[1]) > steps {
298 steps = len(effects[1])
299 }
300
301 for k := 0; k < steps; k++ {
302 tr.step = k + 1
303 var hitIn, counterIn, addVulnerable, selfVulnerable, addStrength, usedVulnerable [2]int
304 var strip [2]bool
305 for side := 0; side < 2; side++ {
306 if k >= len(effects[side]) {
307 continue
308 }
309 o := 1 - side
310 e := effects[side][k]
311 switch e.Kind {
312 case Hit, HitBlock, HitPerVulnerable, HitHardest, HitIfNotAttacked:
313 if skipsHit(e, facts, side) {
314 continue
315 }
316 if rs.VulnerableDoublesOneHit {
317 doubled[side] = f[o].Vulnerable > 0
318 } else if !doubleDecided[side] {
319 doubled[side], doubleDecided[side] = f[o].Vulnerable > 0, true
320 }
321 amount := e.N
322 switch e.Kind {
323 case HitBlock:
324 amount = f[side].Block
325 case HitPerVulnerable:
326 amount = e.N * (1 + f[side].Vulnerable)
327 case HitHardest:
328 amount = hardest[side]
329 if amount > e.N {
330 amount = e.N
331 }
332 }
333 amount += f[side].Strength
334 if doubled[side] {
335 amount *= 2
336 }
337 hitIn[o] += amount
338 counterIn[side] += f[o].Counter
339 hitsDone[side]++
340 if doubled[side] && (rs.VulnerableDoublesOneHit || hitsDone[side] == hitsTotal[side]) {
341 usedVulnerable[o]++
342 }
343 case ApplyVulnerable:
344 addVulnerable[o] += e.N
345 case GainVulnerable:
346 selfVulnerable[side] += e.N
347 case GainStrength:
348 addStrength[side] += e.N
349 case VulnerableIfCancelled:
350 if facts.didCancel[side] {
351 addVulnerable[o] += e.N
352 }
353 case StripBlock:
354 strip[o] = true
355 }
356 }
357 // vulnerableGained counts tokens from the opponent, selfGained a gnome's own; the
358 // opponent's count against the cap first.
359 var hitAbsorbed, counterAbsorbed, vulnerableGained, selfGained, strengthGained, stripped [2]int
360 for side := 0; side < 2; side++ {
361 hitAbsorbed[side] = f[side].take(hitIn[side])
362 counterAbsorbed[side] = f[side].take(counterIn[side])
363 if taken := hitIn[side] - hitAbsorbed[side]; taken > hardest[side] {
364 hardest[side] = taken
365 }
366 if strip[side] {
367 stripped[side], f[side].Block = f[side].Block, 0
368 }
369 v := f[side].Vulnerable - usedVulnerable[side]
370 if v < 0 {
371 v = 0
372 }
373 applied := capped(v+addVulnerable[side], rs.VulnerableCap)
374 f[side].Vulnerable = capped(applied+selfVulnerable[side], rs.VulnerableCap)
375 vulnerableGained[side] = applied - v
376 selfGained[side] = f[side].Vulnerable - applied
377 before := f[side].Strength
378 f[side].Strength = capped(before+addStrength[side], rs.StrengthCap)
379 strengthGained[side] = f[side].Strength - before
380 }
381 for side := 0; side < 2; side++ {
382 if k >= len(effects[side]) {
383 continue
384 }
385 o := 1 - side
386 switch e := effects[side][k]; e.Kind {
387 case Hit, HitBlock, HitPerVulnerable, HitHardest, HitIfNotAttacked:
388 if skipsHit(e, facts, side) {
389 break
390 }
391 tr.emit(HitLanded, Side(side), cards[side], hitIn[o]-hitAbsorbed[o], hitAbsorbed[o], doubled[side])
392 if counterIn[side] > 0 {
393 tr.emit(CounterHit, Side(side), cards[side], counterIn[side]-counterAbsorbed[side], counterAbsorbed[side], false)
394 }
395 case ApplyVulnerable:
396 tr.emit(VulnerableApplied, Side(side), cards[side], vulnerableGained[o], 0, false)
397 case GainVulnerable:
398 tr.emit(VulnerableGained, Side(side), cards[side], selfGained[side], 0, false)
399 case StripBlock:
400 tr.emit(BlockStripped, Side(side), cards[side], stripped[o], 0, false)
401 case VulnerableIfCancelled:
402 if facts.didCancel[side] {
403 tr.emit(VulnerableApplied, Side(side), cards[side], vulnerableGained[o], 0, false)
404 }
405 case GainStrength:
406 tr.emit(StrengthGained, Side(side), cards[side], strengthGained[side], 0, false)
407 }
408 }
409 tr.close(f)
410 outA, outB := f[SideA].HP <= 0, f[SideB].HP <= 0
411 switch {
412 case outA && outB:
413 return Draw
414 case outB:
415 return AWins
416 case outA:
417 return BWins
418 }
419 }
420 return Ongoing
421}
422
423// skipsHit reports whether a conditional hit of side's card deals nothing this beat.
424func skipsHit(e Effect, facts beatFacts, side int) bool {
425 return e.Kind == HitIfNotAttacked && facts.attacked[side]
426}