KDU CGT study materials
KDU CGT study resources

The 7% Damage Gap and the Frame-Rate-Dependent Crit

KDU CGT · Put your knowledge to work in context.

All case studies

The 7% Damage Gap and the Frame-Rate-Dependent Crit

Combat in your action RPG resolves damage as floor( (base x (1 + sum of percentage bonuses)) x critMultiplier ) x situationalMultiplier, with each floor applied at the point shown. Bonuses are additive; crit multipliers are 1.5, 2.0 or 3.0; situational multipliers include a 1.25 backstab and a 0.75 cover reduction. A cap clamps total percentage bonuses at +300%. Three buffs stack (Battle Cry 25%, Ember 40%, Mark 30%) and one debuff subtracts 20%. Damage is stored in a 16-bit unsigned integer. Designers expect a fully buffed crit backstab to land near 4,000. QA observes three things: burst DPS is 7% lower than the design spreadsheet predicts; the same input sequence does different damage at 30 fps versus 60 fps; and players report a double-dip where swapping weapons mid-swing applies Ember twice. A hotfix window opens in five days.

1. Design the test approach for the damage formula. Identify the equivalence classes and boundaries, define the oracle, and say where the 7% gap is most likely to come from.

Read the worked answer

The oracle The oracle is an independent reference implementation of the written formula - a script built from the spec, not the design spreadsheet. Run every case through both and diff. Where spec and spreadsheet disagree, that disagreement is itself a finding; the 7% gap probably lives there.

Equivalence classes and boundaries

  • Bonus total: 0, small, near cap (+299%), exactly +300%, over cap (clamp), and a negative total (−20% alone, and −20% with no buffs, which raises whether damage may go negative or hit a 1-damage floor).
  • Crit multiplier: 1.5, 2.0, 3.0, with a non-crit control at 1.0.
  • Situational: none, backstab 1.25, cover 0.75, and both together, since order matters when they multiply.
  • Order of operations: sum the bonuses then floor once, versus floor per buff (25 + 40 + 30 applied separately). These differ by 1 to 3 per hit and compound across a burst rotation.
  • Data limits: max base × max multiplier against the 16-bit store; a 4,000 crit must not wrap; test 65,535 and 65,536 explicitly.
  • Edges: base 0, minimum damage, multi-hit weapons, damage-over-time ticks, rounding of .5.

Most likely source of the 7% Truncation applied at every stage instead of once, or a spreadsheet that sums bonuses multiplicatively. Both are invisible at low buff counts and grow with stacking. Before touching code, run a zero-buff control and a three-buff case: if the gap scales with stack count, it is the summing or the flooring.

2. Damage differs between 30 fps and 60 fps for identical inputs. Diagnose the likely causes and describe how you would test for frame-rate independence.

Read the worked answer

Diagnosis Damage should be a pure function of game state. If it is not, some quantity is being integrated or sampled per rendered frame rather than per fixed simulation tick. Candidates:

  • Buff duration stored in frames, so a 0.5 s Ember is 15 ticks at 30 fps but 30 ticks at 60 fps - twice the uptime, twice the stacks.
  • Damage over time applied once per rendered frame.
  • The mid-swing swap window sampled on the render tick, widening at low frame rates.
  • Float accumulation in the bonus sum, differing in step count.

How to test

  1. Build a fixed-timestep harness: replay the same recorded input at 30, 60, 120 and uncapped frame rates, then diff the damage logs. Totals must match exactly; timings may differ by no more than one simulation tick.
  2. Determinism check: same seed, same inputs, same build, run three times, hash the damage-event stream. Hashes must be identical.
  3. Vary only frame time - inject artificial hitches with a debug tool - to expose sampling bugs without changing inputs.
  4. Test on hardware that thermally throttles mid-fight (handheld), because that changes real frame time without changing the input recording.

Pass criterion to write down before testing Damage depends only on game state and elapsed game time, with a documented tolerance of one tick. Without a written criterion, the result gets signed off as looks fine, and the bug ships.

3. Characterise the weapon-swap double-dip: how do you reproduce it, how severe is it, and what do you take back to the design team?

Read the worked answer

Reproduction

  1. Record inputs at frame level: swap at swing frame N−1, N and N+1, with Ember on and off, at one and three stacks. A window that can only be expressed in frames is itself a smell.
  2. Hypothesis: damage is computed from a modifier list captured at swing start, and the swap re-enters the swing state without clearing the applied-modifier flag, so Ember is appended twice and summed additively. Confirm by logging the modifier list at damage resolution.
  3. Blast radius: does it affect all on-hit buffs, all weapons, only abilities with a wind-up? Try Mark and Battle Cry. If the same path double-applies, the fix is systemic rather than a patch.
  4. Authority check: does the client compute the damage and the server accept it? If yes, this is also a competitive-integrity and cheating vector, which changes the severity conversation entirely.

Severity and priority High severity if it is player-reachable, repeatable and gives a measurable advantage in a competitive or economy-relevant mode; lower if PvE-only and cosmetic. Even at lower severity it is a fast hotfix candidate, because exploit videos spread faster than crash reports and the whole player base adopts them within days.

Back to design The real defect is partly a spec gap: the intended rule (buffs snapshot at swing start and apply once) was never written down. Give design the repro window, the list of affected abilities, and a request for an explicit rule - otherwise the code fix will be undone by the next ability.