From fe9d28bf4e71823102374d42d1e911688454f817 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Fri, 25 Sep 2026 09:36:10 +0300 Subject: [PATCH] feat(packages): ludic.hints - a rail of things that are true now and can be acted on now: cards as an open registry (urgency, a glyph or an item's picture, the chip's line, three steps and how not to be here again) asked through a port whether each is true, the most urgent few once a pass in registry order within an urgency, a card opened off the rail, walked along it and put down when its subject stops being true, and "I know this" muting kept by the card's key (a key this build lacks is carried through); opened, closed, muted and unmuted as facts Co-Authored-By: Claude Opus 5.5 --- packages/README.md | 1 + packages/ludic.hints/README.md | 75 +++++++++ packages/ludic.hints/card.ludic | 62 ++++++++ packages/ludic.hints/defs.ludic | 39 +++++ packages/ludic.hints/index.ludic | 11 ++ packages/ludic.hints/mute.ludic | 62 ++++++++ packages/ludic.hints/package.ludic | 5 + packages/ludic.hints/port.ludic | 8 + packages/ludic.hints/rail.ludic | 102 ++++++++++++ packages/ludic.hints/system.ludic | 10 ++ packages/ludic.hints/tests/cards.lres | 3 + packages/ludic.hints/tests/hints_test.ludic | 162 ++++++++++++++++++++ 12 files changed, 540 insertions(+) create mode 100644 packages/ludic.hints/README.md create mode 100644 packages/ludic.hints/card.ludic create mode 100644 packages/ludic.hints/defs.ludic create mode 100644 packages/ludic.hints/index.ludic create mode 100644 packages/ludic.hints/mute.ludic create mode 100644 packages/ludic.hints/package.ludic create mode 100644 packages/ludic.hints/port.ludic create mode 100644 packages/ludic.hints/rail.ludic create mode 100644 packages/ludic.hints/system.ludic create mode 100644 packages/ludic.hints/tests/cards.lres create mode 100644 packages/ludic.hints/tests/hints_test.ludic diff --git a/packages/README.md b/packages/README.md index fe0c5770..b40d3713 100644 --- a/packages/README.md +++ b/packages/README.md @@ -23,6 +23,7 @@ section. The rules are in [ludic.base](ludic.base/README.md). | [ludic.effects](ludic.effects/README.md) | timed modifiers that run down in game time (a meal's warmth, a drink's legs) | | [ludic.fire](ludic.fire/README.md) | a camp fire: fuel, the rain on it, warmth at a distance, whether it will cook | | [ludic.fishing](ludic.fishing/README.md) | a rod at the water: cast, bite, the reel's fight, the landing; species as an open registry | +| [ludic.hints](ludic.hints/README.md) | a rail of what is true now, most urgent first, each a card that teaches it and can be muted by key | | [ludic.i18n](ludic.i18n/README.md) | a game in any language: gettext `.po` files, patterns with holes, plurals, a mod folder, a font per language | | [ludic.inventory](ludic.inventory/README.md) | a pack: a count per kind, with room the game decides | | [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse | diff --git a/packages/ludic.hints/README.md b/packages/ludic.hints/README.md new file mode 100644 index 00000000..1a66c3a1 --- /dev/null +++ b/packages/ludic.hints/README.md @@ -0,0 +1,75 @@ +# ludic.hints + +A rail of things that are true now and can be acted on now - thirsty, the fire burning low, a +bear near, a job ready to hand in - sorted by how much they matter, each one a card that teaches +it in three steps and can be muted for good. Uses [`ludic.base`](../ludic.base/README.md) and +nothing else. + +```ludic +import "ludic.hints" +``` + +The package never reads a key and never draws: the game's input calls the verbs (the key opens +and puts down, the arrows walk, a third key mutes), its interface reads the rail and the card, +and its words are the cards' own text keys, translated where they are drawn. + +## Cards are data + +```ludic +def Hints from "assets/data/hints.lres" # the game's cards, in an open registry +``` + +``` +thirsty { + urgency: HINTS_NOTE, icon: "bottle", item: IT_BOTTLE + line: "Thirsty - drink", title: "Water" + s1: "Drink at any shore, for nothing", pic1: "waterfull" + s2: "Fill a bottle there as well", item2: IT_BOTTLE + s3: "Boil it in the pot to be safe", item3: IT_BOILED + later: "Buy or make a bottle, fill it at the shore and boil it." +} +``` + +`urgency` is `HINTS_TIP`, `HINTS_NOTE`, `HINTS_WARN` or `HINTS_URGENT`. A picture is a glyph by +name (`icon`, `picN`) or a thing's picture by number (`item`, `itemN`, -1 for none) - both the +game's to interpret. + +## The rules it keeps + +1. **The most urgent few.** Once a pass (a second by default) the rail is the true, unmuted cards, + most urgent first and in registry order within an urgency, up to `hints_config`'s count. +2. **A card follows its subject.** As the rail re-sorts under an open card, the pick follows it; + when what it was about stops being true (or the rail stops showing) it is put down, with no fact. +3. **A lesson lands once.** `hints_mute_card()` takes the open card off the rail until + `hints_unmute_all()`. Mutes are kept **by key**, so a card added or removed moves nobody else's, + and a key this build does not know is carried through a load and a save. + +## The port + +```ludic +export port HintsWorld { + true_now: fn(int) -> bool # is card i true now? pure and cheap (unbound: never) + showing: fn() -> bool # the rail is wanted: in play, the player's switch on (unbound: yes) +} +``` + +## API + +| | | +| --- | --- | +| `HintCard { key, urgency, icon, item, line, title, s1..s3, pic1..pic3, item1..item3, later }`, `open registry Hints ... as HINT` | a card | +| `hints_config(shown, every)` | how many the rail holds (3) and seconds between passes (1) | +| `hints_tick(dt)`, `hints_sort()`, `hints_refresh()` | a pass when due, a pass now, the next tick's pass now | +| `hints_rail() -> []int`, `hints_count()`, `hints_at(k)`, `hints_pick()`, `hints_hot(k)` | the rail | +| `hints_card()`, `hints_key()`, `hints_open(k)`, `hints_close()`, `hints_move(d)` | the card being read (-1) and its verbs | +| `hints_step_text(id, n)`, `hints_step_icon(id, n)`, `hints_step_item(id, n)` | a step, n 1..3 | +| `hints_mute_card()`, `hints_mute(id, on)`, `hints_muted(id)`, `hints_mute_count()`, `hints_unmute_all()` | "I know this" | +| `hints_mutes_save() -> Val`, `hints_mutes_load(v)` | the mutes as a list of keys, for the game's settings file | +| `hints_facts() -> Queue` | `{ what, card }`: `HINTS_F_OPENED`, `_CLOSED`, `_MUTED`, `_UNMUTED` | +| `hints_system() -> System` | `"hints"`, `PH_PRESENT`: reset and the tick; nothing in a trip's save | + +## Tests + +```bash +ludic test packages/ludic.hints +``` diff --git a/packages/ludic.hints/card.ludic b/packages/ludic.hints/card.ludic new file mode 100644 index 00000000..285729b0 --- /dev/null +++ b/packages/ludic.hints/card.ludic @@ -0,0 +1,62 @@ +# card.ludic - a card read off the rail: the key opens the picked chip and puts it down, the arrows +# walk the rail with the card up, and "I know this" mutes the card being read +export function hints_card() -> int { return hn_card } + +# the key: the picked card opened, or the open one put down +export function hints_key() -> void { + if hn_card >= 0 { + hints_close() + return + } + hints_open(hn_pick) +} + +# the k-th chip on the rail opened as a card +export function hints_open(k: int) -> void { + if k < 0 or k >= hn_n { return } + hn_pick = k + hn_card = hn_top[k] + hn_fact(HINTS_F_OPENED, hn_card) +} + +export function hints_close() -> void { + if hn_card < 0 { return } + let c = hn_card + hn_card = -1 + hn_fact(HINTS_F_CLOSED, c) +} + +# the arrows, with a card up: the next chip along (d = 1) or the one before, round the ends +export function hints_move(d: int) -> void { + if hn_card < 0 or hn_n <= 1 { return } + hints_open((hn_pick + hn_n + d) % hn_n) +} + +# "I know this - do not show it again": the card off the rail until the mutes are given back +export function hints_mute_card() -> void { + if hn_card < 0 { return } + let c = hn_card + hints_mute(c, true) + hn_card = -1 + hn_t = 0.0 + hn_fact(HINTS_F_MUTED, c) +} + +# a step's words and its picture (n 1..3): the glyph's name, or the item (-1 when it is a glyph) +export function hints_step_text(id: int, n: int) -> string { + if n == 1 { return Hints[id].s1 } + if n == 2 { return Hints[id].s2 } + return Hints[id].s3 +} + +export function hints_step_icon(id: int, n: int) -> string { + if n == 1 { return Hints[id].pic1 } + if n == 2 { return Hints[id].pic2 } + return Hints[id].pic3 +} + +export function hints_step_item(id: int, n: int) -> int { + if n == 1 { return Hints[id].item1 } + if n == 2 { return Hints[id].item2 } + return Hints[id].item3 +} diff --git a/packages/ludic.hints/defs.ludic b/packages/ludic.hints/defs.ludic new file mode 100644 index 00000000..a5a9b1bf --- /dev/null +++ b/packages/ludic.hints/defs.ludic @@ -0,0 +1,39 @@ +# defs.ludic - a card as the game writes it, how much it matters, and the facts +export const HINTS_TIP: int = 0 # worth knowing +export const HINTS_NOTE: int = 1 # worth doing soon +export const HINTS_WARN: int = 2 # worth doing now +export const HINTS_URGENT: int = 3 # do it or lose the day + +# The words are the lesson's text keys (the game translates them where it draws). Pictures are +# the game's: `icon` / `picN` name a glyph, `item` / `itemN` a thing's picture instead (-1: none). +export property HintCard { + key: string = "" + urgency: int = HINTS_TIP + icon: string = "" + item: int = -1 + line: string = "" # the chip on the rail: five or six words + title: string = "" # the card's heading + s1: string = "" # three steps, each with a picture + s2: string = "" + s3: string = "" + pic1: string = "" + pic2: string = "" + pic3: string = "" + item1: int = -1 + item2: int = -1 + item3: int = -1 + later: string = "" # how not to be here next time +} + +# the game's cards; a card's mute is kept by its key, so their order is free +export open registry Hints of HintCard as HINT + +export const HINTS_F_OPENED: int = 0 # a card was opened, or the arrows moved to another +export const HINTS_F_CLOSED: int = 1 # put down by the player (not when it stopped being true) +export const HINTS_F_MUTED: int = 2 # "I know this": off the rail for good +export const HINTS_F_UNMUTED: int = 3 # every muted card given back (card -1) + +export property HintsFact { + what: int = 0 + card: int = -1 +} diff --git a/packages/ludic.hints/index.ludic b/packages/ludic.hints/index.ludic new file mode 100644 index 00000000..95bf889e --- /dev/null +++ b/packages/ludic.hints/index.ludic @@ -0,0 +1,11 @@ +# ludic.hints - cards as an open registry, a port that says which are true, the rail of the most +# urgent few, a card walked along it, and "I know this" kept by the card's key +module ludic_hints uses ludic_base +numbers float +import "ludic.base" +import "defs.ludic" +import "port.ludic" +import "rail.ludic" +import "card.ludic" +import "mute.ludic" +import "system.ludic" diff --git a/packages/ludic.hints/mute.ludic b/packages/ludic.hints/mute.ludic new file mode 100644 index 00000000..7c221404 --- /dev/null +++ b/packages/ludic.hints/mute.ludic @@ -0,0 +1,62 @@ +# mute.ludic - the cards a player has said they know, kept BY KEY: a card added, removed or moved in +# the registry leaves every other mute where it was, and a key this build does not know is carried +var hn_mute: []bool = null +var hn_foreign: []string = null # muted keys with no card in this build, written back as read + +function hn_mute_ensure() -> void { + if hn_mute != null { return } + hn_mute = new []bool + for i in 0 .. HINT_COUNT { push(hn_mute, false) } + hn_foreign = new []string +} + +export function hints_muted(id: int) -> bool { + hn_mute_ensure() + if id < 0 or id >= HINT_COUNT { return false } + return hn_mute[id] +} + +export function hints_mute(id: int, on: bool) -> void { + hn_mute_ensure() + if id < 0 or id >= HINT_COUNT { return } + hn_mute[id] = on +} + +export function hints_mute_count() -> int { + hn_mute_ensure() + var n = 0 + for i in 0 .. HINT_COUNT { if hn_mute[i] { n += 1 } } + return n +} + +# every muted card given back, from the next pass +export function hints_unmute_all() -> void { + hn_mute_ensure() + for i in 0 .. HINT_COUNT { hn_mute[i] = false } + hn_foreign = new []string + hn_t = 0.0 + hn_fact(HINTS_F_UNMUTED, -1) +} + +# the mutes as a list of keys, for wherever the game keeps the player's own choices +export function hints_mutes_save() -> Val { + hn_mute_ensure() + let l = Value.list() + for i in 0 .. HINT_COUNT { if hn_mute[i] { Value.add(l, Value.str(Hints[i].key)) } } + for i in 0 .. len(hn_foreign) { Value.add(l, Value.str(hn_foreign[i])) } + return l +} + +# a list of keys read back; anything that is not a list is no mutes +export function hints_mutes_load(l: Val) -> void { + hn_mute = null + hn_mute_ensure() + if l == null or Value.kind(l) != 5 { return } + for i in 0 .. Value.count(l) { + let e = Value.at(l, i) + if Value.kind(e) != 4 { continue } + let k = Value.as_str(e) + let id = hints_find(k) + if id >= 0 { hn_mute[id] = true } else { push(hn_foreign, k) } + } +} diff --git a/packages/ludic.hints/package.ludic b/packages/ludic.hints/package.ludic new file mode 100644 index 00000000..8c0380ae --- /dev/null +++ b/packages/ludic.hints/package.ludic @@ -0,0 +1,5 @@ +# ludic.hints - a rail of things that are true now and can be acted on now, sorted by how much they +# matter, each one a card that teaches it and can be muted for good. Uses ludic.base and nothing else. +package "ludic.hints" +version "0.1.0" +kind source diff --git a/packages/ludic.hints/port.ludic b/packages/ludic.hints/port.ludic new file mode 100644 index 00000000..23f6b6b8 --- /dev/null +++ b/packages/ludic.hints/port.ludic @@ -0,0 +1,8 @@ +# port.ludic - what the rail asks the game. Unbound: nothing is ever true, and the rail may show. +export port HintsWorld { + true_now: fn(int) -> bool = fn hn_never # is card i true right now? Pure and cheap: asked once a pass + showing: fn() -> bool = fn hn_yes # the rail is wanted (in play, the player's switch on) +} + +function hn_never(i: int) -> bool { return false } +function hn_yes() -> bool { return true } diff --git a/packages/ludic.hints/rail.ludic b/packages/ludic.hints/rail.ludic new file mode 100644 index 00000000..05460edb --- /dev/null +++ b/packages/ludic.hints/rail.ludic @@ -0,0 +1,102 @@ +# rail.ludic - the rail: once a pass (a second by default) the most urgent true cards, up to a few, +# muted ones left out; in a tie the registry's order decides +var hn_shown: int = 3 # how many the rail holds +var hn_every: float = 1.0 # seconds between passes +var hn_top: []int = null # the cards on the rail this pass +var hn_n: int = 0 +var hn_t: float = 0.0 # seconds to the next pass +var hn_pick: int = 0 # which chip the key would open +var hn_card: int = -1 # the card being read, or -1 +var hn_facts: Queue = null + +export function hints_facts() -> Queue { + if hn_facts == null { hn_facts = queue_new("hints.facts") } + return hn_facts +} + +function hn_fact(what: int, card: int) -> void { + let f = new HintsFact + f.what = what + f.card = card + q_push(hints_facts(), f) +} + +# how many the rail holds and how often it is sorted +export function hints_config(shown: int, every: float) -> void { + hn_shown = shown + hn_every = every + hn_top = null +} + +function hn_ensure() -> void { + if hn_top == null or len(hn_top) != hn_shown { + hn_top = new []int + for i in 0 .. hn_shown { push(hn_top, -1) } + } + hn_mute_ensure() +} + +# the next pass comes on the next tick rather than in a second +export function hints_refresh() -> void { hn_t = 0.0 } + +export function hints_tick(dt: float) -> void { + hn_ensure() + hn_t = hn_t - dt + if hn_t > 0.0 { return } + hn_t = hn_every + hints_sort() +} + +# one pass now: the rail re-sorted, the pick kept in range, and the card following its subject +# along the rail - or put down when what it was about stopped being true +export function hints_sort() -> void { + hn_ensure() + hn_n = 0 + let on = HintsWorld.showing() + for u in 0 .. 4 { + let want = HINTS_URGENT - u + for id in 0 .. HINT_COUNT { + if not on or hn_n >= hn_shown { break } + if Hints[id].urgency != want or hints_muted(id) { continue } + if not HintsWorld.true_now(id) { continue } + hn_top[hn_n] = id + hn_n += 1 + } + } + if hn_pick >= hn_n { hn_pick = 0 } + if hn_card < 0 { return } + if not on or hints_muted(hn_card) or not HintsWorld.true_now(hn_card) { + hn_card = -1 + return + } + for k in 0 .. hn_n { if hn_top[k] == hn_card { hn_pick = k } } +} + +# the cards on the rail, most urgent first (empty while the rail is not showing) +export function hints_rail() -> []int { + hn_ensure() + let out = new []int + if not HintsWorld.showing() { return out } + for k in 0 .. hn_n { push(out, hn_top[k]) } + return out +} + +export function hints_count() -> int { return hn_n } + +export function hints_at(k: int) -> int { + if k < 0 or k >= hn_n { return -1 } + return hn_top[k] +} + +export function hints_pick() -> int { return hn_pick } + +export function hints_hot(k: int) -> bool { return k == hn_pick } + +export function hints_reset() -> void { + hn_top = null + hn_n = 0 + hn_t = 0.0 + hn_pick = 0 + hn_card = -1 + q_clear(hints_facts()) +} diff --git a/packages/ludic.hints/system.ludic b/packages/ludic.hints/system.ludic new file mode 100644 index 00000000..c054d713 --- /dev/null +++ b/packages/ludic.hints/system.ludic @@ -0,0 +1,10 @@ +# system.ludic - the rail as a system in PH_PRESENT: it only reads the world, and a trip saves +# nothing of it (the mutes are the player's, kept wherever the game keeps its settings) +function hn_tick(t: Tick) -> void { hints_tick(t.dt) } + +export function hints_system() -> System { + let s = system_new("hints", PH_PRESENT) + s.reset = fn hints_reset + s.tick = fn hn_tick + return s +} diff --git a/packages/ludic.hints/tests/cards.lres b/packages/ludic.hints/tests/cards.lres new file mode 100644 index 00000000..30f1cd56 --- /dev/null +++ b/packages/ludic.hints/tests/cards.lres @@ -0,0 +1,3 @@ +# cards.lres - two cards from a file, after the test's own defs +bear { urgency: HINTS_URGENT, line: "Bear near", title: "A bear", icon: "bear" } +fire { urgency: HINTS_NOTE, line: "Light the fire", later: "Carry three logs." } diff --git a/packages/ludic.hints/tests/hints_test.ludic b/packages/ludic.hints/tests/hints_test.ludic new file mode 100644 index 00000000..ab95eded --- /dev/null +++ b/packages/ludic.hints/tests/hints_test.ludic @@ -0,0 +1,162 @@ +# hints_test.ludic - the rail sorted by urgency and capped, ties in registry order, the card walked +# along it and put down when it stops being true, muting by key across a save, cards from a file +import "ludic.hints" +import "ludic.base" +program HintsTest { + numbers float + def Hints thirsty { urgency: HINTS_NOTE, line: "Thirsty - drink", title: "Water", s1: "Drink at a shore", pic1: "water", item2: 7 } + def Hints parched { urgency: HINTS_URGENT, line: "You must drink now", title: "Water, now" } + def Hints rain { urgency: HINTS_TIP, line: "Rain coming" } + def Hints cold { urgency: HINTS_WARN, line: "Cold" } + def Hints night { urgency: HINTS_TIP, line: "Night soon" } + def Hints from "cards.lres" + + var on: []bool = null + var showing: bool = true + + function fake_true(i: int) -> bool { return on[i] } + function fake_showing() -> bool { return showing } + bind HintsWorld { true_now: fn fake_true, showing: fn fake_showing } + + function fresh() -> void { + on = new []bool + for i in 0 .. HINT_COUNT { push(on, false) } + showing = true + hints_config(3, 1.0) + hints_reset() + hints_mutes_load(Value.list()) + } + + function count(what: int) -> int { + let fs = q_drain(hints_facts()) + var n = 0 + for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } + return n + } + + test "the registry is the game's, and a file adds to it" { + expect_eq(HINT_COUNT, 7) + expect_eq(hints_find("parched"), HINT_PARCHED) + expect_eq(Hints[HINT_BEAR].urgency, HINTS_URGENT) + expect(Hints[HINT_FIRE].later == "Carry three logs.") + expect(hints_step_icon(HINT_THIRSTY, 1) == "water") + expect_eq(hints_step_item(HINT_THIRSTY, 2), 7) + expect(hints_step_text(HINT_THIRSTY, 1) == "Drink at a shore") + } + + test "the most urgent true cards, capped, ties in registry order" { + fresh() + for i in 0 .. HINT_COUNT { on[i] = true } + hints_tick(1.0) + expect_eq(hints_count(), 3) + expect_eq(hints_at(0), HINT_PARCHED) + expect_eq(hints_at(1), HINT_BEAR) + expect_eq(hints_at(2), HINT_COLD) + on[HINT_PARCHED] = false + on[HINT_BEAR] = false + hints_tick(0.5) + expect_eq(hints_at(0), HINT_PARCHED) # not a second yet: the rail holds + hints_tick(0.6) + expect_eq(hints_at(0), HINT_COLD) + expect_eq(hints_at(1), HINT_THIRSTY) + expect_eq(hints_at(2), HINT_FIRE) + } + + test "nothing on the rail while it is not showing" { + fresh() + on[HINT_COLD] = true + showing = false + hints_sort() + expect_eq(len(hints_rail()), 0) + expect_eq(hints_count(), 0) + showing = true + hints_sort() + expect_eq(len(hints_rail()), 1) + } + + test "the key opens the picked chip, the arrows walk round, the key puts it down" { + fresh() + on[HINT_COLD] = true + on[HINT_THIRSTY] = true + on[HINT_RAIN] = true + hints_sort() + hints_key() + expect_eq(hints_card(), HINT_COLD) + hints_move(1) + expect_eq(hints_card(), HINT_THIRSTY) + expect(hints_hot(1)) + hints_move(-1) + hints_move(-1) + expect_eq(hints_card(), HINT_RAIN) + hints_key() + expect_eq(hints_card(), -1) + expect_eq(count(HINTS_F_OPENED), 4) + } + + test "the card follows its subject as the rail re-sorts, and goes when it is no longer true" { + fresh() + on[HINT_THIRSTY] = true + on[HINT_RAIN] = true + hints_sort() + hints_open(1) + expect_eq(hints_card(), HINT_RAIN) + on[HINT_COLD] = true + hints_sort() + expect_eq(hints_card(), HINT_RAIN) + expect_eq(hints_pick(), 2) + on[HINT_RAIN] = false + hints_sort() + expect_eq(hints_card(), -1) + expect_eq(count(HINTS_F_CLOSED), 0) # it went; nobody put it down + } + + test "I know this: muted off the rail, counted, given back" { + fresh() + on[HINT_COLD] = true + on[HINT_THIRSTY] = true + hints_sort() + hints_key() + hints_mute_card() + expect_eq(hints_card(), -1) + expect(hints_muted(HINT_COLD)) + expect_eq(hints_mute_count(), 1) + expect_eq(count(HINTS_F_MUTED), 1) + hints_tick(0.0) # the next pass comes at once + expect_eq(hints_at(0), HINT_THIRSTY) + expect_eq(hints_count(), 1) + hints_unmute_all() + hints_tick(0.0) + expect_eq(hints_at(0), HINT_COLD) + expect_eq(count(HINTS_F_UNMUTED), 1) + } + + test "mutes are kept by key, and a key this build lacks is carried through" { + fresh() + hints_mute(HINT_NIGHT, true) + hints_mute(HINT_BEAR, true) + let l = hints_mutes_save() + expect_eq(Value.count(l), 2) + Value.add(l, Value.str("gone_card")) + Value.add(l, Value.int(3)) # not a key: ignored + hints_mutes_load(l) + expect(hints_muted(HINT_NIGHT)) + expect(hints_muted(HINT_BEAR)) + expect(not hints_muted(HINT_RAIN)) + expect_eq(hints_mute_count(), 2) + let back = hints_mutes_save() + expect_eq(Value.count(back), 3) + expect(Value.as_str(Value.at(back, 2)) == "gone_card") + hints_mutes_load(null) + expect_eq(hints_mute_count(), 0) + } + + test "as a system it ticks the rail" { + fresh() + core_clear() + core_add(hints_system()) + core_reset_all() + on[HINT_FIRE] = true + core_tick_all(tick_new(1.0, 1, 0.0)) + expect_eq(hints_at(0), HINT_FIRE) + } +}