soma.cell

Online game-match lifecycle

lobby -> starting -> playing -> finished, with an abandoned escape from the early phases. A rematch flows finished -> lobby, but because both finished and abandoned are reachable terminals listed in [verify], the rematch loop does not break liveness. Memory holds the player roster.

A Soma program from the state_machines part of the verified corpus: 81 lines, cells GameMatch, GameMatchTests. It passes soma check with soma 3.2.4.

How it is built

The cell GameMatch declares 8 signals in its face: create(id: String) -> String, join_player(id: String, name: String) -> Map, begin(id: String) -> String, play(id: String) -> String, finish(id: String) -> String, abandon(id: String) -> String, rematch(id: String) -> String and phase(id: String) -> String; keeps 1 memory slot: roster (Map<String, Int>, persistent and consistent) under the invariant roster >= 0 && roster <= 4. Its state machine session starts in lobby and allows 7 transitions: lobby -> starting, starting -> playing, playing -> finished, finished -> lobby, lobby -> abandoned, starting -> abandoned and playing -> abandoned. abandoned is terminal. Handlers: create (line 35), join_player (line 40), begin (line 46), play (line 47), finish (line 48), abandon (line 49), rematch (line 50) and phase (line 51).

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

Generated from soma describe on this file.

The source

game_match.cell

// Online game-match lifecycle.
// lobby -> starting -> playing -> finished, with an abandoned escape from
// the early phases. A rematch flows finished -> lobby, but because both
// finished and abandoned are reachable terminals listed in [verify], the
// rematch loop does not break liveness. Memory holds the player roster.

cell GameMatch {
    face {
        signal create(id: String) -> String
        signal join_player(id: String, name: String) -> Map
        signal begin(id: String) -> String
        signal play(id: String) -> String
        signal finish(id: String) -> String
        signal abandon(id: String) -> String
        signal rematch(id: String) -> String
        signal phase(id: String) -> String
    }

    memory {
        roster: Map<String, Int> [persistent, consistent]
        invariant roster >= 0 && roster <= 4
    }

    state session {
        initial: lobby
        lobby    -> starting
        starting -> playing
        playing  -> finished
        finished -> lobby
        lobby    -> abandoned
        starting -> abandoned
        playing  -> abandoned
    }

    on create(id: String) {
        roster.set(id, 0)
        return get_status(id)
    }

    on join_player(id: String, name: String) {
        let n = (roster.get(id) ?? 0) + 1
        roster.set(id, n)
        return map("players", n, "joined", name)
    }

    on begin(id: String)   { transition(id, "starting")  return get_status(id) }
    on play(id: String)    { transition(id, "playing")   return get_status(id) }
    on finish(id: String)  { transition(id, "finished")  return get_status(id) }
    on abandon(id: String) { transition(id, "abandoned") return get_status(id) }
    on rematch(id: String) { transition(id, "lobby")     return get_status(id) }
    on phase(id: String)   { return get_status(id) }
}

cell test GameMatchTests {
    rules {
        // full match then rematch back to lobby
        assert create("g1") == "lobby"
        assert join_player("g1", "ana").players == 1
        assert join_player("g1", "bo").players == 2
        assert begin("g1") == "starting"
        assert play("g1") == "playing"
        assert finish("g1") == "finished"
        assert rematch("g1") == "lobby"

        // abandon from the lobby
        assert create("g2") == "lobby"
        assert abandon("g2") == "abandoned"

        // roster invariant rejects a 5th player
        assert create("g3") == "lobby"
        assert join_player("g3", "a").players == 1
        assert join_player("g3", "b").players == 2
        assert join_player("g3", "c").players == 3
        assert join_player("g3", "d").players == 4
        assert_fails join_player("g3", "e")

        // illegal: cannot start playing straight from the lobby
        assert_fails play("g4")
    }
}

What the toolchain says

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

$ soma check game_match.cell
✓ All checks passed.
$ soma test game_match.cell
test GameMatchTests ...
  ✓ assert create("g1") == "lobby"
  ✓ assert join_player("g1", "ana").players == 1
  ✓ assert join_player("g1", "bo").players == 2
  ✓ assert begin("g1") == "starting"
  ✓ assert play("g1") == "playing"
  ✓ assert finish("g1") == "finished"
  ✓ assert rematch("g1") == "lobby"
  ✓ assert create("g2") == "lobby"
  ✓ assert abandon("g2") == "abandoned"
  ✓ assert create("g3") == "lobby"
  ✓ assert join_player("g3", "a").players == 1
  ✓ assert join_player("g3", "b").players == 2
  ✓ assert join_player("g3", "c").players == 3
  ✓ assert join_player("g3", "d").players == 4
  ✓ assert_fails join_player("g3", "e") — raised memory invariant violated on 'roster': roster >= 0 && roster <= 4 — rejected write of 5 (key "g3"); the slot is unchanged
  ✓ assert_fails play("g4") — raised invalid transition: lobby → playing. Current state: 'lobby'. Valid targets: [starting, abandoned]

16 tests: 16 passed, 0 failed
$ soma verify game_match.cell
Verifying game_match.cell...

State machine 'session': 5 states, initial 'lobby'
  States: [abandoned, finished, lobby, playing, starting]

  ✓ 5 states, 7 transitions
  ✓ all states reachable from 'lobby'
  ✓ terminal states: [abandoned]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 5 literal transitions, 0 dynamic)
  ✓ termination: all 8 handlers structurally terminate
  ✓ refinement: handler `begin` ⟶ {starting}
  ✓ refinement: handler `play` ⟶ {playing}
  ✓ refinement: handler `finish` ⟶ {finished}
  ✓ refinement: handler `abandon` ⟶ {abandoned}
  ✓ refinement: handler `rematch` ⟶ {lobby}
  · transition sources: `begin`, `play`, `finish`, `abandon`, `rematch` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)

GameMatch/invariants:

  ✓ invariant roster >= 0 && roster <= 4 — writer 'create' proven (writes 0)
  ✓ invariant roster >= 0 — writer 'join_player' proven by induction (writes n)
  ⚠ invariant roster >= 0 && roster <= 4 — runtime-checked (computed values): join_player → roster [roster <= 4] because `n` is only known to lie in [1, 5] — narrow it: `require n <= 4 else …`

14 passed, 1 warnings, 0 failures

  Temporal properties for 'session':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/state_machines/game_match.cell · examples/corpus/state_machines/game_match.cell