From 63e29f024ccf7e475b4f75129b6665d777ed53b5 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Fri, 25 Sep 2026 05:36:11 +0300 Subject: [PATCH] refactor(packages): the mechanic packages use ludic_base only, and ask through ports ludic.clock, .effects, .inventory, .wallet, .weather and .tracks say 'uses ludic_base' (ludic.base uses nothing). ClockWorld, PackRules, WeatherWorld and TracksWorld are export ports whose defaults are the old fallbacks; clock_bind, inv_bind, weather_bind and tracks_bind are gone, and the tests bind their fakes. The base README's toy does the same. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.base/README.md | 29 +++++++--------- packages/ludic.base/index.ludic | 2 +- packages/ludic.base/tests/route_test.ludic | 4 +-- .../ludic.base/tests/toys/fishing/index.ludic | 8 ++--- .../ludic.base/tests/toys/pack/index.ludic | 2 +- packages/ludic.clock/README.md | 16 ++++++--- packages/ludic.clock/index.ludic | 2 +- packages/ludic.clock/state.ludic | 25 ++++++-------- packages/ludic.clock/tests/clock_test.ludic | 7 ++-- packages/ludic.clock/verbs.ludic | 4 +-- packages/ludic.effects/index.ludic | 2 +- packages/ludic.inventory/README.md | 8 +++-- packages/ludic.inventory/index.ludic | 2 +- packages/ludic.inventory/state.ludic | 11 +++---- .../tests/inventory_test.ludic | 5 ++- packages/ludic.tracks/README.md | 10 ++++-- packages/ludic.tracks/config.ludic | 9 ++--- packages/ludic.tracks/index.ludic | 2 +- packages/ludic.tracks/leave.ludic | 2 +- packages/ludic.tracks/state.ludic | 20 ++++++----- packages/ludic.tracks/tests/tracks_test.ludic | 8 ++--- packages/ludic.wallet/index.ludic | 2 +- packages/ludic.weather/README.md | 12 +++++-- packages/ludic.weather/bias.ludic | 3 +- packages/ludic.weather/config.ludic | 20 +++-------- packages/ludic.weather/index.ludic | 2 +- packages/ludic.weather/pick.ludic | 3 +- packages/ludic.weather/run.ludic | 3 +- packages/ludic.weather/state.ludic | 33 +++++++++++-------- .../ludic.weather/tests/weather_test.ludic | 19 ++++++----- packages/ludic.weather/verbs.ludic | 4 +-- 31 files changed, 130 insertions(+), 149 deletions(-) diff --git a/packages/ludic.base/README.md b/packages/ludic.base/README.md index 89f13fa0..3863dfd7 100644 --- a/packages/ludic.base/README.md +++ b/packages/ludic.base/README.md @@ -13,11 +13,13 @@ import "ludic.base" ## The rules -1. **A mechanic uses only this.** If a mechanic needs a second mechanic to compile, that is the - bug. Its tests are one program: `ludic.base`, the mechanic, and a fake for each port. -2. **Ports for questions.** What a mechanic needs to ASK the world is a record of function - values it owns (`FishingWorld { is_water: fn(float, float) -> bool }`), in primitive and - `ludic.base` types only. The game binds it once. +1. **A mechanic uses only this.** Its module line says so - `module ludic_clock uses ludic_base` + - and the compiler refuses a reference into any other module, a package's included. Its tests are one program: `ludic.base`, the mechanic, and a fake for each port. +2. **Ports for questions.** What a mechanic needs to ASK the world is a `port` it owns + (`export port FishingWorld { is_water: fn(float, float) -> bool }`), in primitive and + `ludic.base` types only. The game binds it once, as a declaration (`bind FishingWorld { + is_water: fn lake }`), and the compiler refuses a program that uses a port with a required + member and never binds it. A member with a default may be left out. 3. **Queues for facts.** What HAPPENED is pushed onto a `Queue` the mechanic owns (`caught`) and drained by the game in a later phase. A mechanic never acts on another's behalf. 4. **Verbs for changes.** A mechanic's state changes only through its exported functions @@ -49,18 +51,16 @@ import "ludic.base" ## A toy mechanic ```ludic -module toy_fishing +module toy_fishing uses ludic_base numbers float import "ludic.base" export property Caught { species: int = 0, weight: float = 0.0 } -export property FishingWorld { is_water: fn(float, float) -> bool = null } # its port -var world: FishingWorld = null +export port FishingWorld { is_water: fn(float, float) -> bool } # its port var dice: Rng = null var casts: int = 0 export var caught: Queue = null # its facts -export function fishing_bind(w: FishingWorld) -> void { world = w } export function fishing_cast(x: float, z: float) -> bool { # its verb - if not world.is_water(x, z) { return false } + if not FishingWorld.is_water(x, z) { return false } casts += 1 return true } @@ -99,6 +99,7 @@ import "ludic.base" program Game { numbers float function lake(x: float, z: float) -> bool { return x < 100.0 } + bind FishingWorld { is_water: fn lake } # the route: a landed fish goes into the pack function route_fishing_pack(t: Tick) -> void { @@ -107,9 +108,6 @@ program Game { } function game_start() -> void { - let w = new FishingWorld - w.is_water = fn lake - fishing_bind(w) core_add(fishing_system()) let r = system_new("route.fishing_pack", PH_RESOLVE) r.tick = fn route_fishing_pack @@ -136,14 +134,11 @@ written before a generic call inside a `test` body resolved and before each test state, so the queue cases are functions a test calls and the runner's cases start with `core_clear()`; neither is needed any more. -## Later +## Declared systems -- `port FishingWorld { ... }` / `bind` will replace the record of function values, checked at - compile time instead of a null at run time. - `Systems` is an `open registry`, so a system can be declared instead of added: `def Systems fishing { phase: PH_SIMULATE, tick: fn fishing_tick }` from any module. The runner starts from the declared systems - the order the compiler gives an open registry: ludic.base's own (none), then each other module's by module name, each module's in the order it is read - and `core_add` appends after them. A game that wants to decide the order itself writes the `def`s in its own files, or keeps calling `core_add` in one place. -- `module toy_fishing uses ludic_base` will make rule 1 a compile error rather than a review. diff --git a/packages/ludic.base/index.ludic b/packages/ludic.base/index.ludic index 421cf055..aab1d11b 100644 --- a/packages/ludic.base/index.ludic +++ b/packages/ludic.base/index.ludic @@ -1,6 +1,6 @@ # ludic.base - what a mechanic may use and nothing else: the frame it is handed, the phases # that order it, queues for the facts it reports, its own dice, its save section, its system -module ludic_base +module ludic_base uses numbers float import "tick.ludic" import "queue.ludic" diff --git a/packages/ludic.base/tests/route_test.ludic b/packages/ludic.base/tests/route_test.ludic index 48a0b0b9..ab3b6717 100644 --- a/packages/ludic.base/tests/route_test.ludic +++ b/packages/ludic.base/tests/route_test.ludic @@ -7,6 +7,7 @@ program RouteTest { numbers float function water_everywhere_but_land(x: float, z: float) -> bool { return x < 100.0 } + bind FishingWorld { is_water: fn water_everywhere_but_land } # the route: a landed fish goes into the pack function route_fishing_pack(t: Tick) -> void { @@ -16,9 +17,6 @@ program RouteTest { function game_start() -> void { core_clear() - let w = new FishingWorld - w.is_water = fn water_everywhere_but_land - fishing_bind(w) core_add(fishing_system()) let r = system_new("route.fishing_pack", PH_RESOLVE) r.tick = fn route_fishing_pack diff --git a/packages/ludic.base/tests/toys/fishing/index.ludic b/packages/ludic.base/tests/toys/fishing/index.ludic index 78f84a1d..1bd7fe13 100644 --- a/packages/ludic.base/tests/toys/fishing/index.ludic +++ b/packages/ludic.base/tests/toys/fishing/index.ludic @@ -1,16 +1,14 @@ # a toy mechanic: casting a line. It uses ludic.base and nothing else. -module toy_fishing +module toy_fishing uses ludic_base numbers float import "ludic.base" export property Caught { species: int = 0, weight: float = 0.0 } -export property FishingWorld { is_water: fn(float, float) -> bool = null } # its port -var world: FishingWorld = null +export port FishingWorld { is_water: fn(float, float) -> bool } # its port var dice: Rng = null var casts: int = 0 export var caught: Queue = null # its facts -export function fishing_bind(w: FishingWorld) -> void { world = w } export function fishing_cast(x: float, z: float) -> bool { # its verb - if not world.is_water(x, z) { return false } + if not FishingWorld.is_water(x, z) { return false } casts += 1 return true } diff --git a/packages/ludic.base/tests/toys/pack/index.ludic b/packages/ludic.base/tests/toys/pack/index.ludic index d03597e8..7ec041e4 100644 --- a/packages/ludic.base/tests/toys/pack/index.ludic +++ b/packages/ludic.base/tests/toys/pack/index.ludic @@ -1,5 +1,5 @@ # a toy mechanic: a pack of counts by kind, saved in its own section -module toy_pack +module toy_pack uses ludic_base numbers float import "ludic.base" var counts: []int = null diff --git a/packages/ludic.clock/README.md b/packages/ludic.clock/README.md index de17cb26..6d4641c1 100644 --- a/packages/ludic.clock/README.md +++ b/packages/ludic.clock/README.md @@ -16,18 +16,24 @@ the hour; another machine is told it (`clock_set_hours`, `clock_set_day`, `clock ## The port ```ludic -property ClockWorld { - owns_world: fn() -> bool # does this machine run the world? (unbound: yes) - rest_scale: fn() -> float # how much everyone's rest speeds the clock; 1 when nobody rests - seed: fn() -> int # a seed for a new trip's moon +export port ClockWorld { + owns_world: fn() -> bool = ... # does this machine run the world? (unbound: yes) + rest_scale: fn() -> float = ... # how much everyone's rest speeds the clock (unbound: 1) + seed: fn() -> int = ... # a seed for a new trip's moon (unbound: 1) } ``` +The game fills it once, where it is put together, and may leave any member out: + +```ludic +bind ClockWorld { owns_world: fn ses_owns_world, rest_scale: fn rt_rest_scale, seed: fn rt_clock_seed } +``` + ## API | | | | --- | --- | -| `clock_bind(w)` | the port | +| `bind ClockWorld { ... }` | the port (a declaration, not a call) | | `clock_config(start_hours, night_from, night_to)` | when a trip starts, and which hours are night | | `clock_config_moon(days, lo, hi)` | the moon's month in days, and the part of it a new trip starts in | | `clock_calendar(month_days, season_of_month)` | a month's length, and each month's season (the list's length is the year) | diff --git a/packages/ludic.clock/index.ludic b/packages/ludic.clock/index.ludic index b8bc55b1..99aed836 100644 --- a/packages/ludic.clock/index.ludic +++ b/packages/ludic.clock/index.ludic @@ -1,6 +1,6 @@ # ludic.clock - one clock for a whole world: the hour, the day, the moon's place in its month and # where the day falls in a calendar the game describes; a day turning is a fact on a queue -module ludic_clock +module ludic_clock uses ludic_base numbers float import "ludic.base" import "state.ludic" diff --git a/packages/ludic.clock/state.ludic b/packages/ludic.clock/state.ludic index 6725f288..a91ec525 100644 --- a/packages/ludic.clock/state.ludic +++ b/packages/ludic.clock/state.ludic @@ -1,11 +1,15 @@ # state.ludic - the clock's port, its facts, and what it holds # what the clock asks the world: whether this machine runs it, how much the rest speeds it (1 when # nobody rests), and a seed for a new trip's moon -export property ClockWorld { - owns_world: fn() -> bool = null - rest_scale: fn() -> float = null - seed: fn() -> int = null +# (unbound: this machine runs it, nobody rests, and a seed of 1) +export port ClockWorld { + owns_world: fn() -> bool = fn ck_owns_alone + rest_scale: fn() -> float = fn ck_no_rest + seed: fn() -> int = fn ck_seed_one } +function ck_owns_alone() -> bool { return true } +function ck_no_rest() -> float { return 1.0 } +function ck_seed_one() -> int { return 1 } # how a day turned: the hour ran past midnight, or a verb turned it (a night slept, a collapse) export const DAY_STAYED_UP: int = 0 @@ -16,7 +20,6 @@ export property DayTurned { how: int = 0 } -var ck_world: ClockWorld = null var ck_day: int = 1 var ck_hours: float = 0.0 var ck_moon: float = 0.0 @@ -30,23 +33,15 @@ var ck_moon_hi: float = 1.0 var ck_dice: Rng = null var ck_days: Queue = null -export function clock_bind(w: ClockWorld) -> void { ck_world = w } - # the facts: each day that turned, in order; the game drains it and runs its morning export function clock_days() -> Queue { if ck_days == null { ck_days = queue_new("clock.days") } return ck_days } -function ck_owns() -> bool { - if ck_world == null or ck_world.owns_world == null { return true } - return ck_world.owns_world() -} +function ck_owns() -> bool { return ClockWorld.owns_world() } -function ck_rest() -> float { - if ck_world == null or ck_world.rest_scale == null { return 1.0 } - return ck_world.rest_scale() -} +function ck_rest() -> float { return ClockWorld.rest_scale() } # when a trip starts, when the night is, and how long the moon's month is export function clock_config(start_hours: float, night_from: float, night_to: float) -> void { diff --git a/packages/ludic.clock/tests/clock_test.ludic b/packages/ludic.clock/tests/clock_test.ludic index f53c06f2..bbd81906 100644 --- a/packages/ludic.clock/tests/clock_test.ludic +++ b/packages/ludic.clock/tests/clock_test.ludic @@ -11,14 +11,11 @@ program ClockTest { function fake_rest() -> float { return rest } function fake_seed() -> int { return 42 } + bind ClockWorld { owns_world: fn fake_owns, rest_scale: fn fake_rest, seed: fn fake_seed } + function fresh() -> void { owns = true rest = 1.0 - let w = new ClockWorld - w.owns_world = fn fake_owns - w.rest_scale = fn fake_rest - w.seed = fn fake_seed - clock_bind(w) clock_config(8.0, 20.0, 6.0) clock_config_moon(29.5, 0.2, 0.8) clock_set_scale(60.0) diff --git a/packages/ludic.clock/verbs.ludic b/packages/ludic.clock/verbs.ludic index e0d8fe87..b2a8b2dd 100644 --- a/packages/ludic.clock/verbs.ludic +++ b/packages/ludic.clock/verbs.ludic @@ -35,9 +35,7 @@ export function clock_advance(gh: float) -> void { # a new trip: day one at the start hour, the moon somewhere in its month export function clock_reset() -> void { - var seed = 1 - if ck_world != null and ck_world.seed != null { seed = ck_world.seed() } - ck_dice = rng_new(seed) + ck_dice = rng_new(ClockWorld.seed()) ck_day = 1 ck_hours = ck_start_hours ck_moon = rng_span(ck_dice, ck_moon_lo, ck_moon_hi) % 1.0 diff --git a/packages/ludic.effects/index.ludic b/packages/ludic.effects/index.ludic index d65a10de..f424eb41 100644 --- a/packages/ludic.effects/index.ludic +++ b/packages/ludic.effects/index.ludic @@ -1,6 +1,6 @@ # ludic.effects - a small ring of timed modifiers, each a kind (the game's number), a magnitude # in that kind's units, a time left and a label; the game asks the sum for a kind -module ludic_effects +module ludic_effects uses ludic_base numbers float import "ludic.base" import "state.ludic" diff --git a/packages/ludic.inventory/README.md b/packages/ludic.inventory/README.md index 460bf8db..a82d2f77 100644 --- a/packages/ludic.inventory/README.md +++ b/packages/ludic.inventory/README.md @@ -16,16 +16,18 @@ longer knows is dropped on load. ## The port ```ludic -property PackRules { - stack_limit: fn(int) -> int # how many of a kind fit (unbound: no limit) +export port PackRules { + stack_limit: fn(int) -> int = ... # how many of a kind fit (unbound: no limit) } ``` +The game fills it once, where it is put together: `bind PackRules { stack_limit: fn gear_stack }`. + ## API | | | | --- | --- | -| `inv_bind(r)`, `inv_config(keys)` | the port, and the kinds by name (the pack starts empty) | +| `bind PackRules { ... }`, `inv_config(keys)` | the port (a declaration), and the kinds by name (the pack starts empty) | | `inv_add(it, n) -> int` | as many of `n` as there is room for; returns how many went in | | `inv_take(it, n) -> bool` | all `n` or none | | `inv_set(it, n)` | a count outright, room or not (a starting kit, a correction, the network's word); never below 0 | diff --git a/packages/ludic.inventory/index.ludic b/packages/ludic.inventory/index.ludic index 253d95e4..e01f4827 100644 --- a/packages/ludic.inventory/index.ludic +++ b/packages/ludic.inventory/index.ludic @@ -1,6 +1,6 @@ # ludic.inventory - one pack of counts by item kind: the game names the kinds (so the save is by # name, never by position) and answers how many of one fit; every change is a fact on a queue -module ludic_inventory +module ludic_inventory uses ludic_base numbers float import "ludic.base" import "state.ludic" diff --git a/packages/ludic.inventory/state.ludic b/packages/ludic.inventory/state.ludic index e00f7d9c..e6315562 100644 --- a/packages/ludic.inventory/state.ludic +++ b/packages/ludic.inventory/state.ludic @@ -1,8 +1,9 @@ # state.ludic - the pack's port, its facts, and its counts # what the pack asks the game: how many of a kind fit (unbound: no limit) -export property PackRules { - stack_limit: fn(int) -> int = null +export port PackRules { + stack_limit: fn(int) -> int = fn iv_no_limit } +function iv_no_limit(it: int) -> int { return 2147483647 } # a count changed, however: added, taken, or set outright export property ItemChanged { @@ -11,13 +12,10 @@ export property ItemChanged { after: int = 0 } -var iv_rules: PackRules = null var iv_keys: []string = null var iv_counts: []int = null var iv_changes: Queue = null -export function inv_bind(r: PackRules) -> void { iv_rules = r } - # the kinds, by the name each is saved under; the pack starts empty export function inv_config(keys: []string) -> void { iv_keys = keys @@ -42,8 +40,7 @@ export function inv_count(it: int) -> int { } export function inv_limit(it: int) -> int { - if iv_rules == null or iv_rules.stack_limit == null { return 2147483647 } - return iv_rules.stack_limit(it) + return PackRules.stack_limit(it) } # how many more fit diff --git a/packages/ludic.inventory/tests/inventory_test.ludic b/packages/ludic.inventory/tests/inventory_test.ludic index 0680c805..6511cc36 100644 --- a/packages/ludic.inventory/tests/inventory_test.ludic +++ b/packages/ludic.inventory/tests/inventory_test.ludic @@ -21,10 +21,9 @@ program InventoryTest { return k } + bind PackRules { stack_limit: fn limit } + function fresh() -> void { - let r = new PackRules - r.stack_limit = fn limit - inv_bind(r) inv_config(names("wood", "rod", "fish")) inv_reset() } diff --git a/packages/ludic.tracks/README.md b/packages/ludic.tracks/README.md index b97054b5..65015f68 100644 --- a/packages/ludic.tracks/README.md +++ b/packages/ludic.tracks/README.md @@ -20,7 +20,7 @@ good as walking a trail. ## The port ```ludic -property TracksWorld { +export port TracksWorld { # every member has a default, so any may be left out now: fn() -> float # game hours counted from the start of the trip (unbound: 0) skill: fn() -> int # the reader's tier; at tracks_config_reading's name_at it names the animal bearing: fn(float) -> string # a yaw in words, "" for none (a reader with no compass) @@ -28,11 +28,17 @@ property TracksWorld { } ``` +The game fills it once, where it is put together - a declaration, not a call: + +```ludic +bind TracksWorld { now: fn rt_trip_hours, skill: fn track_skill, bearing: fn track_bearing, placed: fn track_mark_placed } +``` + ## API | | | | --- | --- | -| `tracks_bind(w)`, `tracks_config(kinds, cap)`, `tracks_config_reading(name_at, fresh, recent)` | the port; how many kinds and how many prints of each; the skill that names the animal and the ages (hours) under which a print is fresh or recent | +| `bind TracksWorld { ... }`, `tracks_config(kinds, cap)`, `tracks_config_reading(name_at, fresh, recent)` | the port; how many kinds and how many prints of each; the skill that names the animal and the ages (hours) under which a print is fresh or recent | | `tracks_leave(kind, x, z, yaw, sp, legend) -> int`, `tracks_leave_at(..., made)` | a print, now or at another machine's time; returns its ring index | | `tracks_clear()` | the ground forgets every print (a new world); the reader keeps what it learned | | `tracks_nearest(x, z, r) -> int` | the nearest print within `r`, as a slot (`kind * cap + index`), or -1 | diff --git a/packages/ludic.tracks/config.ludic b/packages/ludic.tracks/config.ludic index 0d2b7507..7b4c3df4 100644 --- a/packages/ludic.tracks/config.ludic +++ b/packages/ludic.tracks/config.ludic @@ -22,16 +22,13 @@ export function tracks_config_reading(name_at: int, fresh: float, recent: float) } function tk_now() -> float { - if tk_world == null or tk_world.now == null { return 0.0 } - return tk_world.now() + return TracksWorld.now() } function tk_named() -> bool { - if tk_world == null or tk_world.skill == null { return true } - return tk_world.skill() >= tk_name_at + return TracksWorld.skill() >= tk_name_at } function tk_bearing(yaw: float) -> string { - if tk_world == null or tk_world.bearing == null { return "" } - return tk_world.bearing(yaw) + return TracksWorld.bearing(yaw) } diff --git a/packages/ludic.tracks/index.ludic b/packages/ludic.tracks/index.ludic index 1489c88d..509116de 100644 --- a/packages/ludic.tracks/index.ludic +++ b/packages/ludic.tracks/index.ludic @@ -1,6 +1,6 @@ # ludic.tracks - a ring of prints per kind, each remembering who left it, which way it went and # when; reading one is a fact on a queue, and the game draws them through a hook -module ludic_tracks +module ludic_tracks uses ludic_base numbers float import "ludic.base" import "state.ludic" diff --git a/packages/ludic.tracks/leave.ludic b/packages/ludic.tracks/leave.ludic index e37c08ce..f24fe7a4 100644 --- a/packages/ludic.tracks/leave.ludic +++ b/packages/ludic.tracks/leave.ludic @@ -19,7 +19,7 @@ export function tracks_leave_at(kind: int, x: float, z: float, yaw: float, sp: i t.legend = legend t.made = made t.read = false - if tk_world != null and tk_world.placed != null { tk_world.placed(kind, i) } + TracksWorld.placed(kind, i) return i } diff --git a/packages/ludic.tracks/state.ludic b/packages/ludic.tracks/state.ludic index ca5a83cc..01c71eda 100644 --- a/packages/ludic.tracks/state.ludic +++ b/packages/ludic.tracks/state.ludic @@ -1,12 +1,16 @@ # state.ludic - the port, the prints, the facts, and what the reader has learned -# what the tracks ask the world. Any field may be left null: the time is then 0, the reader -# can name every animal, no bearing is given, and nothing is drawn. -export property TracksWorld { - now: fn() -> float = null # game hours, counted from the start of the trip - skill: fn() -> int = null # how well the reader reads (the game's tiers) - bearing: fn(float) -> string = null # a yaw in words ("north-west"), "" for none - placed: fn(int, int) -> void = null # a print was put down at (kind, ring index): draw it +# what the tracks ask the world. Any member may be left out of the bind: the time is then 0, +# the reader can name every animal, no bearing is given, and nothing is drawn. +export port TracksWorld { + now: fn() -> float = fn tk_time_zero # game hours, counted from the start of the trip + skill: fn() -> int = fn tk_every_skill # how well the reader reads (the game's tiers) + bearing: fn(float) -> string = fn tk_no_bearing # a yaw in words ("north-west"), "" for none + placed: fn(int, int) -> void = fn tk_not_drawn # a print was put down at (kind, ring index): draw it } +function tk_time_zero() -> float { return 0.0 } +function tk_every_skill() -> int { return 2147483647 } +function tk_no_bearing(yaw: float) -> string { return "" } +function tk_not_drawn(kind: int, i: int) -> void { } # where a sign came from export const TRACK_PRINT: int = 0 # a print in a ring, read with tracks_read @@ -43,7 +47,6 @@ export property TrackRead { first: bool = false } -var tk_world: TracksWorld = null var tk_kinds: int = 0 var tk_cap: int = 0 var tk_ring: []Track = null # kinds x cap, kind-major @@ -57,7 +60,6 @@ var tk_yaw: float = 0.0 # the heading of the last sign read var tk_has_yaw: bool = false var tk_reads: Queue = null -export function tracks_bind(w: TracksWorld) -> void { tk_world = w } # the facts: every sign read, oldest first export function tracks_reads() -> Queue { diff --git a/packages/ludic.tracks/tests/tracks_test.ludic b/packages/ludic.tracks/tests/tracks_test.ludic index 558cbe76..1521b519 100644 --- a/packages/ludic.tracks/tests/tracks_test.ludic +++ b/packages/ludic.tracks/tests/tracks_test.ludic @@ -21,17 +21,13 @@ program TracksTest { placed_last = i } + bind TracksWorld { now: fn fake_now, skill: fn fake_skill, bearing: fn fake_bearing, placed: fn fake_placed } + function fresh() -> void { now = 10.0 skill = 1 placed_n = 0 placed_last = -1 - let w = new TracksWorld - w.now = fn fake_now - w.skill = fn fake_skill - w.bearing = fn fake_bearing - w.placed = fn fake_placed - tracks_bind(w) tracks_config(3, 4) tracks_config_reading(1, 0.5, 2.5) tracks_reset() diff --git a/packages/ludic.wallet/index.ludic b/packages/ludic.wallet/index.ludic index e1cdf14a..8f63fd25 100644 --- a/packages/ludic.wallet/index.ludic +++ b/packages/ludic.wallet/index.ludic @@ -1,6 +1,6 @@ # ludic.wallet - one purse of whole money: verbs to earn, spend (only what is there) and lose (a # cost that happens anyway), a MoneyChanged fact per change, and its own save section -module ludic_wallet +module ludic_wallet uses ludic_base numbers float import "ludic.base" import "wallet.ludic" diff --git a/packages/ludic.weather/README.md b/packages/ludic.weather/README.md index ede40095..a606d6b0 100644 --- a/packages/ludic.weather/README.md +++ b/packages/ludic.weather/README.md @@ -39,7 +39,7 @@ property WeatherKind { ## The port ```ludic -property WeatherWorld { +export port WeatherWorld { # every member has a default, so any may be left out owns_world: fn() -> bool # does this machine run the world? (unbound: yes) hours: fn() -> float # the hour, for the wind's daily turn (unbound: noon) is_night: fn() -> bool # a front clearing at night is no WEATHER_CLEARED @@ -47,10 +47,16 @@ property WeatherWorld { bias: fn() -> int # what the story wants now: WEATHER_NO_BIAS, _WANT_WET, _HOLD, _WANT_CLEAR front: fn() -> int # the kind a wanted front comes as (unbound: the first wet kind) front_hours: fn() -> float # the least a wanted front lasts (never under 5) - seed: fn() -> int # a new trip's dice + seed: fn() -> int # a new trip's dice (unbound: 1) } ``` +The game fills it once, where it is put together - a declaration, not a call: + +```ludic +bind WeatherWorld { owns_world: fn ses_owns_world, hours: fn clock_hours, odds: fn sky_odds } +``` + The bias is asked every tick on the owner. `WANT_WET` on a dry sky forecasts a front (`WEATHER_FRONT`), due within an hour and a half; `HOLD` keeps the current spell going at least 1.2 hours more; `WANT_CLEAR` clears within half an hour and keeps it clear. The game decides WHEN @@ -60,7 +66,7 @@ it wants them (daylight, a tarp up, a stars job open). | | | | --- | --- | -| `weather_bind(w)`, `weather_kinds(ks)`, `weather_config_wind(down, up, from, to)` | the port, the kinds, and the valley's wind: toward `down` by night, toward `up` from `from` to `to` | +| `bind WeatherWorld { ... }`, `weather_kinds(ks)`, `weather_config_wind(down, up, from, to)` | the port, the kinds, and the valley's wind: toward `down` by night, toward `up` from `from` to `to` | | `weather_run(dt, gh)` | one step (the system's tick does this from `Tick.dt` and `Tick.hours`) | | `weather_morning()` | a new day's forecast: seven times in ten it becomes the next spell | | `weather_reset()` | a new trip: the first clear kind, nothing forecast, dice from the seed | diff --git a/packages/ludic.weather/bias.ludic b/packages/ludic.weather/bias.ludic index 71d8f81a..aa3aa2c6 100644 --- a/packages/ludic.weather/bias.ludic +++ b/packages/ludic.weather/bias.ludic @@ -2,8 +2,7 @@ # rain could wait a week for it; so while the world says it wants a front, one is forecast and # arrives within the hour and a half, and while it wants clear sky, the sky clears and holds. function wx_front_kind() -> int { - if wx_world != null and wx_world.front != null { return wx_world.front() } - return wx_first_wet() + return WeatherWorld.front() } function wx_want_wet() -> void { diff --git a/packages/ludic.weather/config.ludic b/packages/ludic.weather/config.ludic index 23631d03..76e7de50 100644 --- a/packages/ludic.weather/config.ludic +++ b/packages/ludic.weather/config.ludic @@ -32,25 +32,13 @@ function wx_first_wet() -> int { return 0 } -function wx_owns() -> bool { - if wx_world == null or wx_world.owns_world == null { return true } - return wx_world.owns_world() -} +function wx_owns() -> bool { return WeatherWorld.owns_world() } -function wx_hours() -> float { - if wx_world == null or wx_world.hours == null { return 12.0 } - return wx_world.hours() -} +function wx_hours() -> float { return WeatherWorld.hours() } -function wx_night() -> bool { - if wx_world == null or wx_world.is_night == null { return false } - return wx_world.is_night() -} +function wx_night() -> bool { return WeatherWorld.is_night() } -function wx_bias() -> int { - if wx_world == null or wx_world.bias == null { return WEATHER_NO_BIAS } - return wx_world.bias() -} +function wx_bias() -> int { return WeatherWorld.bias() } function wx_wrap(a: float) -> float { var d = a diff --git a/packages/ludic.weather/index.ludic b/packages/ludic.weather/index.ludic index a1740237..735e811b 100644 --- a/packages/ludic.weather/index.ludic +++ b/packages/ludic.weather/index.ludic @@ -1,6 +1,6 @@ # ludic.weather - the sky as a chain of spells: each kind is data the game hands in, the next is # drawn from odds the game may lean on, and what changed is a fact on a queue -module ludic_weather +module ludic_weather uses ludic_base numbers float import "ludic.base" import "state.ludic" diff --git a/packages/ludic.weather/pick.ludic b/packages/ludic.weather/pick.ludic index 28745854..d53ea6bd 100644 --- a/packages/ludic.weather/pick.ludic +++ b/packages/ludic.weather/pick.ludic @@ -1,8 +1,7 @@ # pick.ludic - the next spell, drawn from weights, and how long one lasts. The weights are the # kind's own unless the world leans on them (a season, a wet week); every draw is this package's function wx_odds(from: int) -> []float { - if wx_world != null and wx_world.odds != null { return wx_world.odds(from) } - return wx_kinds[from].odds + return WeatherWorld.odds(from) } # the kind a set of weights lands on; no weights (or all zero) keeps the kind it came from diff --git a/packages/ludic.weather/run.ludic b/packages/ludic.weather/run.ludic index e8918fdc..6f285c51 100644 --- a/packages/ludic.weather/run.ludic +++ b/packages/ludic.weather/run.ludic @@ -7,8 +7,7 @@ function wx_turn() -> void { wx_left = wx_spell_hours(wx_state) if wx_forced { wx_forced = false - var least = 5.0 - if wx_world != null and wx_world.front_hours != null { least = Math.max(least, wx_world.front_hours()) } + let least = Math.max(5.0, WeatherWorld.front_hours()) if wx_left < least { wx_left = least } } if wx_state == was { return } diff --git a/packages/ludic.weather/state.ludic b/packages/ludic.weather/state.ludic index 7a4e8911..b9cd1748 100644 --- a/packages/ludic.weather/state.ludic +++ b/packages/ludic.weather/state.ludic @@ -1,16 +1,24 @@ # state.ludic - the port, the kinds, the facts, and what the sky holds -# what the weather asks the world. Any field may be left null: the answer is then "yes, it is -# mine", noon and daylight, the kind's own odds, no bias, the first wet kind, and a seed of 1. -export property WeatherWorld { - owns_world: fn() -> bool = null - hours: fn() -> float = null - is_night: fn() -> bool = null - odds: fn(int) -> []float = null # weights for the next spell, per kind, from this kind - bias: fn() -> int = null # what the story wants now: WEATHER_* below - front: fn() -> int = null # the kind a wanted front comes as - front_hours: fn() -> float = null # the least a wanted front lasts once it arrives - seed: fn() -> int = null +# what the weather asks the world. Any member may be left out of the bind: the answer is then +# "yes, it is mine", noon and daylight, the kind's own odds, no bias, the first wet kind, and a +# seed of 1. +export port WeatherWorld { + owns_world: fn() -> bool = fn wx_owns_alone + hours: fn() -> float = fn wx_noon + is_night: fn() -> bool = fn wx_daylight + odds: fn(int) -> []float = fn wx_own_odds # weights for the next spell, per kind, from this kind + bias: fn() -> int = fn wx_no_bias # what the story wants now: WEATHER_* below + front: fn() -> int = fn wx_first_wet # the kind a wanted front comes as + front_hours: fn() -> float = fn wx_no_hours # the least a wanted front lasts once it arrives + seed: fn() -> int = fn wx_seed_one } +function wx_owns_alone() -> bool { return true } +function wx_noon() -> float { return 12.0 } +function wx_daylight() -> bool { return false } +function wx_own_odds(from: int) -> []float { return wx_kinds[from].odds } +function wx_no_bias() -> int { return WEATHER_NO_BIAS } +function wx_no_hours() -> float { return 0.0 } +function wx_seed_one() -> int { return 1 } # what the story may ask of the sky export const WEATHER_NO_BIAS: int = 0 @@ -45,7 +53,6 @@ export property WeatherFact { from: int = 0 } -var wx_world: WeatherWorld = null var wx_kinds: []WeatherKind = null var wx_state: int = 0 var wx_next: int = 0 @@ -64,8 +71,6 @@ var wx_wet_yesterday: bool = false var wx_dice: Rng = null var wx_facts: Queue = null -export function weather_bind(w: WeatherWorld) -> void { wx_world = w } - # the facts, oldest first; the game drains them into its notices and sounds export function weather_facts() -> Queue { if wx_facts == null { wx_facts = queue_new("weather.facts") } diff --git a/packages/ludic.weather/tests/weather_test.ludic b/packages/ludic.weather/tests/weather_test.ludic index 03fa386e..80d7f0e9 100644 --- a/packages/ludic.weather/tests/weather_test.ludic +++ b/packages/ludic.weather/tests/weather_test.ludic @@ -46,6 +46,16 @@ program WeatherTest { return k } + bind WeatherWorld { + owns_world: fn fake_owns + hours: fn fake_hours + is_night: fn fake_night + bias: fn fake_bias + seed: fn fake_seed + odds: fn fake_odds + front_hours: fn fake_front_hours + } + # clear, rain and storm: clear always goes to rain, rain to storm, storm to clear function fresh() -> void { owns = true @@ -57,15 +67,6 @@ program WeatherTest { push(ks, kind("rain", true, false, odds3(0.0, 0.0, 1.0))) push(ks, kind("storm", true, true, odds3(1.0, 0.0, 0.0))) weather_kinds(ks) - let w = new WeatherWorld - w.owns_world = fn fake_owns - w.hours = fn fake_hours - w.is_night = fn fake_night - w.bias = fn fake_bias - w.seed = fn fake_seed - w.odds = fn fake_odds - w.front_hours = fn fake_front_hours - weather_bind(w) weather_config_wind(0.5, 2.5, 10.0, 19.0) weather_reset() } diff --git a/packages/ludic.weather/verbs.ludic b/packages/ludic.weather/verbs.ludic index 47083fba..58090133 100644 --- a/packages/ludic.weather/verbs.ludic +++ b/packages/ludic.weather/verbs.ludic @@ -1,9 +1,7 @@ # verbs.ludic - the only way the sky changes from outside: a morning, a reset, the host's word # a new trip: clear sky, a still-ish wind, nothing forecast; the dice from the world's seed export function weather_reset() -> void { - var seed = 1 - if wx_world != null and wx_world.seed != null { seed = wx_world.seed() } - wx_dice = rng_new(seed) + wx_dice = rng_new(WeatherWorld.seed()) let clear = wx_clear_kind() wx_state = clear wx_next = clear