Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}