2048 — slide-and-merge a single row (the core move primitive)
slide_left() compacts a row toward the left, merging equal adjacent tiles exactly once per move (left-to-right), and reports the points scored (sum of merged tiles). A full board move is four row slides; this is that primitive. Run: soma run game_2048.cell --signal demo Test: soma test game_2048.cell
A Soma program from the games part of the verified corpus: 77 lines, cells Game2048, Game2048Tests. It passes soma check with soma 3.2.4.
How it is built
The cell Game2048 declares 3 signals in its face: compact(row: List) -> List, slide_left(row: List) -> Map and demo() -> Map. Handlers: compact (line 20), slide_left (line 28) and demo (line 51).
The file also holds 1 test cell (Game2048Tests) with 12 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 = truegame_2048.cell
// ============================================================================
// 2048 — slide-and-merge a single row (the core move primitive)
// ============================================================================
//
// slide_left() compacts a row toward the left, merging equal adjacent tiles
// exactly once per move (left-to-right), and reports the points scored (sum of
// merged tiles). A full board move is four row slides; this is that primitive.
//
// Run: soma run game_2048.cell --signal demo
// Test: soma test game_2048.cell
cell Game2048 {
face {
signal compact(row: List) -> List
signal slide_left(row: List) -> Map
signal demo() -> Map
}
// Drop zeros, preserving order of non-zero tiles.
on compact(row: List) {
let out = []
for v in row {
if v != 0 { out = push(out, v) }
}
return out
}
on slide_left(row: List) {
let tiles = compact(row)
let merged = []
let score = 0
let i = 0
while i < len(tiles) {
if i + 1 < len(tiles) && tiles[i] == tiles[i + 1] {
let v = tiles[i] * 2
merged = push(merged, v)
score = score + v
i = i + 2
} else {
merged = push(merged, tiles[i])
i = i + 1
}
}
// pad back to the original width with zeros
while len(merged) < len(row) {
merged = push(merged, 0)
}
return map("row", merged, "score", score)
}
on demo() {
// [2,2,4,0] → [4,4,0,0] scoring 4
return slide_left([2, 2, 4, 0])
}
}
cell test Game2048Tests {
rules {
// basic merge of the leading pair
assert slide_left([2, 2, 4, 0]).row[0] == 4
assert slide_left([2, 2, 4, 0]).row[1] == 4
assert slide_left([2, 2, 4, 0]).score == 4
// a tile only merges once: [2,2,2,2] → [4,4,0,0] scoring 8
assert slide_left([2, 2, 2, 2]).row[0] == 4
assert slide_left([2, 2, 2, 2]).row[1] == 4
assert slide_left([2, 2, 2, 2]).row[2] == 0
assert slide_left([2, 2, 2, 2]).score == 8
// gaps collapse, no merge
assert slide_left([0, 4, 0, 2]).row[0] == 4
assert slide_left([0, 4, 0, 2]).row[1] == 2
assert slide_left([0, 4, 0, 2]).score == 0
// already-merged trailing pair: [4,2,2,0] → [4,4,0,0]
assert slide_left([4, 2, 2, 0]).score == 4
assert compact([0, 0, 8, 0])[0] == 8
}
}What the toolchain says
$ soma check game_2048.cell
✓ All checks passed.$ soma test game_2048.cell
test Game2048Tests ...
✓ assert slide_left([2, 2, 4, 0]).row[0] == 4
✓ assert slide_left([2, 2, 4, 0]).row[1] == 4
✓ assert slide_left([2, 2, 4, 0]).score == 4
✓ assert slide_left([2, 2, 2, 2]).row[0] == 4
✓ assert slide_left([2, 2, 2, 2]).row[1] == 4
✓ assert slide_left([2, 2, 2, 2]).row[2] == 0
✓ assert slide_left([2, 2, 2, 2]).score == 8
✓ assert slide_left([0, 4, 0, 2]).row[0] == 4
✓ assert slide_left([0, 4, 0, 2]).row[1] == 2
✓ assert slide_left([0, 4, 0, 2]).score == 0
✓ assert slide_left([4, 2, 2, 0]).score == 4
✓ assert compact([0, 0, 8, 0])[0] == 8
12 tests: 12 passed, 0 failed$ soma verify game_2048.cell
Verifying game_2048.cell...
Game2048/termination:
⚠ handler `slide_left`: while-loop without provable termination bound (write `while [loop_bound(N)] cond { … }`, or a for-loop over a bounded range)
⚠ `demo` calls `Game2048.slide_left`, which may not terminate
0 passed, 2 warnings, 0 failures
Temporal: 0 passed, 0 failed
VERIFY OK