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

Items (base price, sell fraction) and vendors (stock, buys, refuses) are the game's data handed
over at boot; standing, the day, a shortage and "buys beyond its list" are the ShopWorld port;
money is ShopPurse and the pack ShopPack. The week's wanted and glutted items come from an Rng
seeded by the week, never the world's. The balance invariants - a shop is not a fountain,
friction falls and never inverts, raw keeps its order - are the package's tests (nine).

ludic.crafting: its private names carry the package's prefix too (a private name is still one
global namespace with the game's - ludic.shop's `sp_seed` met the game's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 06:44:44 +03:00
parent cc9cec922a
commit 12cfcb10ac
12 changed files with 663 additions and 28 deletions

View file

@ -0,0 +1,60 @@
# 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`](../ludic.base/README.md) and nothing else.
```ludic
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
```ludic
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
```bash
ludic test packages/ludic.shop
```