soma.cell

Card deck — build, deterministic Fisher-Yates shuffle, and deal

A 52-card deck is built as "Rank of Suit" strings. shuffle() uses a seeded linear-congruential PRNG so the permutation is reproducible (no random()). deal() splits the deck into n hands of k cards round-robin. Run: soma run card_deck.cell --signal demo Test: soma test card_deck.cell

A Soma program from the games part of the verified corpus: 85 lines, cells CardDeck, CardDeckTests. It passes soma check with soma 3.2.4.

How it is built

The cell CardDeck declares 4 signals in its face: new_deck() -> List, shuffle(deck: List, seed: Int) -> List, deal(deck: List, hands: Int, cards: Int) -> List and demo() -> Map. Handlers: new_deck (line 20), shuffle (line 33), deal (line 50) and demo (line 62).

The file also holds 1 test cell (CardDeckTests) with 7 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

card_deck.cell

// ============================================================================
// Card deck — build, deterministic Fisher-Yates shuffle, and deal
// ============================================================================
//
// A 52-card deck is built as "Rank of Suit" strings. shuffle() uses a seeded
// linear-congruential PRNG so the permutation is reproducible (no random()).
// deal() splits the deck into n hands of k cards round-robin.
//
// Run:  soma run card_deck.cell --signal demo
// Test: soma test card_deck.cell

cell CardDeck {
    face {
        signal new_deck() -> List
        signal shuffle(deck: List, seed: Int) -> List
        signal deal(deck: List, hands: Int, cards: Int) -> List
        signal demo() -> Map
    }

    on new_deck() {
        let suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
        let ranks = ["2","3","4","5","6","7","8","9","10","J","Q","K","A"]
        let deck = []
        for s in suits {
            for r in ranks {
                deck = push(deck, "{r} of {s}")
            }
        }
        return deck
    }

    // Fisher-Yates driven by a seeded LCG → deterministic permutation.
    on shuffle(deck: List, seed: Int) {
        let d = deck
        let state = seed
        let i = len(d) - 1
        while i > 0 {
            // Numerical Recipes LCG, kept in 31 bits.
            state = band(state * 1103515245 + 12345, 2147483647)
            let j = state % (i + 1)
            let tmp = d[i]
            d[i] = d[j]
            d[j] = tmp
            i = i - 1
        }
        return d
    }

    // Round-robin deal: hand h gets cards h, h+hands, h+2*hands, ...
    on deal(deck: List, hands: Int, cards: Int) {
        let out = []
        for h in range(0, hands) {
            let hand = []
            for k in range(0, cards) {
                hand = push(hand, deck[h + k * hands])
            }
            out = push(out, hand)
        }
        return out
    }

    on demo() {
        let d = new_deck()
        let s = shuffle(d, 42)
        let hands = deal(s, 4, 5)
        return map("size", len(d), "top", s[0], "hands", len(hands))
    }
}

cell test CardDeckTests {
    rules {
        assert len(new_deck()) == 52
        // a shuffle is a permutation — same length, all cards retained
        assert len(shuffle(new_deck(), 7)) == 52
        // determinism: same seed → identical top card
        assert shuffle(new_deck(), 99)[0] == shuffle(new_deck(), 99)[0]
        // dealing 4 hands of 5 yields 4 hands of 5 cards each
        assert len(deal(new_deck(), 4, 5)) == 4
        assert len(deal(new_deck(), 4, 5)[0]) == 5
        // an unshuffled deck starts with "2 of Clubs"
        assert new_deck()[0] == "2 of Clubs"
        assert new_deck()[51] == "A of Spades"
    }
}

What the toolchain says

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

$ soma check card_deck.cell
✓ All checks passed.
$ soma test card_deck.cell
test CardDeckTests ...
  ✓ assert len(new_deck()) == 52
  ✓ assert len(shuffle(new_deck(), 7)) == 52
  ✓ assert shuffle(new_deck(), 99)[0] == shuffle(new_deck(), 99)[0]
  ✓ assert len(deal(new_deck(), 4, 5)) == 4
  ✓ assert len(deal(new_deck(), 4, 5)[0]) == 5
  ✓ assert new_deck()[0] == "2 of Clubs"
  ✓ assert new_deck()[51] == "A of Spades"

7 tests: 7 passed, 0 failed
$ soma verify card_deck.cell
Verifying card_deck.cell...

CardDeck/termination:

  ⚠ handler `shuffle`: while-loop without provable termination bound (write `while [loop_bound(N)] cond { … }`, or a for-loop over a bounded range)
  ⚠ `demo` calls `CardDeck.shuffle`, which may not terminate

0 passed, 2 warnings, 0 failures
Temporal: 0 passed, 0 failed
VERIFY OK

Builtins used

band, len, map, push, range

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