Stamina — the energy bar that cannot be overfilled or driven negative
Mobile-game energy systems are attacked two ways. Clock rollback: set the device clock forward, bank the regen, roll it back, regen again. Spend underflow: spend more than you have and an unsigned wrap leaves you with ~4 billion energy — this shipped in more than one free-to-play title. Both attacks are, at bottom, a write outside [0, 100]. Here the MEMORY refuses them: energy past the cap or below zero is not a bug to catch, it is a value that cannot be stored.
A Soma program from the games_economy part of the verified corpus: 68 lines, cells Stamina, StaminaTests. It passes soma check with soma 3.2.4.
How it is built
The cell Stamina declares 3 signals in its face: spend(player: String, cost: Int) -> Map, regen(player: String, amount: Int) -> Map and energy_of(player: String) -> Int; keeps 1 memory slot: energy (Map<String, Int>, persistent and consistent) under the invariant energy >= 0 && energy <= 100. Handlers: energy_of (line 25), spend (line 31) and regen (line 40).
The file also holds 1 test cell (StaminaTests) with 11 assertions, run by soma test.
The source
stamina.cell
// ============================================================================
// STAMINA — the energy bar that cannot be overfilled or driven negative
// ============================================================================
// Mobile-game energy systems are attacked two ways. Clock rollback: set the
// device clock forward, bank the regen, roll it back, regen again. Spend
// underflow: spend more than you have and an unsigned wrap leaves you with
// ~4 billion energy — this shipped in more than one free-to-play title.
// Both attacks are, at bottom, a write outside [0, 100]. Here the MEMORY
// refuses them: energy past the cap or below zero is not a bug to catch,
// it is a value that cannot be stored.
// ============================================================================
cell Stamina {
face {
signal spend(player: String, cost: Int) -> Map
signal regen(player: String, amount: Int) -> Map
signal energy_of(player: String) -> Int
}
memory {
energy: Map<String, Int> [persistent, consistent]
invariant energy >= 0 && energy <= 100
}
on energy_of(player: String) {
let e = energy.get(player)
if e == () { return 100 } // fresh players start full
return e
}
on spend(player: String, cost: Int) {
if cost <= 0 { return map("ok", false, "reason", "cost must be positive") }
let cur = energy_of(player)
// the wall: overspending writes a negative — REFUSED by the memory
let w = try { energy.set(player, cur - cost) }
if w.error != () { return map("ok", false, "reason", "not enough energy: {cur} < {cost}") }
return map("ok", true, "left", cur - cost)
}
on regen(player: String, amount: Int) {
if amount <= 0 { return map("ok", false, "reason", "regen must be positive") }
let cur = energy_of(player)
// regen is clamped to the cap — banked clock time cannot overfill
energy.set(player, min(100, cur + amount))
return map("ok", true, "energy", energy.get(player))
}
}
cell test StaminaTests {
rules {
assert energy_of("ana") == 100
assert spend("ana", 70).ok == true
assert energy_of("ana") == 30
// underflow: spending 40 from 30 is a negative write — refused,
// and the balance is untouched (no 4-billion-energy wrap)
assert spend("ana", 40).ok == false
assert energy_of("ana") == 30
assert_fails energy.set("ana", -10)
// clock-rollback regen: the cap holds however much time was banked
assert regen("ana", 9999).ok == true
assert energy_of("ana") == 100
assert_fails energy.set("ana", 101)
// degenerate inputs bounce
assert spend("ana", 0).ok == false
assert regen("ana", -50).ok == false
}
}What the toolchain says
$ soma check stamina.cell
✓ All checks passed.$ soma test stamina.cell
test StaminaTests ...
✓ assert energy_of("ana") == 100
✓ assert spend("ana", 70).ok == true
✓ assert energy_of("ana") == 30
✓ assert spend("ana", 40).ok == false
✓ assert energy_of("ana") == 30
✓ assert_fails energy.set("ana", -10) — raised memory invariant violated on 'energy': energy >= 0 && energy <= 100 — rejected write of -10 (key "ana"); the slot is unchanged
✓ assert regen("ana", 9999).ok == true
✓ assert energy_of("ana") == 100
✓ assert_fails energy.set("ana", 101) — raised memory invariant violated on 'energy': energy >= 0 && energy <= 100 — rejected write of 101 (key "ana"); the slot is unchanged
✓ assert spend("ana", 0).ok == false
✓ assert regen("ana", -50).ok == false
11 tests: 11 passed, 0 failed$ soma verify stamina.cell
Verifying stamina.cell...
Stamina/invariants:
✓ invariant energy >= 0 && energy <= 100 — writer 'regen' proven by induction (writes min(100, cur + amount))
✓ invariant energy <= 100 — writer 'spend' proven by induction (writes cur - cost)
⚠ invariant energy >= 0 && energy <= 100 — runtime-checked (computed values): spend → energy [energy >= 0] because `cur - cost` is only known to lie in (-∞, 99] — narrow it: `require (cur - cost) >= 0 else …`
2 passed, 1 warnings, 0 failures
Temporal: 0 passed, 0 failed
VERIFY OK