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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 05:36:11 +03:00
parent f4062010e9
commit 63e29f024c
31 changed files with 130 additions and 149 deletions

View file

@ -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 |

View file

@ -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 {

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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 }

View file

@ -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<WeatherFact> = 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<WeatherFact> {
if wx_facts == null { wx_facts = queue_new("weather.facts") }

View file

@ -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()
}

View file

@ -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