soma.cell

Nim — optimal play via the Sprague-Grundy nim-sum

In normal-play Nim a position is a loss for the player to move iff the XOR of all heap sizes is 0. nim_sum() computes it; best_move() finds a winning (heap, take) move when one exists, else surrenders. Run: soma run nim.cell --signal demo Test: soma test nim.cell

A Soma program from the games part of the verified corpus: 75 lines, cells Nim, NimTests. It passes soma check with soma 3.2.4.

How it is built

The cell Nim declares 4 signals in its face: nim_sum(heaps: List) -> Int, is_losing(heaps: List) -> Bool, best_move(heaps: List) -> Map and demo() -> Map. Handlers: nim_sum (line 20), is_losing (line 29), best_move (line 35) and demo (line 48).

The file also holds 1 test cell (NimTests) with 8 assertions, run by soma test.

Generated from soma describe on this file.

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 = true

nim.cell

// ============================================================================
// Nim — optimal play via the Sprague-Grundy nim-sum
// ============================================================================
//
// In normal-play Nim a position is a loss for the player to move iff the XOR
// of all heap sizes is 0. nim_sum() computes it; best_move() finds a winning
// (heap, take) move when one exists, else surrenders.
//
// Run:  soma run nim.cell --signal demo
// Test: soma test nim.cell

cell Nim {
    face {
        signal nim_sum(heaps: List) -> Int
        signal is_losing(heaps: List) -> Bool
        signal best_move(heaps: List) -> Map
        signal demo() -> Map
    }

    on nim_sum(heaps: List) {
        let x = 0
        for h in heaps {
            x = bxor(x, h)
        }
        return x
    }

    // A position with nim-sum 0 is a loss for the player about to move.
    on is_losing(heaps: List) {
        return nim_sum(heaps) == 0
    }

    // Find a heap whose target size (heap XOR nim_sum) is smaller, then take
    // down to it. Returns heap=-1 when the position is already lost.
    on best_move(heaps: List) {
        let s = nim_sum(heaps)
        if s == 0 { return map("heap", -1, "take", 0) }
        let result = map("heap", -1, "take", 0)
        for i in range(0, len(heaps)) {
            let target = bxor(heaps[i], s)
            if target < heaps[i] && result.heap == -1 {
                result = map("heap", i, "take", heaps[i] - target)
            }
        }
        return result
    }

    on demo() {
        // 3,4,5 → nim-sum 2, winning move exists
        return map(
            "nim_sum", nim_sum([3, 4, 5]),
            "move", best_move([3, 4, 5])
        )
    }
}

cell test NimTests {
    rules {
        // 1^2^3 = 0 → losing position
        assert nim_sum([1, 2, 3]) == 0
        assert is_losing([1, 2, 3]) == true
        // 3^4^5 = 2 → winning
        assert nim_sum([3, 4, 5]) == 2
        assert is_losing([3, 4, 5]) == false
        // the winning move turns it into a 0-sum position for the opponent
        // take from heap, leaving nim-sum 0
        assert best_move([3, 4, 5]).heap == 0
        assert best_move([3, 4, 5]).take == 2
        // applying the move yields a losing position [1,4,5]
        assert nim_sum([1, 4, 5]) == 0
        // a lost position offers no move
        assert best_move([1, 2, 3]).heap == -1
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check nim.cell
✓ All checks passed.
$ soma test nim.cell
test NimTests ...
  ✓ assert nim_sum([1, 2, 3]) == 0
  ✓ assert is_losing([1, 2, 3]) == true
  ✓ assert nim_sum([3, 4, 5]) == 2
  ✓ assert is_losing([3, 4, 5]) == false
  ✓ assert best_move([3, 4, 5]).heap == 0
  ✓ assert best_move([3, 4, 5]).take == 2
  ✓ assert nim_sum([1, 4, 5]) == 0
  ✓ assert best_move([1, 2, 3]).heap == -1

8 tests: 8 passed, 0 failed
$ soma verify nim.cell
Verifying nim.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)

Builtins used

bxor, len, map, range

Source file: /corpus/games/nim.cell · examples/corpus/games/nim.cell