Pig — the press-your-luck dice game, scored from a roll log
In Pig a player rolls a d6 repeatedly, banking the running total — but a 1 busts the turn and scores 0. turn_score() folds a list of rolls into the banked points. play() runs the "hold at 20" strategy over a deterministic roll stream and reports the turn outcomes. Run: soma run pig_dice.cell --signal demo Test: soma test pig_dice.cell
A Soma program from the games part of the verified corpus: 66 lines, cells PigDice, PigTests. It passes soma check with soma 3.2.4.
How it is built
The cell PigDice declares 3 signals in its face: turn_score(rolls: List) -> Int, hold_at(rolls: List, target: Int) -> Map and demo() -> Map. Handlers: turn_score (line 21), hold_at (line 27) and demo (line 45).
The file also holds 1 test cell (PigTests) with 7 assertions, run by soma test.
The source
soma.toml
[package]
name = "games-corpus"
version = "0.1.0"
description = "Training corpus: games & simulations"
# The turn/phase machine is intentionally cyclic (a game loops through
# phases each round) with a `* -> over` escape, so we verify only that no
# reachable state deadlocks — liveness to a terminal is not the contract here.
[verify]
deadlock_free = truepig_dice.cell
// ============================================================================
// Pig — the press-your-luck dice game, scored from a roll log
// ============================================================================
//
// In Pig a player rolls a d6 repeatedly, banking the running total — but a 1
// busts the turn and scores 0. turn_score() folds a list of rolls into the
// banked points. play() runs the "hold at 20" strategy over a deterministic
// roll stream and reports the turn outcomes.
//
// Run: soma run pig_dice.cell --signal demo
// Test: soma test pig_dice.cell
cell PigDice {
face {
signal turn_score(rolls: List) -> Int
signal hold_at(rolls: List, target: Int) -> Map
signal demo() -> Map
}
// Sum the rolls; a single 1 anywhere busts the whole turn to 0.
on turn_score(rolls: List) {
if any(rolls, r => r == 1) { return 0 }
return sum(rolls)
}
// Stop as soon as the running total reaches `target` (or a 1 busts).
on hold_at(rolls: List, target: Int) {
let total = 0
let busted = false
let used = 0
for r in rolls {
if total < target && busted == false {
used = used + 1
if r == 1 {
total = 0
busted = true
} else {
total = total + r
}
}
}
return map("score", total, "rolls_used", used, "busted", busted)
}
on demo() {
// rolls 5,4,6,3 → 18, then 5 → 23 ≥ 20, hold
return hold_at([5, 4, 6, 3, 5, 2], 20)
}
}
cell test PigTests {
rules {
// clean run, no 1
assert turn_score([3, 4, 5]) == 12
// a 1 busts the turn to 0
assert turn_score([6, 6, 1, 6]) == 0
assert turn_score([]) == 0
// hold-at-20: 5+4+6+3+5 = 23, stops after 5 rolls
assert hold_at([5, 4, 6, 3, 5, 2], 20).score == 23
assert hold_at([5, 4, 6, 3, 5, 2], 20).rolls_used == 5
// busting before target
assert hold_at([5, 1, 6, 6], 20).busted == true
assert hold_at([5, 1, 6, 6], 20).score == 0
}
}What the toolchain says
$ soma check pig_dice.cell
✓ All checks passed.$ soma test pig_dice.cell
test PigTests ...
✓ assert turn_score([3, 4, 5]) == 12
✓ assert turn_score([6, 6, 1, 6]) == 0
✓ assert turn_score([]) == 0
✓ assert hold_at([5, 4, 6, 3, 5, 2], 20).score == 23
✓ assert hold_at([5, 4, 6, 3, 5, 2], 20).rolls_used == 5
✓ assert hold_at([5, 1, 6, 6], 20).busted == true
✓ assert hold_at([5, 1, 6, 6], 20).score == 0
7 tests: 7 passed, 0 failed$ soma verify pig_dice.cell
Verifying pig_dice.cell...
No state machine in this program: nothing to prove beyond `soma check` (invariants and lifecycles are what verify proves).
VERIFY OK (vacuous — no state machine)