Every formula, in integers

All the math behind it

The exact formulas the kernel runs: semi-implicit Euler integration, swept peg collision with a reflection-and-restitution impulse, board-owned gravity from the frozen trig table, damage, wall and bucket handling, elastic ball-to-ball collisions, and the authoritative combo-scoring kernel. All of it is integer arithmetic with named constants.

Determinism & math10 min read

Every formula below runs in integers, with named constants and a fixed evaluation order. None of it uses floating point. This is the complete arithmetic of a tick.

#Integration (semi-implicit Euler)

Each active ball advances with semi-implicit (symplectic) Euler - gravity is added to velocity first, the speed is clamped to stop tunnelling, then position moves. Symplectic Euler is stable for the orbital, bouncy motion of a peg field in a way naive Euler is not.

vel += gravity  ·  if |vel|² > max_speed² rescale  ·  pos += vel
Order matters: gravity, then clamp, then move.

#Board-owned gravity

Gravity belongs to the board, not the shot. Every ball on a board falls the same way regardless of which edge it launched from. The pull direction is turned into a vector through the frozen trig table:

engine/cpp/src/world.cppcpp
const Side away = static_cast<Side>((static_cast<int>(arena_.gravity_pull) + 2) & 3);
const int base = side_base_angle_deg(away);
b.gravity = { mul_fp(arena_.gravity_accel, cos_fp(base)),
              mul_fp(arena_.gravity_accel, sin_fp(base)) };
gravity_accel
22 sub-units / tick² - the default fall acceleration.
gravity_pull
South by default; every board drains downward.
max_speed
2,200 sub-units / tick - the anti-tunnelling clamp.

#Swept peg collision

A fast ball must never teleport through a small peg. So collision is swept: the per-tick move from the previous position to the current one is sampled every 150 sub-units, and the engine stops at the first contact along that path. Three collider shapes share the same primitives - circle-vs-circle for round pegs, circle-vs-box for bars, and circle-vs-segment for curved capsule rails.

#The reflection impulse

The bounce itself is a clean elastic reflection about the contact normal, with restitution expressed as a fraction over 256 (256 = perfectly elastic):

v′ = v − (1 + e)(v · n) n
e = restitution / 256, computed before de-penetration.
engine/cpp/src/world.cppcpp
// v' = v - (1+e) * (v . n) * n, with e = restitution/256
const std::int64_t impulse = (std::int64_t{256 + b.restitution} * vn) / 256;
b.vel = { b.vel.x - mul_fp((std::int32_t)impulse, n.x),
          b.vel.y - mul_fp((std::int32_t)impulse, n.y) };

After the bounce, the ball is pushed back out of the peg by the penetration depth along the same normal, so it never rests inside geometry.

#Damage math

A hit's damage scales with the shot's power and how hard it struck along the normal:

damage = max( 1, power + |v · n| / 700 )
kVelDmgDiv = 700, kChipMin = 1, with a 6-tick re-hit cooldown.

Pegs that reach zero HP are marked, not removed - Peggle-style, all marked pegs clear together when the ball dies, and the clear is credited to the shooter's player_index. Default HP runs from 1 (fragile/normal) up to 12 (vault).

#Walls, drains and the moving bucket

A wall reflects a ball only when it is moving into the wall - a graze is clamped silently, with no phantom bounce - using the board's wall restitution (default 205/256, about 0.80). The drain edge deactivates a ball; if it arrives inside the current bucket window it scores as a bucket, otherwise it simply drains. That bucket window sweeps along the edge as a pure function of the tick, so it is identical on every replay.

#Ball-to-ball collisions

With many balls in flight at once, they collide elastically at equal mass, resolved in a fixed pair order (lower index first) with a split de-penetration and an averaged restitution. Equal-mass elastic collision in a fixed order stays deterministic no matter how crowded the board gets.

#The combo-scoring kernel

Score is authoritative and computed at the round-end sweep - not on the client. Each cleared peg contributes a base by type, with bonuses for orange and money pegs; the sweep then multiplies by two factors:

delta = base_sum × combo_mult × streak_mult / 10
Bigger sweeps and longer orange streaks multiply the base.
base per peg
10 fragile/normal · 15 sturdy · 25 shop · 50 gate · 150 vault (+40 orange, +25 × money tier).
combo_mult
min(cleared this sweep, 10) - reward big clears.
streak_mult
10 + 5 × min(orange streak, 6) → ×1.0 to ×4.0 in halves.
clean board
clearing the last orange sweeps the board and pays a 1,500 jackpot.

#The rolling-cannon aim model

A shot has three integer knobs: where along the launch edge the muzzle sits (rail_t), the aim offset in whole degrees, and the power fraction. The muzzle rides the edge; aim is a pure rotation within a legal arc; velocity comes straight from the trig table:

θ = base_angle(side) + legal_aim + aim_offset  ·  vel = ( speed·cos θ, speed·sin θ )

Speed is launch_speed × power / 65536 (base launch speed 900 for a normal ball, 760 for a heavy one). The legal aim arc starts at ±35° and widens +7° per Cannon-Arc rank up to a cap. A centered shot (rail_t at the midpoint) reproduces the older fixed-cannon exactly, so it hashes identically across engine versions - a deliberate compatibility choice.