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:
parent
cc9cec922a
commit
12cfcb10ac
12 changed files with 663 additions and 28 deletions
|
|
@ -1,42 +1,42 @@
|
|||
# hold.ludic - a recipe made by holding a button for its `hold` seconds: letting go wastes
|
||||
# nothing, and it stops by itself the moment it can no longer be made
|
||||
var cr_hold: int = -1
|
||||
var cr_hold_t: float = 0.0
|
||||
var craft_hold_i: int = -1
|
||||
var craft_hold_t: float = 0.0
|
||||
|
||||
export function craft_holds(i: int) -> bool { return craft_ok(i) and CraftRecipes[i].hold > 0.0 }
|
||||
|
||||
# the recipe being held, or -1
|
||||
export function craft_holding() -> int { return cr_hold }
|
||||
export function craft_holding() -> int { return craft_hold_i }
|
||||
|
||||
# starts holding one that is made by holding and can be made now
|
||||
export function craft_hold_start(i: int) -> bool {
|
||||
if not craft_holds(i) or not craft_can(i) { return false }
|
||||
cr_hold = i
|
||||
cr_hold_t = 0.0
|
||||
craft_hold_i = i
|
||||
craft_hold_t = 0.0
|
||||
return true
|
||||
}
|
||||
|
||||
export function craft_hold_stop() -> void {
|
||||
cr_hold = -1
|
||||
cr_hold_t = 0.0
|
||||
craft_hold_i = -1
|
||||
craft_hold_t = 0.0
|
||||
}
|
||||
|
||||
# how far through the hold on `i` is, 0 to 1
|
||||
export function craft_hold_frac(i: int) -> float {
|
||||
if cr_hold != i or not craft_holds(i) { return 0.0 }
|
||||
return Math.clamp(cr_hold_t / CraftRecipes[i].hold, 0.0, 1.0)
|
||||
if craft_hold_i != i or not craft_holds(i) { return 0.0 }
|
||||
return Math.clamp(craft_hold_t / CraftRecipes[i].hold, 0.0, 1.0)
|
||||
}
|
||||
|
||||
# `dt` real seconds of holding; true on the frame it is made
|
||||
export function craft_hold_run(dt: float) -> bool {
|
||||
if cr_hold < 0 { return false }
|
||||
if not craft_can(cr_hold) {
|
||||
if craft_hold_i < 0 { return false }
|
||||
if not craft_can(craft_hold_i) {
|
||||
craft_hold_stop()
|
||||
return false
|
||||
}
|
||||
cr_hold_t = cr_hold_t + dt
|
||||
if cr_hold_t <= CraftRecipes[cr_hold].hold { return false }
|
||||
let i = cr_hold
|
||||
craft_hold_t = craft_hold_t + dt
|
||||
if craft_hold_t <= CraftRecipes[craft_hold_i].hold { return false }
|
||||
let i = craft_hold_i
|
||||
craft_hold_stop()
|
||||
return craft_make(i)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ export port CraftPack {
|
|||
count: fn(int) -> int
|
||||
take: fn(int, int) -> bool
|
||||
add: fn(int, int) -> int
|
||||
room: fn(int) -> int = fn cr_no_limit
|
||||
leaves: fn(int) -> int = fn cr_leaves_nothing
|
||||
room: fn(int) -> int = fn craft_no_limit
|
||||
leaves: fn(int) -> int = fn craft_leaves_nothing
|
||||
}
|
||||
function cr_no_limit(it: int) -> int { return 2147483647 }
|
||||
function cr_leaves_nothing(it: int) -> int { return -1 }
|
||||
function craft_no_limit(it: int) -> int { return 2147483647 }
|
||||
function craft_leaves_nothing(it: int) -> int { return -1 }
|
||||
|
||||
# the stations. Unbound, there are none: only the hands are ready, and nothing can be started.
|
||||
# `start(station, recipe)` puts a timed recipe in; false gives the makings back.
|
||||
export port CraftStations {
|
||||
ready: fn(int) -> bool = fn cr_hands_only
|
||||
start: fn(int, int) -> bool = fn cr_no_start
|
||||
ready: fn(int) -> bool = fn craft_hands_only
|
||||
start: fn(int, int) -> bool = fn craft_no_start
|
||||
}
|
||||
function cr_hands_only(station: int) -> bool { return station == CRAFT_HANDS }
|
||||
function cr_no_start(station: int, recipe: int) -> bool { return false }
|
||||
function craft_hands_only(station: int) -> bool { return station == CRAFT_HANDS }
|
||||
function craft_no_start(station: int, recipe: int) -> bool { return false }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# verbs.ludic - can it be made, taking and giving back its makings, and making it
|
||||
var cr_facts: Queue<Crafted> = null
|
||||
var craft_fact_q: Queue<Crafted> = null
|
||||
|
||||
export function craft_facts() -> Queue<Crafted> {
|
||||
if cr_facts == null { cr_facts = queue_new("crafting.crafted") }
|
||||
return cr_facts
|
||||
if craft_fact_q == null { craft_fact_q = queue_new("crafting.crafted") }
|
||||
return craft_fact_q
|
||||
}
|
||||
|
||||
# every making is in the pack
|
||||
|
|
@ -46,7 +46,7 @@ export function craft_refund(i: int) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
function cr_fact(i: int, queued: bool) -> void {
|
||||
function craft_fact_push(i: int, queued: bool) -> void {
|
||||
let r = CraftRecipes[i]
|
||||
let c = new Crafted
|
||||
c.recipe = i
|
||||
|
|
@ -68,10 +68,10 @@ export function craft_make(i: int) -> bool {
|
|||
craft_refund(i)
|
||||
return false
|
||||
}
|
||||
cr_fact(i, true)
|
||||
craft_fact_push(i, true)
|
||||
return true
|
||||
}
|
||||
CraftPack.add(r.out, r.n)
|
||||
cr_fact(i, false)
|
||||
craft_fact_push(i, false)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
60
packages/ludic.shop/README.md
Normal file
60
packages/ludic.shop/README.md
Normal 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
|
||||
```
|
||||
104
packages/ludic.shop/demand.ludic
Normal file
104
packages/ludic.shop/demand.ludic
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# demand.ludic - what each vendor is short of this week, and the one thing it is full of: rolled
|
||||
# from the shop's own dice, seeded by the week, so it never moves anyone else's rolls
|
||||
var shop_wants_n: int = 3
|
||||
var shop_want_mult: float = 1.5
|
||||
var shop_glut_mult: float = 0.6
|
||||
var shop_week_days: int = 7
|
||||
var shop_seed: int = 1013
|
||||
var shop_week_rolled: int = -1
|
||||
var shop_want_list: []int = null # shop_wants_n per vendor, -1 empty
|
||||
var shop_full_list: []int = null # one per vendor, -1 none
|
||||
var shop_dice: Rng = null
|
||||
|
||||
# how many things a vendor wants a week, what that pays and what a glut pays, the week, the seed
|
||||
export function shop_config_demand(wants: int, want_mult: float, glut_mult: float, days: int, seed: int) -> void {
|
||||
shop_wants_n = wants
|
||||
shop_want_mult = want_mult
|
||||
shop_glut_mult = glut_mult
|
||||
shop_week_days = days
|
||||
shop_seed = seed
|
||||
shop_week_rolled = -1
|
||||
}
|
||||
|
||||
export function shop_week() -> int { return (ShopWorld.day() - 1) / shop_week_days }
|
||||
|
||||
function shop_pick_one(v: int, taken: []int) -> int {
|
||||
for t in 0 .. 200 {
|
||||
let it = rng_between(shop_dice, 0, shop_items() - 1)
|
||||
if not shop_buys(v, it) { continue }
|
||||
var dup = false
|
||||
for k in 0 .. len(taken) { if taken[k] == it { dup = true } }
|
||||
if not dup { return it }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function shop_roll_vendor(v: int) -> void {
|
||||
let taken = new []int
|
||||
for k in 0 .. shop_wants_n {
|
||||
let it = shop_pick_one(v, taken)
|
||||
shop_want_list[v * shop_wants_n + k] = it
|
||||
if it >= 0 { push(taken, it) }
|
||||
}
|
||||
shop_full_list[v] = shop_pick_one(v, taken)
|
||||
}
|
||||
|
||||
# the week's lists, rolled again only when the week turns
|
||||
function shop_roll() -> void {
|
||||
let week = shop_week()
|
||||
if week == shop_week_rolled and shop_want_list != null { return }
|
||||
shop_week_rolled = week
|
||||
shop_dice = rng_new(shop_seed + week * 37)
|
||||
shop_want_list = new []int
|
||||
shop_full_list = new []int
|
||||
for i in 0 .. shop_vendors() * shop_wants_n { push(shop_want_list, -1) }
|
||||
for v in 0 .. shop_vendors() { push(shop_full_list, -1) }
|
||||
if shop_items() == 0 { return }
|
||||
for v in 0 .. shop_vendors() { shop_roll_vendor(v) }
|
||||
}
|
||||
|
||||
export function shop_wants(v: int, it: int) -> bool {
|
||||
if not shop_vendor_ok(v) { return false }
|
||||
shop_roll()
|
||||
for k in 0 .. shop_wants_n { if shop_want_list[v * shop_wants_n + k] == it { return true } }
|
||||
return false
|
||||
}
|
||||
|
||||
export function shop_glutted(v: int, it: int) -> bool {
|
||||
if not shop_vendor_ok(v) { return false }
|
||||
shop_roll()
|
||||
return shop_full_list[v] == it
|
||||
}
|
||||
|
||||
# what a vendor wants this week, in its order
|
||||
export function shop_wanted(v: int) -> []int {
|
||||
let out = new []int
|
||||
if not shop_vendor_ok(v) { return out }
|
||||
shop_roll()
|
||||
for k in 0 .. shop_wants_n {
|
||||
let it = shop_want_list[v * shop_wants_n + k]
|
||||
if it >= 0 { push(out, it) }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
# the week's multiplier on what `v` pays for `it` (1 with no vendor)
|
||||
export function shop_demand_mult(v: int, it: int) -> float {
|
||||
if shop_wants(v, it) { return shop_want_mult }
|
||||
if shop_glutted(v, it) { return shop_glut_mult }
|
||||
return 1.0
|
||||
}
|
||||
|
||||
# this week, `v` wants `it` (a test, staging, a board that says so)
|
||||
export function shop_set_want(v: int, it: int) -> void {
|
||||
if not shop_vendor_ok(v) { return }
|
||||
shop_roll()
|
||||
shop_want_list[v * shop_wants_n] = it
|
||||
if shop_full_list[v] == it { shop_full_list[v] = -1 }
|
||||
}
|
||||
|
||||
function shop_demand_reset() -> void {
|
||||
shop_week_rolled = -1
|
||||
shop_want_list = null
|
||||
shop_full_list = null
|
||||
}
|
||||
10
packages/ludic.shop/index.ludic
Normal file
10
packages/ludic.shop/index.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# ludic.shop - the counter: items' base prices and vendors' shelves are the game's data, money and
|
||||
# the pack are asked through ports, and every sale or purchase is a fact
|
||||
module ludic_shop uses ludic_base
|
||||
numbers float
|
||||
import "ludic.base"
|
||||
import "state.ludic"
|
||||
import "demand.ludic"
|
||||
import "prices.ludic"
|
||||
import "trade.ludic"
|
||||
import "system.ludic"
|
||||
5
packages/ludic.shop/package.ludic
Normal file
5
packages/ludic.shop/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# ludic.shop - vendors that sell what they stock and buy what they take, at prices that move with
|
||||
# reputation and a weekly demand, and never become a fountain. Uses ludic.base and nothing else.
|
||||
package "ludic.shop"
|
||||
version "0.1.0"
|
||||
kind source
|
||||
43
packages/ludic.shop/prices.ludic
Normal file
43
packages/ludic.shop/prices.ludic
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# prices.ludic - what a thing costs and what it fetches. One standing moves both, toward each
|
||||
# other and never past: a newcomer pays a little over and sells a little under.
|
||||
function shop_rep01() -> float { return Math.clamp(float(ShopWorld.rep()), 0.0, 100.0) / 100.0 }
|
||||
|
||||
# the fraction of base a buyer pays, and a seller is paid before the item's own fraction
|
||||
export function shop_buy_factor() -> float { return shop_buy_new + (shop_buy_best - shop_buy_new) * shop_rep01() }
|
||||
export function shop_sell_factor() -> float { return shop_sell_new + (shop_sell_best - shop_sell_new) * shop_rep01() }
|
||||
|
||||
export function shop_price_buy(it: int) -> int {
|
||||
if not shop_item_ok(it) { return 0 }
|
||||
let p = int(float(shop_base[it]) * shop_buy_factor() * ShopWorld.buy_mult() + 0.5)
|
||||
return Math.max(p, 1)
|
||||
}
|
||||
|
||||
# what vendor `v` pays (-1: no vendor, no demand). ROUNDED, so cheap raw material keeps its
|
||||
# order; and a vendor that stocks the thing never pays what it charges, not even a floor of $1
|
||||
export function shop_price_sell_at(v: int, it: int) -> int {
|
||||
if not shop_item_ok(it) { return 0 }
|
||||
let f = shop_sell_factor() * shop_sell_frac[it] * shop_demand_mult(v, it)
|
||||
var p = Math.max(int(float(shop_base[it]) * f + 0.5), 1)
|
||||
if shop_stocks(v, it) { p = Math.max(Math.min(p, shop_price_buy(it) - 1), 0) }
|
||||
return p
|
||||
}
|
||||
|
||||
# what the vendor at the counter pays
|
||||
export function shop_price_sell(it: int) -> int { return shop_price_sell_at(shop_at, it) }
|
||||
|
||||
export function shop_stocks(v: int, it: int) -> bool {
|
||||
if not shop_vendor_ok(v) or shop_vendor_list[v].stock == null { return false }
|
||||
let st = shop_vendor_list[v].stock
|
||||
for k in 0 .. len(st) { if st[k] == it { return true } }
|
||||
return false
|
||||
}
|
||||
|
||||
# what `v` takes: its list and whatever the world says, and never the one thing it refuses
|
||||
export function shop_buys(v: int, it: int) -> bool {
|
||||
if not shop_vendor_ok(v) or it == shop_vendor_list[v].refuses { return false }
|
||||
if ShopWorld.also_buys(v, it) { return true }
|
||||
let b = shop_vendor_list[v].buys
|
||||
if b == null { return false }
|
||||
for k in 0 .. len(b) { if b[k] == it { return true } }
|
||||
return false
|
||||
}
|
||||
101
packages/ludic.shop/state.ludic
Normal file
101
packages/ludic.shop/state.ludic
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# state.ludic - the ports, the data a game hands over, the numbers it may change, and the facts
|
||||
# the world a price depends on. Unbound: day one, no shortage, nobody buys beyond their list.
|
||||
export port ShopWorld {
|
||||
rep: fn() -> int # the player's standing, 0 to 100
|
||||
day: fn() -> int = fn shop_day_one # the game day; the demand turns with the week
|
||||
buy_mult: fn() -> float = fn shop_one # a shortage on everything bought
|
||||
also_buys: fn(int, int) -> bool = fn shop_never # (vendor, item) taken beyond the vendor's list
|
||||
}
|
||||
function shop_day_one() -> int { return 1 }
|
||||
function shop_one() -> float { return 1.0 }
|
||||
function shop_never(v: int, it: int) -> bool { return false }
|
||||
|
||||
export port ShopPurse {
|
||||
money: fn() -> int
|
||||
spend: fn(int) -> bool
|
||||
earn: fn(int) -> void
|
||||
}
|
||||
|
||||
export port ShopPack {
|
||||
count: fn(int) -> int
|
||||
take: fn(int, int) -> bool
|
||||
add: fn(int, int) -> int
|
||||
room: fn(int) -> int = fn shop_no_limit
|
||||
}
|
||||
function shop_no_limit(it: int) -> int { return 2147483647 }
|
||||
|
||||
# one vendor's shelves: what it sells, what it buys, and one thing it will not take (-1: none)
|
||||
export property ShopVendor {
|
||||
stock: []int = null
|
||||
buys: []int = null
|
||||
refuses: int = -1
|
||||
}
|
||||
|
||||
var shop_base: []int = null # an item's base price
|
||||
var shop_sell_frac: []float = null # what fraction of the curve a vendor pays for it
|
||||
var shop_vendor_list: []ShopVendor = null
|
||||
var shop_at: int = -1 # the vendor at the counter, -1 none
|
||||
|
||||
# the curve: buying from `buy_new` of base at rep 0 down to `buy_best` at 100; selling the same way
|
||||
var shop_buy_new: float = 1.15
|
||||
var shop_buy_best: float = 0.92
|
||||
var shop_sell_new: float = 0.62
|
||||
var shop_sell_best: float = 0.85
|
||||
|
||||
# the items: a base price and a sell fraction each (raw material pays less than something made)
|
||||
export function shop_config_items(prices: []int, sell: []float) -> void {
|
||||
shop_base = prices
|
||||
shop_sell_frac = sell
|
||||
}
|
||||
|
||||
export function shop_config_curve(buy_new: float, buy_best: float, sell_new: float, sell_best: float) -> void {
|
||||
shop_buy_new = buy_new
|
||||
shop_buy_best = buy_best
|
||||
shop_sell_new = sell_new
|
||||
shop_sell_best = sell_best
|
||||
}
|
||||
|
||||
export function shop_clear_vendors() -> void { shop_vendor_list = new []ShopVendor }
|
||||
|
||||
# a vendor; its number is the order added
|
||||
export function shop_add_vendor(stock: []int, buys: []int, refuses: int) -> int {
|
||||
if shop_vendor_list == null { shop_clear_vendors() }
|
||||
let v = new ShopVendor
|
||||
v.stock = stock
|
||||
v.buys = buys
|
||||
v.refuses = refuses
|
||||
push(shop_vendor_list, v)
|
||||
return len(shop_vendor_list) - 1
|
||||
}
|
||||
|
||||
export function shop_vendors() -> int {
|
||||
if shop_vendor_list == null { return 0 }
|
||||
return len(shop_vendor_list)
|
||||
}
|
||||
|
||||
export function shop_items() -> int {
|
||||
if shop_base == null { return 0 }
|
||||
return len(shop_base)
|
||||
}
|
||||
|
||||
export function shop_vendor_ok(v: int) -> bool { return v >= 0 and v < shop_vendors() }
|
||||
export function shop_item_ok(it: int) -> bool { return it >= 0 and it < shop_items() }
|
||||
|
||||
export const SHOP_BOUGHT: int = 0
|
||||
export const SHOP_SOLD: int = 1
|
||||
|
||||
# something changed hands: `n` of `item` at `price` each
|
||||
export property ShopTrade {
|
||||
what: int = 0
|
||||
vendor: int = -1
|
||||
item: int = 0
|
||||
n: int = 0
|
||||
price: int = 0
|
||||
}
|
||||
|
||||
var shop_trade_q: Queue<ShopTrade> = null
|
||||
|
||||
export function shop_trades() -> Queue<ShopTrade> {
|
||||
if shop_trade_q == null { shop_trade_q = queue_new("shop.trades") }
|
||||
return shop_trade_q
|
||||
}
|
||||
13
packages/ludic.shop/system.ludic
Normal file
13
packages/ludic.shop/system.ludic
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# system.ludic - the shop as a system: a reset and nothing saved (the week's demand follows from
|
||||
# the day, and nobody is at a counter across a save)
|
||||
export function shop_reset() -> void {
|
||||
shop_at = -1
|
||||
shop_demand_reset()
|
||||
q_clear(shop_trades())
|
||||
}
|
||||
|
||||
export function shop_system() -> System {
|
||||
let s = system_new("shop", PH_COMMIT)
|
||||
s.reset = fn shop_reset
|
||||
return s
|
||||
}
|
||||
236
packages/ludic.shop/tests/shop_test.ludic
Normal file
236
packages/ludic.shop/tests/shop_test.ludic
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# shop_test.ludic - a counter over fake money and a fake pack: buying and selling, the week's
|
||||
# demand from the shop's own dice, and the three balance invariants a price curve must keep:
|
||||
# a shop is not a fountain, friction falls with standing and never inverts, raw keeps its order
|
||||
import "ludic.shop"
|
||||
import "ludic.base"
|
||||
program ShopTest {
|
||||
numbers float
|
||||
const WOOD: int = 0
|
||||
const CHANTERELLE: int = 1
|
||||
const PUFFBALL: int = 2
|
||||
const TROUT: int = 3
|
||||
const COOKED: int = 4
|
||||
const BAIT: int = 5
|
||||
const ROPE: int = 6
|
||||
const JACKET: int = 7
|
||||
const GRUB: int = 8
|
||||
const BURNT: int = 9
|
||||
const RAW: float = 0.50
|
||||
const MADE: float = 1.15
|
||||
const BOUGHT: float = 0.55
|
||||
|
||||
var rep: int = 0
|
||||
var day: int = 1
|
||||
var money: int = 0
|
||||
var pack: []int = null
|
||||
var food_too: bool = false
|
||||
|
||||
function fake_rep() -> int { return rep }
|
||||
function fake_day() -> int { return day }
|
||||
function fake_food(v: int, it: int) -> bool { return food_too and v == 1 and (it == TROUT or it == COOKED or it == BURNT) }
|
||||
function fake_money() -> int { return money }
|
||||
function fake_spend(n: int) -> bool {
|
||||
if n > money { return false }
|
||||
money -= n
|
||||
return true
|
||||
}
|
||||
function fake_earn(n: int) -> void { money += n }
|
||||
function fake_count(it: int) -> int { return pack[it] }
|
||||
function fake_take(it: int, n: int) -> bool {
|
||||
if pack[it] < n { return false }
|
||||
pack[it] = pack[it] - n
|
||||
return true
|
||||
}
|
||||
function fake_add(it: int, n: int) -> int {
|
||||
pack[it] = pack[it] + n
|
||||
return n
|
||||
}
|
||||
function fake_room(it: int) -> int {
|
||||
if it == JACKET { return 1 - pack[it] }
|
||||
return 99
|
||||
}
|
||||
|
||||
bind ShopWorld { rep: fn fake_rep, day: fn fake_day, also_buys: fn fake_food }
|
||||
bind ShopPurse { money: fn fake_money, spend: fn fake_spend, earn: fn fake_earn }
|
||||
bind ShopPack { count: fn fake_count, take: fn fake_take, add: fn fake_add, room: fn fake_room }
|
||||
|
||||
function ints(a: int, b: int, c: int, d: int) -> []int {
|
||||
let out = new []int
|
||||
if a >= 0 { push(out, a) }
|
||||
if b >= 0 { push(out, b) }
|
||||
if c >= 0 { push(out, c) }
|
||||
if d >= 0 { push(out, d) }
|
||||
return out
|
||||
}
|
||||
|
||||
# the valley's own numbers: base prices and the sell classes
|
||||
function fresh() -> void {
|
||||
let prices = new []int
|
||||
let sell = new []float
|
||||
let p = ints(3, 6, 8, 9)
|
||||
let q = ints(10, 1, 6, 60)
|
||||
for i in 0 .. 4 { push(prices, p[i]) }
|
||||
for i in 0 .. 4 { push(prices, q[i]) }
|
||||
push(prices, 1)
|
||||
push(prices, 1)
|
||||
push(sell, RAW)
|
||||
push(sell, RAW)
|
||||
push(sell, RAW)
|
||||
push(sell, RAW)
|
||||
push(sell, MADE)
|
||||
push(sell, MADE)
|
||||
push(sell, BOUGHT)
|
||||
push(sell, BOUGHT)
|
||||
push(sell, RAW)
|
||||
push(sell, RAW)
|
||||
shop_config_items(prices, sell)
|
||||
shop_clear_vendors()
|
||||
shop_add_vendor(ints(ROPE, JACKET, -1, -1), ints(WOOD, CHANTERELLE, -1, -1), -1)
|
||||
shop_add_vendor(ints(BAIT, COOKED, -1, -1), ints(WOOD, PUFFBALL, BAIT, -1), BURNT)
|
||||
shop_add_vendor(ints(BAIT, COOKED, GRUB, -1), ints(TROUT, COOKED, BAIT, GRUB), -1)
|
||||
pack = new []int
|
||||
for i in 0 .. 10 { push(pack, 0) }
|
||||
rep = 0
|
||||
day = 1
|
||||
money = 0
|
||||
shop_reset()
|
||||
}
|
||||
|
||||
function trades() -> []ShopTrade { return q_drain(shop_trades()) }
|
||||
|
||||
test "buying takes the price and puts one in the pack, and says so" {
|
||||
fresh()
|
||||
money = 100
|
||||
shop_visit(0)
|
||||
let pr = shop_price_buy(JACKET)
|
||||
expect(shop_buy(JACKET))
|
||||
expect_eq(money, 100 - pr)
|
||||
expect_eq(pack[JACKET], 1)
|
||||
expect(not shop_buy(JACKET))
|
||||
let ts = trades()
|
||||
expect_eq(len(ts), 1)
|
||||
expect_eq(ts[0].what, SHOP_BOUGHT)
|
||||
expect_eq(ts[0].vendor, 0)
|
||||
expect_eq(ts[0].price, pr)
|
||||
}
|
||||
|
||||
test "no money, nothing bought and nothing spent" {
|
||||
fresh()
|
||||
money = 3
|
||||
expect(not shop_can_buy(JACKET))
|
||||
expect(not shop_buy(JACKET))
|
||||
expect_eq(money, 3)
|
||||
expect_eq(len(trades()), 0)
|
||||
}
|
||||
|
||||
test "selling pays what the counter's vendor pays, only what the pack has" {
|
||||
fresh()
|
||||
pack[TROUT] = 3
|
||||
shop_visit(2)
|
||||
let pr = shop_price_sell(TROUT)
|
||||
expect_eq(shop_sell(TROUT, 5), 3)
|
||||
expect_eq(money, pr * 3)
|
||||
expect_eq(pack[TROUT], 0)
|
||||
expect_eq(trades()[0].n, 3)
|
||||
}
|
||||
|
||||
test "a vendor buys its list and what the world adds, never what it refuses" {
|
||||
fresh()
|
||||
expect(shop_buys(1, PUFFBALL))
|
||||
expect(not shop_buys(1, TROUT))
|
||||
food_too = true
|
||||
expect(shop_buys(1, TROUT))
|
||||
expect(not shop_buys(1, BURNT))
|
||||
pack[BURNT] = 1
|
||||
pack[TROUT] = 1
|
||||
let s = shop_sellable(1)
|
||||
expect_eq(len(s), 1)
|
||||
expect_eq(s[0], TROUT)
|
||||
shop_visit(1)
|
||||
expect_eq(shop_sell(BURNT, 1), 0)
|
||||
}
|
||||
|
||||
test "the week's demand is the shop's own dice, and turns with the week" {
|
||||
fresh()
|
||||
let a = shop_wanted(0)
|
||||
expect(len(a) > 0)
|
||||
for k in 0 .. len(a) { expect(shop_buys(0, a[k])) }
|
||||
let again = shop_wanted(0)
|
||||
expect_eq(again[0], a[0])
|
||||
shop_visit(0)
|
||||
let it = a[0]
|
||||
let plain = shop_price_sell_at(-1, it)
|
||||
expect(shop_demand_mult(0, it) == 1.5)
|
||||
expect(shop_price_sell(it) >= plain)
|
||||
var moved = false
|
||||
for w in 1 .. 12 {
|
||||
day = 1 + w * 7
|
||||
let b = shop_wanted(0)
|
||||
if len(b) != len(a) or b[0] != a[0] { moved = true }
|
||||
}
|
||||
expect(moved)
|
||||
}
|
||||
|
||||
test "a shop is not a fountain: nothing it stocks sells back for what it costs" {
|
||||
fresh()
|
||||
for v in 0 .. shop_vendors() {
|
||||
let st = shop_stock(v)
|
||||
for k in 0 .. len(st) {
|
||||
for r in 0 .. 11 {
|
||||
rep = r * 10
|
||||
shop_set_want(v, st[k])
|
||||
let buy = shop_price_buy(st[k])
|
||||
let sell = shop_price_sell_at(v, st[k])
|
||||
expect(sell < buy)
|
||||
expect(sell >= 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "not even at a dollar: a penny item a vendor stocks fetches nothing" {
|
||||
fresh()
|
||||
rep = 100
|
||||
shop_set_want(2, GRUB)
|
||||
expect_eq(shop_price_buy(GRUB), 1)
|
||||
expect_eq(shop_price_sell_at(2, GRUB), 0)
|
||||
expect(shop_price_sell_at(-1, GRUB) >= 1)
|
||||
}
|
||||
|
||||
test "friction falls with standing and never inverts" {
|
||||
fresh()
|
||||
var last_buy = 1000000
|
||||
var last_sell = -1
|
||||
for r in 0 .. 11 {
|
||||
rep = r * 10
|
||||
expect(shop_buy_factor() > shop_sell_factor())
|
||||
let b = shop_price_buy(JACKET)
|
||||
let s = shop_price_sell_at(-1, COOKED)
|
||||
expect(b <= last_buy)
|
||||
expect(s >= last_sell)
|
||||
last_buy = b
|
||||
last_sell = s
|
||||
}
|
||||
rep = 0
|
||||
let b0 = shop_price_buy(JACKET)
|
||||
rep = 100
|
||||
expect(shop_price_buy(JACKET) < b0)
|
||||
rep = 0
|
||||
expect(float(b0) / 60.0 <= 1.25)
|
||||
}
|
||||
|
||||
test "raw material keeps its order, and is not an income" {
|
||||
fresh()
|
||||
for r in 0 .. 11 {
|
||||
rep = r * 10
|
||||
let w = shop_price_sell_at(-1, WOOD)
|
||||
let ch = shop_price_sell_at(-1, CHANTERELLE)
|
||||
let pf = shop_price_sell_at(-1, PUFFBALL)
|
||||
let tr = shop_price_sell_at(-1, TROUT)
|
||||
expect(w <= ch and ch <= pf and pf <= tr)
|
||||
expect(pf <= 8 / 2)
|
||||
}
|
||||
rep = 0
|
||||
expect(shop_price_sell_at(-1, WOOD) < shop_price_sell_at(-1, TROUT))
|
||||
}
|
||||
}
|
||||
63
packages/ludic.shop/trade.ludic
Normal file
63
packages/ludic.shop/trade.ludic
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# trade.ludic - standing at a counter, and the only ways anything changes hands
|
||||
export function shop_visit(v: int) -> void {
|
||||
if shop_vendor_ok(v) { shop_at = v } else { shop_at = -1 }
|
||||
}
|
||||
|
||||
export function shop_away() -> void { shop_at = -1 }
|
||||
|
||||
# the vendor at the counter, -1 none
|
||||
export function shop_vendor() -> int { return shop_at }
|
||||
|
||||
export function shop_at_counter() -> bool { return shop_at >= 0 }
|
||||
|
||||
# what `v` has on its shelves, in shelf order
|
||||
export function shop_stock(v: int) -> []int {
|
||||
let out = new []int
|
||||
if not shop_vendor_ok(v) or shop_vendor_list[v].stock == null { return out }
|
||||
let st = shop_vendor_list[v].stock
|
||||
for k in 0 .. len(st) { push(out, st[k]) }
|
||||
return out
|
||||
}
|
||||
|
||||
# what in the pack `v` would buy
|
||||
export function shop_sellable(v: int) -> []int {
|
||||
let out = new []int
|
||||
for it in 0 .. shop_items() { if ShopPack.count(it) > 0 and shop_buys(v, it) { push(out, it) } }
|
||||
return out
|
||||
}
|
||||
|
||||
function shop_trade_fact(what: int, it: int, n: int, price: int) -> void {
|
||||
let t = new ShopTrade
|
||||
t.what = what
|
||||
t.vendor = shop_at
|
||||
t.item = it
|
||||
t.n = n
|
||||
t.price = price
|
||||
q_push(shop_trades(), t)
|
||||
}
|
||||
|
||||
export function shop_can_buy(it: int) -> bool {
|
||||
return shop_item_ok(it) and ShopPurse.money() >= shop_price_buy(it) and ShopPack.room(it) > 0
|
||||
}
|
||||
|
||||
# one, paid for and in the pack; false and nothing spent when it cannot be
|
||||
export function shop_buy(it: int) -> bool {
|
||||
if not shop_can_buy(it) { return false }
|
||||
let pr = shop_price_buy(it)
|
||||
if not ShopPurse.spend(pr) { return false }
|
||||
ShopPack.add(it, 1)
|
||||
shop_trade_fact(SHOP_BOUGHT, it, 1, pr)
|
||||
return true
|
||||
}
|
||||
|
||||
# `n` to the vendor at the counter (as many as the pack has); how many went
|
||||
export function shop_sell(it: int, n: int) -> int {
|
||||
if not shop_buys(shop_at, it) { return 0 }
|
||||
let k = Math.min(n, ShopPack.count(it))
|
||||
if k <= 0 { return 0 }
|
||||
let pr = shop_price_sell(it)
|
||||
if not ShopPack.take(it, k) { return 0 }
|
||||
ShopPurse.earn(pr * k)
|
||||
shop_trade_fact(SHOP_SOLD, it, k, pr)
|
||||
return k
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue