feat(ludic.steps): chapters of steps, their kinds, progress and a party's pooled shares

A step is (kind, param, need) and a question about state: a kind has a
shape (yes, a count, a mask counted, every bit of a mask) and a pool
(world, any, sum, or, max, each), handed in by the game as StepsKind
records. An arc is chapters of up to STEPS_PER_CHAPTER steps; steps_check
sticks each met step of the current chapter (STEPS_MET) and opens the
next when all are (STEPS_CHAPTER), so a step met before its chapter
ticks at once. With company each seat's shares are held per chapter,
pooled as the kind says (steps_met, steps_party_got), counted for an
EACH step (steps_each, steps_lacking), and a leaver takes theirs along.

Port: StepsWorld { value(kind, param, player) (required), party,
present }. The package knows no kind's meaning and no chapter's words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 06:02:39 +03:00
parent 4318adc98c
commit 31e6740033
10 changed files with 719 additions and 0 deletions

View file

@ -0,0 +1,72 @@
# ludic.steps
Chapters made of steps. A step is three numbers - a **kind**, a **param** and a **need** - and a
question about the state of the game ("five firewood gathered", "reputation 25", "all three signs
stood at"), never a count of what happened while its chapter was open: so a step met before its
chapter opens ticks the moment it is looked at. A chapter is a table of up to six steps; the arc
is a table of chapters; the current one ticks its steps as they are met and moves on when all
are. With company, every player's share of each step is pooled as its kind says. Uses
[`ludic.base`](../ludic.base/README.md) and nothing else.
```ludic
import "ludic.steps"
```
The kinds, the chapters and every word about them are the game's content: the package knows a
kind's **shape** and **pool**, never what it measures or what a chapter is called.
## Kinds
```ludic
property StepsKind {
key: string
shape: int # STEPS_SHAPE_YES (met or not), _COUNT (a number against the need),
# _MASK (bits counted against the need), _MASK_ALL (every bit of param)
pool: int # STEPS_POOL_WORLD (the same for everyone: asked of this machine), _ANY (anyone
# having done it), _SUM (the counts add up), _OR (the masks union), _MAX (the
# best one), _EACH (every player needs their own)
shows: bool # a panel shows its count against what it wants
}
```
A kind is its place in the list handed to `steps_kinds`, which is how a game's registry
(`registry StepKinds of StepsKind from "steps.lres"`) hands it in.
## The port
```ludic
port StepsWorld {
value: fn(int, int, int) -> int # (kind, param, player): a count, a mask, or 1/0; 0 is this machine
party: fn() -> bool # is a party playing (unbound: no)
present: fn(int) -> bool # is a seat taken (unbound: seat 0 only)
}
```
`value` has no default: a game that uses steps binds it (`bind StepsWorld { value: fn ... }`).
## API
| | |
| --- | --- |
| `steps_kinds(ks)`, `steps_config(players)` | the kinds; how many seats a party has (12) |
| `steps_eval(kind, param, need)`, `steps_got(kind, param)`, `steps_want(kind, param, need)`, `steps_shape`, `steps_pool`, `steps_shows` | a step against this machine's state; its count (-1 when not shown) and what it wants |
| `steps_mine(kind, param, need)`, `steps_from(kind, param, need, v, players, have)`, `steps_got_from(kind, param, v)` | this machine's share (a mask, a count, 1/0, nothing for the world's), and a verdict or a count from shares already pooled |
| `steps_arc_clear()`, `steps_chapter_add() -> int`, `steps_add(kind, param, need) -> bool` | build an arc (false: no chapter, or it has `STEPS_PER_CHAPTER`) |
| `steps_count()`, `steps_in(ch)`, `steps_kind(ch, i)` / `_param` / `_need`, `steps_kind_at(k)` / `_param_at` / `_need_at` | the tables, by chapter and step or by the flat index `ch * STEPS_PER_CHAPTER + i` |
| `steps_current()`, `steps_done_mask()`, `steps_done(i)`, `steps_finished()`, `steps_set_current(ch)`, `steps_set_done(mask)` | the progress, and setting it outright (a load, the host's word, a test) |
| `steps_check()` | once a frame on the machine that owns the story: `STEPS_MET` for each step newly met, `STEPS_CHAPTER` and the next chapter when all are |
| `steps_check_at(ch, i)`, `steps_wants(kind, param)` | a step of any chapter against this machine; is an open step of the current chapter asking for a kind (and a param, unless < 0) |
| `steps_my_shares() -> []int`, `steps_share(p, ch, shares)`, `steps_share_of(p, i)`, `steps_leave(p)`, `steps_party_reset()` | this machine's shares to send; a teammate's as they arrived (ignored when about another chapter); a seat emptied |
| `steps_met(i)`, `steps_party_got(i)`, `steps_players()`, `steps_each(i)`, `steps_lacking(i)` | the party's verdict and count for step i of the current chapter; how many seats are taken, how many carry an EACH step's thing, and which do not |
| `steps_facts() -> Queue<StepsFact>` | `{ what, chapter, step }` |
| `steps_popcount(m)`, `steps_reset()` | bits in a mask; back to nothing (tests) |
## Tests
```bash
ludic build packages/ludic.steps/tests/steps_test.ludic --headless -o /tmp/steps_test && /tmp/steps_test
```
A fake world of values and seats: every shape, an arc that ticks and moves on (and a step met
before its chapter), the six-step cap, and every pool across a party, a leaver and a share about
another chapter.

View file

@ -0,0 +1,78 @@
# arc.ludic - an arc is a table of chapters, each a table of up to STEPS_PER_CHAPTER steps. The
# game builds one with steps_chapter_add / steps_add; what a chapter is called is the game's.
# empty the tables for another arc (the progress goes back to the first chapter)
export function steps_arc_clear() -> void {
steps__n = new []int
steps__kind = new []int
steps__param = new []int
steps__need = new []int
steps__cur = 0
steps__done = 0
}
function steps__ready() -> void { if steps__n == null { steps_arc_clear() } }
# open the next chapter; its number
export function steps_chapter_add() -> int {
steps__ready()
push(steps__n, 0)
for i in 0 .. STEPS_PER_CHAPTER {
push(steps__kind, -1)
push(steps__param, 0)
push(steps__need, 0)
}
return len(steps__n) - 1
}
# a step in the chapter just opened; false when there is no chapter or it is full
export function steps_add(kind: int, param: int, need: int) -> bool {
steps__ready()
let c = len(steps__n) - 1
if c < 0 or steps__n[c] >= STEPS_PER_CHAPTER { return false }
let k = c * STEPS_PER_CHAPTER + steps__n[c]
steps__kind[k] = kind
steps__param[k] = param
steps__need[k] = need
steps__n[c] += 1
return true
}
export function steps_count() -> int {
if steps__n == null { return 0 }
return len(steps__n)
}
export function steps_in(ch: int) -> int {
if ch < 0 or ch >= steps_count() { return 0 }
return steps__n[ch]
}
# a step by its flat index, ch * STEPS_PER_CHAPTER + i (-1, 0, 0 past the table)
export function steps_kind_at(k: int) -> int {
if steps__kind == null or k < 0 or k >= len(steps__kind) { return -1 }
return steps__kind[k]
}
export function steps_param_at(k: int) -> int {
if steps__param == null or k < 0 or k >= len(steps__param) { return 0 }
return steps__param[k]
}
export function steps_need_at(k: int) -> int {
if steps__need == null or k < 0 or k >= len(steps__need) { return 0 }
return steps__need[k]
}
export function steps_kind(ch: int, i: int) -> int { return steps_kind_at(ch * STEPS_PER_CHAPTER + i) }
export function steps_param(ch: int, i: int) -> int { return steps_param_at(ch * STEPS_PER_CHAPTER + i) }
export function steps_need(ch: int, i: int) -> int { return steps_need_at(ch * STEPS_PER_CHAPTER + i) }
# the chapter being played, and which of its steps are done (sticky)
export function steps_current() -> int { return steps__cur }
export function steps_done_mask() -> int { return steps__done }
export function steps_done(i: int) -> bool { return (steps__done & (1 << i)) != 0 }
export function steps_finished() -> bool { return steps__cur >= steps_count() }
# the progress outright: a load, the host's word, a test
export function steps_set_current(ch: int) -> void { steps__cur = ch }
export function steps_set_done(mask: int) -> void { steps__done = mask }

View file

@ -0,0 +1,43 @@
# check.ludic - once a frame on the machine that owns the story: every open step of the current
# chapter that is met now sticks (STEPS_MET), and a chapter with all of them done moves the arc on
# (STEPS_CHAPTER). A step asks about STATE, so one met before its chapter opened ticks at once.
export function steps_check() -> void {
if steps_finished() { return }
let ch = steps__cur
let n = steps_in(ch)
var all = true
for i in 0 .. n {
if not steps_done(i) and steps_met(i) {
steps__done = steps__done | (1 << i)
steps__fact(STEPS_MET, ch, i)
}
if not steps_done(i) { all = false }
}
if not all { return }
steps__cur = ch + 1
steps__done = 0
steps__fact(STEPS_CHAPTER, ch, n)
}
# is step i of chapter ch met by this machine's own state? (a chapter not being played, a panel)
export function steps_check_at(ch: int, i: int) -> bool {
if ch < 0 or ch >= steps_count() or i < 0 or i >= steps_in(ch) { return false }
return steps_eval(steps_kind(ch, i), steps_param(ch, i), steps_need(ch, i))
}
# is an open step of the current chapter asking for this kind (and this param, unless < 0)?
export function steps_wants(kind: int, param: int) -> bool {
if steps_finished() { return false }
for i in 0 .. steps_in(steps__cur) {
if steps_done(i) or steps_kind(steps__cur, i) != kind { continue }
if param < 0 or steps_param(steps__cur, i) == param { return true }
}
return false
}
# back to no arc and no party (tests)
export function steps_reset() -> void {
steps_arc_clear()
steps__party_clear()
q_clear(steps_facts())
}

View file

@ -0,0 +1,27 @@
# each.ludic - a step every player must meet in their own pack: how many seats are taken, how
# many of them carry it, and which do not
export function steps_players() -> int {
var n = 0
for p in 0 .. steps__players { if StepsWorld.present(p) { n += 1 } }
return n
}
# how many seats taken have step i's thing (seat 0 counted live)
export function steps_each(i: int) -> int {
steps__cols()
var n = 0
for p in 0 .. steps__players {
if StepsWorld.present(p) and steps__col[p * STEPS_PER_CHAPTER + i] != 0 { n += 1 }
}
return n
}
# the seats that do not have it yet
export function steps_lacking(i: int) -> []int {
steps__cols()
let out = new []int
for p in 0 .. steps__players {
if StepsWorld.present(p) and steps__col[p * STEPS_PER_CHAPTER + i] == 0 { push(out, p) }
}
return out
}

View file

@ -0,0 +1,11 @@
# ludic.steps - a step is a question about state, answered by the game through a port; a chapter
# is a table of steps; the current one ticks its steps as they are met and moves on when all are
module ludic_steps uses ludic_base
numbers float
import "ludic.base"
import "state.ludic"
import "kinds.ludic"
import "arc.ludic"
import "check.ludic"
import "party.ludic"
import "each.ludic"

View file

@ -0,0 +1,88 @@
# kinds.ludic - the kinds the game hands in, and a step answered from its kind's shape: met or
# not, its count, what it wants. A kind past the list is a plain yes, the world's.
export function steps_kinds(ks: []StepsKind) -> void { steps__kinds = ks }
function steps__known(kind: int) -> bool { return steps__kinds != null and kind >= 0 and kind < len(steps__kinds) }
export function steps_shape(kind: int) -> int {
if not steps__known(kind) { return STEPS_SHAPE_YES }
return steps__kinds[kind].shape
}
export function steps_pool(kind: int) -> int {
if not steps__known(kind) { return STEPS_POOL_WORLD }
return steps__kinds[kind].pool
}
export function steps_shows(kind: int) -> bool {
if not steps__known(kind) { return false }
return steps__kinds[kind].shows
}
function steps__value(kind: int, param: int) -> int { return StepsWorld.value(kind, param, 0) }
# is (kind, param, need) met by this machine's own state?
export function steps_eval(kind: int, param: int, need: int) -> bool {
if kind < 0 { return false }
return steps__met_by(kind, param, need, steps__value(kind, param))
}
function steps__met_by(kind: int, param: int, need: int, v: int) -> bool {
let sh = steps_shape(kind)
if sh == STEPS_SHAPE_COUNT { return v >= need }
if sh == STEPS_SHAPE_MASK { return steps_popcount(v) >= need }
if sh == STEPS_SHAPE_MASK_ALL { return (v & param) == param }
return v != 0
}
# how far a counted step has got, -1 for a kind a panel shows no count for
export function steps_got(kind: int, param: int) -> int {
if kind < 0 or not steps_shows(kind) { return -1 }
return steps__shaped(kind, param, steps__value(kind, param))
}
function steps__shaped(kind: int, param: int, v: int) -> int {
let sh = steps_shape(kind)
if sh == STEPS_SHAPE_MASK { return steps_popcount(v) }
if sh == STEPS_SHAPE_MASK_ALL { return steps_popcount(v & param) }
return v
}
export function steps_want(kind: int, param: int, need: int) -> int {
if steps_shape(kind) == STEPS_SHAPE_MASK_ALL { return steps_popcount(param) }
return need
}
# THIS machine's share: a mask for OR, a count for SUM and MAX, 1 or 0 for ANY and EACH, and
# nothing for a question about the world
export function steps_mine(kind: int, param: int, need: int) -> int {
let how = steps_pool(kind)
if how == STEPS_POOL_WORLD { return 0 }
if how == STEPS_POOL_ANY or how == STEPS_POOL_EACH {
if steps_eval(kind, param, need) { return 1 }
return 0
}
return steps__value(kind, param)
}
# the party's verdict from what it has between it: `v` combined as the pool says; for EACH,
# how many are in the party and how many carry it
export function steps_from(kind: int, param: int, need: int, v: int, players: int, have: int) -> bool {
let how = steps_pool(kind)
if how == STEPS_POOL_WORLD { return steps_eval(kind, param, need) }
if how == STEPS_POOL_ANY { return v != 0 }
if how == STEPS_POOL_EACH { return players > 0 and have >= players }
if how == STEPS_POOL_OR {
if steps_shape(kind) == STEPS_SHAPE_MASK_ALL { return (v & param) == param }
return steps_popcount(v) >= need
}
return v >= need
}
# the count from a share already combined across the party
export function steps_got_from(kind: int, param: int, v: int) -> int {
let how = steps_pool(kind)
if not steps_shows(kind) { return -1 }
if how == STEPS_POOL_OR or how == STEPS_POOL_SUM or how == STEPS_POOL_MAX { return steps__shaped(kind, param, v) }
return steps_got(kind, param)
}

View file

@ -0,0 +1,6 @@
# ludic.steps - chapters made of steps (kind, param, need), step kinds with a shape and a party
# pool, progress and done masks, and a party's shares pooled into one verdict. Uses ludic.base and
# nothing else. See README.md.
package "ludic.steps"
version "0.1.0"
kind source

View file

@ -0,0 +1,92 @@
# party.ludic - with company every player reports what THEY have toward each step of the current
# chapter (steps_mine) and the owner of the story answers the step from the lot. Held per seat,
# so a player who leaves takes their column with them; seat 0 is this machine, read live.
var steps__players: int = 12
var steps__col: []int = null # players * STEPS_PER_CHAPTER shares
var steps__col_ch: int = -1 # the chapter they are about
# how many seats a party has
export function steps_config(players: int) -> void {
steps__players = players
steps__col = null
}
function steps__party_clear() -> void {
steps__col = new []int
for i in 0 .. steps__players * STEPS_PER_CHAPTER { push(steps__col, 0) }
steps__col_ch = steps__cur
}
# a party over (or a new one): every column forgotten
export function steps_party_reset() -> void { steps__col = null }
# the columns, emptied when the chapter they were about is not this one; seat 0 refilled
function steps__cols() -> void {
if steps__col == null or steps__col_ch != steps__cur { steps__party_clear() }
if steps_finished() { return }
for i in 0 .. steps_in(steps__cur) {
steps__col[i] = steps_mine(steps_kind(steps__cur, i), steps_param(steps__cur, i), steps_need(steps__cur, i))
}
}
# this machine's shares for the current chapter, to send to whoever owns the story
export function steps_my_shares() -> []int {
steps__cols()
let out = new []int
for i in 0 .. STEPS_PER_CHAPTER { push(out, steps__col[i]) }
return out
}
# a teammate's shares, as they sent them; ignored when they were about another chapter
export function steps_share(p: int, ch: int, shares: []int) -> void {
if p <= 0 or p >= steps__players or ch != steps__cur { return }
steps__cols()
for i in 0 .. min(len(shares), STEPS_PER_CHAPTER) { steps__col[p * STEPS_PER_CHAPTER + i] = shares[i] }
}
export function steps_share_of(p: int, i: int) -> int {
steps__cols()
return steps__col[p * STEPS_PER_CHAPTER + i]
}
# a seat emptied: what it had goes with it
export function steps_leave(p: int) -> void {
if steps__col == null or p <= 0 or p >= steps__players { return }
for i in 0 .. STEPS_PER_CHAPTER { steps__col[p * STEPS_PER_CHAPTER + i] = 0 }
}
# the shares of every seat taken, combined as the pool says (seat 0 is the start)
function steps__combined(i: int, how: int) -> int {
var v = steps__col[i]
for p in 1 .. steps__players {
if not StepsWorld.present(p) { continue }
let c = steps__col[p * STEPS_PER_CHAPTER + i]
if how == STEPS_POOL_ANY or how == STEPS_POOL_OR { v = v | c }
else if how == STEPS_POOL_SUM { v += c }
else if how == STEPS_POOL_MAX { if c > v { v = c } }
}
return v
}
# the answer to step i of the current chapter: solo this is steps_eval and nothing more
export function steps_met(i: int) -> bool {
let ch = steps__cur
let kind = steps_kind(ch, i)
if not StepsWorld.party() { return steps_eval(kind, steps_param(ch, i), steps_need(ch, i)) }
let how = steps_pool(kind)
if how == STEPS_POOL_WORLD { return steps_eval(kind, steps_param(ch, i), steps_need(ch, i)) }
steps__cols()
return steps_from(kind, steps_param(ch, i), steps_need(ch, i), steps__combined(i, how), steps_players(), steps_each(i))
}
# the party's count for step i of the current chapter, -1 for one that is not a tally
export function steps_party_got(i: int) -> int {
let ch = steps__cur
let kind = steps_kind(ch, i)
let param = steps_param(ch, i)
if steps_got(kind, param) < 0 { return -1 }
let how = steps_pool(kind)
if not StepsWorld.party() or how == STEPS_POOL_WORLD or how == STEPS_POOL_ANY or how == STEPS_POOL_EACH { return steps_got(kind, param) }
steps__cols()
return steps_got_from(kind, param, steps__combined(i, how))
}

View file

@ -0,0 +1,78 @@
# state.ludic - the port, the kinds' shapes and pools, the facts, and the arc being played
# what a step kind measures, asked of the game: a count, a mask, or 1 / 0, for one player (0 is
# this machine's own); whether a party is playing, and whether a seat in it is taken
export port StepsWorld {
value: fn(int, int, int) -> int
party: fn() -> bool = fn steps__solo
present: fn(int) -> bool = fn steps__only_me
}
function steps__solo() -> bool { return false }
function steps__only_me(p: int) -> bool { return p == 0 }
# a kind's shape: met or not; a number against the need; a mask whose bits are counted against
# the need; every bit of the mask in `param`
export const STEPS_SHAPE_YES: int = 0
export const STEPS_SHAPE_COUNT: int = 1
export const STEPS_SHAPE_MASK: int = 2
export const STEPS_SHAPE_MASK_ALL: int = 3
# how a party combines a kind: the same for everyone already (asked of this machine); anyone
# having done it; the counts add up; the masks union; the best one; every player needs their own
export const STEPS_POOL_WORLD: int = 0
export const STEPS_POOL_ANY: int = 1
export const STEPS_POOL_SUM: int = 2
export const STEPS_POOL_OR: int = 3
export const STEPS_POOL_MAX: int = 4
export const STEPS_POOL_EACH: int = 5
# the most steps a chapter carries: its done steps are a bit mask
export const STEPS_PER_CHAPTER: int = 6
export property StepsKind {
key: string = ""
shape: int = STEPS_SHAPE_YES
pool: int = STEPS_POOL_WORLD
shows: bool = false # a panel shows its count against what it wants
}
export const STEPS_MET: int = 0 # a step of the current chapter was met (chapter, step)
export const STEPS_CHAPTER: int = 1 # every step of a chapter is met, and the next one is open
export property StepsFact {
what: int = 0
chapter: int = 0
step: int = 0
}
var steps__kinds: []StepsKind = null
var steps__n: []int = null # steps per chapter
var steps__kind: []int = null # STEPS_PER_CHAPTER per chapter
var steps__param: []int = null
var steps__need: []int = null
var steps__cur: int = 0
var steps__done: int = 0
var steps__facts: Queue<StepsFact> = null
export function steps_facts() -> Queue<StepsFact> {
if steps__facts == null { steps__facts = queue_new("steps.facts") }
return steps__facts
}
function steps__fact(what: int, ch: int, i: int) -> void {
let f = new StepsFact
f.what = what
f.chapter = ch
f.step = i
q_push(steps_facts(), f)
}
export function steps_popcount(m: int) -> int {
var n = 0
var v = m
while v != 0 {
n += v & 1
v = (v >> 1) & 0x7FFFFFFF
}
return n
}

View file

@ -0,0 +1,224 @@
# steps_test.ludic - shapes answered from a fake world, an arc that ticks and moves on, and a party
# whose shares pool as each kind says
import "ludic.steps"
import "ludic.base"
program StepsTest {
numbers float
var vals: []int = null # value of kind k for this machine
var party: bool = false
var seats: int = 1 # seats 0 .. seats-1 are taken
function fake_value(kind: int, param: int, player: int) -> int {
if player != 0 or kind < 0 or kind >= len(vals) { return 0 }
return vals[kind]
}
function fake_party() -> bool { return party }
function fake_present(p: int) -> bool { return p < seats }
bind StepsWorld { value: fn fake_value, party: fn fake_party, present: fn fake_present }
const K_WOOD: int = 0 # count, summed
const K_SIGNS: int = 1 # mask counted, or'd
const K_ALL: int = 2 # every bit of param, or'd
const K_FIRE: int = 3 # yes, anyone
const K_REP: int = 4 # count, best of
const K_COMPASS: int = 5 # yes, each
const K_HOUR: int = 6 # yes, the world's
function kind(shape: int, pool: int, shows: bool) -> StepsKind {
let k = new StepsKind
k.shape = shape
k.pool = pool
k.shows = shows
return k
}
function fresh() -> void {
steps_reset()
steps_config(4)
party = false
seats = 1
vals = new []int
for i in 0 .. 7 { push(vals, 0) }
let ks = new []StepsKind
push(ks, kind(STEPS_SHAPE_COUNT, STEPS_POOL_SUM, true))
push(ks, kind(STEPS_SHAPE_MASK, STEPS_POOL_OR, true))
push(ks, kind(STEPS_SHAPE_MASK_ALL, STEPS_POOL_OR, true))
push(ks, kind(STEPS_SHAPE_YES, STEPS_POOL_ANY, false))
push(ks, kind(STEPS_SHAPE_COUNT, STEPS_POOL_MAX, true))
push(ks, kind(STEPS_SHAPE_YES, STEPS_POOL_EACH, false))
push(ks, kind(STEPS_SHAPE_YES, STEPS_POOL_WORLD, false))
steps_kinds(ks)
}
# chapter 0: 5 wood, a fire; chapter 1: two signs, the compass
function two_chapters() -> void {
steps_chapter_add()
steps_add(K_WOOD, 0, 5)
steps_add(K_FIRE, 0, 1)
steps_chapter_add()
steps_add(K_SIGNS, 0, 2)
steps_add(K_COMPASS, 0, 1)
}
function facts_of(what: int) -> int {
let fs = q_drain(steps_facts())
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
}
function shares(a: int, b: int) -> []int {
let s = new []int
push(s, a)
push(s, b)
return s
}
test "a step is answered by its kind's shape" {
fresh()
vals[K_WOOD] = 4
expect(not steps_eval(K_WOOD, 0, 5))
vals[K_WOOD] = 5
expect(steps_eval(K_WOOD, 0, 5))
vals[K_SIGNS] = 5
expect(steps_eval(K_SIGNS, 0, 2))
expect_eq(steps_got(K_SIGNS, 0), 2)
vals[K_ALL] = 3
expect(not steps_eval(K_ALL, 7, 0))
expect_eq(steps_got(K_ALL, 7), 2)
expect_eq(steps_want(K_ALL, 7, 0), 3)
expect_eq(steps_got(K_FIRE, 0), -1)
}
test "an arc's steps stick as they are met, and a full chapter opens the next" {
fresh()
two_chapters()
expect_eq(steps_count(), 2)
expect_eq(steps_in(1), 2)
vals[K_FIRE] = 1
steps_check()
expect(steps_done(1))
expect(not steps_done(0))
expect_eq(facts_of(STEPS_MET), 1)
vals[K_FIRE] = 0
steps_check()
expect(steps_done(1))
vals[K_WOOD] = 9
steps_check()
expect_eq(steps_current(), 1)
expect_eq(steps_done_mask(), 0)
let fs = q_drain(steps_facts())
expect_eq(len(fs), 2)
expect_eq(fs[1].what, STEPS_CHAPTER)
expect_eq(fs[1].chapter, 0)
}
test "a step met before its chapter opens ticks the moment it is looked at" {
fresh()
two_chapters()
vals[K_SIGNS] = 3
vals[K_COMPASS] = 1
vals[K_WOOD] = 5
vals[K_FIRE] = 1
steps_check()
expect_eq(steps_current(), 1)
steps_check()
expect(steps_finished())
}
test "a chapter holds at most six steps" {
fresh()
expect(not steps_add(K_WOOD, 0, 1))
steps_chapter_add()
for i in 0 .. STEPS_PER_CHAPTER { expect(steps_add(K_WOOD, 0, i)) }
expect(not steps_add(K_WOOD, 0, 9))
}
test "the steps asked for right now" {
fresh()
two_chapters()
expect(steps_wants(K_FIRE, -1))
expect(not steps_wants(K_SIGNS, -1))
}
test "a tally adds up across the party, and a mask unions" {
fresh()
two_chapters()
party = true
seats = 2
vals[K_WOOD] = 3
expect(not steps_met(0))
steps_share(1, 0, shares(2, 0))
expect(steps_met(0))
expect_eq(steps_party_got(0), 5)
steps_set_current(1)
vals[K_SIGNS] = 1
steps_share(1, 1, shares(2, 0))
expect(steps_met(0))
}
test "anyone having done it is the party having done it" {
fresh()
two_chapters()
party = true
seats = 3
steps_share(2, 0, shares(0, 1))
expect(steps_met(1))
}
test "a thing in the pack is met when every player has one, and says who has not" {
fresh()
two_chapters()
steps_set_current(1)
party = true
seats = 3
vals[K_COMPASS] = 1
steps_share(1, 1, shares(0, 1))
expect(not steps_met(1))
expect_eq(steps_each(1), 2)
let lack = steps_lacking(1)
expect_eq(len(lack), 1)
expect_eq(lack[0], 2)
steps_share(2, 1, shares(0, 1))
expect(steps_met(1))
}
test "a player who leaves takes their share with them" {
fresh()
two_chapters()
party = true
seats = 2
steps_share(1, 0, shares(5, 0))
expect(steps_met(0))
steps_leave(1)
expect(not steps_met(0))
}
test "a share about another chapter is ignored, and the best of a tally is the best" {
fresh()
steps_chapter_add()
steps_add(K_REP, 0, 25)
party = true
seats = 2
steps_share(1, 3, shares(99, 0))
expect(not steps_met(0))
vals[K_REP] = 10
steps_share(1, 0, shares(30, 0))
expect(steps_met(0))
expect_eq(steps_party_got(0), 30)
}
test "the world's steps are asked of this machine alone" {
fresh()
steps_chapter_add()
steps_add(K_HOUR, 0, 1)
party = true
seats = 2
steps_share(1, 0, shares(1, 0))
expect(not steps_met(0))
vals[K_HOUR] = 1
expect(steps_met(0))
expect_eq(steps_mine(K_HOUR, 0, 1), 0)
}
}