ludic/packages/ludic.shop
Orkuncakilkaya 71735b10a2 feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune
ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v),
q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding
facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)).

ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no
longer uses, nor anything it calls, and the argument that fills it - including an argument for a
parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is
told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would
make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:19:56 +03:00
..
tests feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
demand.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
index.ludic feat(packages): ludic.shop - prices by standing with the fountain guard, a weekly demand on its own dice, stock, buying and selling through purse and pack ports, a trade fact 2026-09-25 06:44:44 +03:00
package.ludic feat(packages): ludic.shop - prices by standing with the fountain guard, a weekly demand on its own dice, stock, buying and selling through purse and pack ports, a trade fact 2026-09-25 06:44:44 +03:00
prices.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
README.md feat(packages): ludic.shop - prices by standing with the fountain guard, a weekly demand on its own dice, stock, buying and selling through purse and pack ports, a trade fact 2026-09-25 06:44:44 +03:00
state.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
system.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00
trade.ludic feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune 2026-09-26 04:19:56 +03:00

ludic.shop

Vendors behind counters: what each sells and buys, what a thing costs and fetches at the player's standing, what each vendor is short of this week, and buying and selling through the game's money and pack. Uses ludic.base and nothing else.

import "ludic.shop"

Items and vendors are the game's data, handed over as it boots: every item's base price and the fraction of the curve a vendor pays for it (shop_config_items), and every vendor's shelves, what it buys and the one thing it refuses (shop_add_vendor, numbered in the order added). A vendor is a number; its name, face and words stay the game's.

The rules it keeps

These hold whatever the numbers become, and the tests hold them:

  1. A shop is not a fountain. A vendor never pays as much for a thing it stocks as it charges for it - not in a week it wants them, not at the best standing, and not even by a $1 floor: a penny item it stocks fetches nothing.
  2. Friction falls with standing and never inverts. Buying runs from 1.15x base at standing 0 to 0.92x at 100, selling from 0.62x to 0.85x (times the item's fraction), so a newcomer pays a little over and sells a little under, and the two never cross.
  3. Raw material keeps its order. A sale is ROUNDED, not truncated, so cheap things spanning a wide range of base prices do not all land on $1.

The ports

export port ShopWorld {
  rep: fn() -> int                       # standing, 0 to 100
  day: fn() -> int                       # the game day; the demand turns every week (unbound: 1)
  buy_mult: fn() -> float                # a shortage on everything bought (unbound: 1)
  also_buys: fn(int, int) -> bool        # (vendor, item) taken beyond its list (unbound: none)
}
export port ShopPurse { money: fn() -> int, spend: fn(int) -> bool, earn: fn(int) -> void }
export port ShopPack { count, take, add, room }   # room unbound: no limit

API

shop_config_items(prices, sell), shop_clear_vendors(), shop_add_vendor(stock, buys, refuses) -> int the data
shop_config_curve(buy_new, buy_best, sell_new, sell_best), shop_config_demand(wants, want_mult, glut_mult, days, seed) the numbers (defaults 1.15, 0.92, 0.62, 0.85; 3 wanted at 1.5x, one glut at 0.6x, 7 days, seed 1013)
shop_visit(v), shop_away(), shop_vendor(), shop_at_counter() who is behind the counter (-1 nobody)
shop_price_buy(it), shop_price_sell(it), shop_price_sell_at(v, it), shop_buy_factor(), shop_sell_factor() prices; with no vendor, no demand and no fountain guard
shop_stocks(v, it), shop_buys(v, it), shop_stock(v), shop_sellable(v) shelves, and what in the pack a vendor takes
shop_wants(v, it), shop_glutted(v, it), shop_wanted(v), shop_demand_mult(v, it), shop_week(), shop_set_want(v, it) the week's demand, from the shop's own Rng seeded by the week
shop_can_buy(it), shop_buy(it) -> bool, shop_sell(it, n) -> int one bought; up to n sold to the vendor at the counter
shop_trades() -> Queue<ShopTrade> { what: SHOP_BOUGHT / SHOP_SOLD, vendor, item, n, price }
shop_reset(), shop_system() -> System "shop", PH_COMMIT: a reset; nothing is saved (the week follows from the day)

Tests

ludic test packages/ludic.shop