diff --git a/packages/ludic.inventory/README.md b/packages/ludic.inventory/README.md new file mode 100644 index 00000000..460bf8db --- /dev/null +++ b/packages/ludic.inventory/README.md @@ -0,0 +1,41 @@ +# ludic.inventory + +A pack: a count per kind of item. The game names the kinds and says how many of each fit; the +package keeps the counts, refuses what does not fit, and reports every change. Uses +[`ludic.base`](../ludic.base/README.md) and nothing else. + +```ludic +import "ludic.inventory" +``` + +A kind is the game's number for an item (its place in the game's item table). The game hands the +package each kind's NAME with `inv_config(keys)`, and the save is by name - `{"counts": {"wood": +3}}` - so a game can add, remove or reorder its items without a migration; a name the build no +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) +} +``` + +## API + +| | | +| --- | --- | +| `inv_bind(r)`, `inv_config(keys)` | the port, 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 | +| `inv_clear()` | everything gone, silently | +| `inv_count(it)`, `inv_has(it, n)`, `inv_room(it)`, `inv_limit(it)`, `inv_total()`, `inv_kinds()` | what the game asks; a kind out of range counts 0 and has no room | +| `inv_changes() -> Queue` | the facts: `{ item, before, after }`, one per change, none for a no-op or a load | +| `inv_system() -> System` | `"inventory"`, `PH_COMMIT`: reset, and its save section (`inv_save`, `inv_load`) | + +## Tests + +```bash +ludic build packages/ludic.inventory/tests/inventory_test.ludic --headless -o /tmp/inventory_test && /tmp/inventory_test +``` diff --git a/packages/ludic.inventory/index.ludic b/packages/ludic.inventory/index.ludic new file mode 100644 index 00000000..253d95e4 --- /dev/null +++ b/packages/ludic.inventory/index.ludic @@ -0,0 +1,8 @@ +# 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 +numbers float +import "ludic.base" +import "state.ludic" +import "verbs.ludic" +import "system.ludic" diff --git a/packages/ludic.inventory/package.ludic b/packages/ludic.inventory/package.ludic new file mode 100644 index 00000000..2222c5af --- /dev/null +++ b/packages/ludic.inventory/package.ludic @@ -0,0 +1,5 @@ +# ludic.inventory - a pack: a count per kind of item, with room that the game decides. Uses +# ludic.base and nothing else. See README.md. +package "ludic.inventory" +version "0.1.0" +kind source diff --git a/packages/ludic.inventory/state.ludic b/packages/ludic.inventory/state.ludic new file mode 100644 index 00000000..e00f7d9c --- /dev/null +++ b/packages/ludic.inventory/state.ludic @@ -0,0 +1,62 @@ +# 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 +} + +# a count changed, however: added, taken, or set outright +export property ItemChanged { + item: int = 0 + before: int = 0 + 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 + inv_clear() +} + +export function inv_changes() -> Queue { + if iv_changes == null { iv_changes = queue_new("inventory.changes") } + return iv_changes +} + +function iv_valid(it: int) -> bool { return iv_counts != null and it >= 0 and it < len(iv_counts) } + +export function inv_kinds() -> int { + if iv_keys == null { return 0 } + return len(iv_keys) +} + +export function inv_count(it: int) -> int { + if not iv_valid(it) { return 0 } + return iv_counts[it] +} + +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) +} + +# how many more fit +export function inv_room(it: int) -> int { + if not iv_valid(it) { return 0 } + return Math.max(inv_limit(it) - iv_counts[it], 0) +} + +export function inv_has(it: int, n: int) -> bool { return inv_count(it) >= n } + +export function inv_total() -> int { + var t = 0 + if iv_counts == null { return 0 } + for i in 0 .. len(iv_counts) { t += iv_counts[i] } + return t +} diff --git a/packages/ludic.inventory/system.ludic b/packages/ludic.inventory/system.ludic new file mode 100644 index 00000000..b1d932b0 --- /dev/null +++ b/packages/ludic.inventory/system.ludic @@ -0,0 +1,32 @@ +# system.ludic - the pack as a system: reset and its own save section, each count under its +# kind's name, so kinds can be added or reordered without a migration +export function inv_reset() -> void { + inv_clear() + q_clear(inv_changes()) +} + +export function inv_save() -> Val { + let counts = Value.object() + for i in 0 .. inv_kinds() { + if iv_counts[i] != 0 { sv_put_int(counts, iv_keys[i], iv_counts[i]) } + } + let v = Value.object() + Value.put(v, "counts", counts) + return v +} + +# a name this build does not know is dropped +export function inv_load(v: Val, version: int) -> void { + inv_clear() + if Value.has(v, "counts") == 0 { return } + let counts = Value.get(v, "counts") + for i in 0 .. inv_kinds() { iv_counts[i] = Math.max(sv_int(counts, iv_keys[i], 0), 0) } +} + +export function inv_system() -> System { + let s = system_new("inventory", PH_COMMIT) + s.reset = fn inv_reset + s.save = fn inv_save + s.load = fn inv_load + return s +} diff --git a/packages/ludic.inventory/tests/inventory_test.ludic b/packages/ludic.inventory/tests/inventory_test.ludic new file mode 100644 index 00000000..0680c805 --- /dev/null +++ b/packages/ludic.inventory/tests/inventory_test.ludic @@ -0,0 +1,97 @@ +# inventory_test.ludic - the pack: room from the port, add what fits, take all or none, set +# outright, a fact per change, and a save by name that survives the kinds being reordered +import "ludic.inventory" +import "ludic.base" +program InventoryTest { + numbers float + const WOOD: int = 0 + const ROD: int = 1 + const FISH: int = 2 + + function limit(it: int) -> int { + if it == ROD { return 1 } + return 10 + } + + function names(a: string, b: string, c: string) -> []string { + let k = new []string + push(k, a) + push(k, b) + push(k, c) + return k + } + + function fresh() -> void { + let r = new PackRules + r.stack_limit = fn limit + inv_bind(r) + inv_config(names("wood", "rod", "fish")) + inv_reset() + } + + function changes() -> []ItemChanged { return q_drain(inv_changes()) } + function waiting() -> int { return q_len(inv_changes()) } + + test "add puts in what fits and says how many" { + fresh() + expect_eq(inv_add(WOOD, 7), 7) + expect_eq(inv_add(WOOD, 7), 3) + expect_eq(inv_count(WOOD), 10) + expect_eq(inv_room(WOOD), 0) + expect_eq(inv_add(ROD, 2), 1) + expect_eq(inv_total(), 11) + } + + test "take is all or none" { + fresh() + inv_add(FISH, 3) + expect(not inv_take(FISH, 4)) + expect_eq(inv_count(FISH), 3) + expect(inv_take(FISH, 3)) + expect(not inv_has(FISH, 1)) + } + + test "set goes past the room, never below zero" { + fresh() + inv_set(ROD, 3) + expect_eq(inv_count(ROD), 3) + expect_eq(inv_room(ROD), 0) + inv_set(ROD, -2) + expect_eq(inv_count(ROD), 0) + } + + test "every change is a fact, and no change is not" { + fresh() + inv_add(WOOD, 2) + inv_take(WOOD, 1) + inv_set(WOOD, 1) + let cs = changes() + expect_eq(len(cs), 2) + expect_eq(cs[0].after, 2) + expect_eq(cs[1].before, 2) + expect_eq(cs[1].after, 1) + expect_eq(waiting(), 0) + } + + test "an item out of range is nothing" { + fresh() + expect_eq(inv_add(9, 1), 0) + expect_eq(inv_count(-1), 0) + expect(not inv_take(9, 0)) + } + + test "the save is by name, so a reordered table still reads it" { + fresh() + inv_add(WOOD, 4) + inv_add(FISH, 2) + let root = save_tree() + save_section(root, "inventory", 1, inv_save()) + let text = save_encode(root) + inv_config(names("fish", "bait", "wood")) + let n = load_section(save_decode(text), "inventory") + inv_load(n.data, n.version) + expect_eq(inv_count(0), 2) + expect_eq(inv_count(1), 0) + expect_eq(inv_count(2), 4) + } +} diff --git a/packages/ludic.inventory/verbs.ludic b/packages/ludic.inventory/verbs.ludic new file mode 100644 index 00000000..017a2eac --- /dev/null +++ b/packages/ludic.inventory/verbs.ludic @@ -0,0 +1,38 @@ +# verbs.ludic - the only way a count changes, and each change is said once +function iv_change(it: int, after: int) -> void { + let before = iv_counts[it] + if before == after { return } + iv_counts[it] = after + let c = new ItemChanged + c.item = it + c.before = before + c.after = after + q_push(inv_changes(), c) +} + +# as many of n as there is room for; returns how many went in +export function inv_add(it: int, n: int) -> int { + if not iv_valid(it) or n <= 0 { return 0 } + let k = Math.min(n, inv_room(it)) + if k > 0 { iv_change(it, iv_counts[it] + k) } + return k +} + +# all n or none +export function inv_take(it: int, n: int) -> bool { + if not iv_valid(it) or iv_counts[it] < n { return false } + if n > 0 { iv_change(it, iv_counts[it] - n) } + return true +} + +# a count outright, room or not (a starting kit, a correction, the network's word); never below 0 +export function inv_set(it: int, n: int) -> void { + if not iv_valid(it) { return } + iv_change(it, Math.max(n, 0)) +} + +# everything gone, silently: a new trip, or before a load +export function inv_clear() -> void { + iv_counts = new []int + for i in 0 .. inv_kinds() { push(iv_counts, 0) } +} diff --git a/packages/ludic.wallet/README.md b/packages/ludic.wallet/README.md new file mode 100644 index 00000000..2d597b2d --- /dev/null +++ b/packages/ludic.wallet/README.md @@ -0,0 +1,29 @@ +# ludic.wallet + +Money carried, in whole units: earned, spent only when it is there, lost when a cost happens +anyway, and every change reported. Uses [`ludic.base`](../ludic.base/README.md) and nothing +else. Prices, reputation and what a thing costs are the game's; the wallet only holds the money. + +```ludic +import "ludic.wallet" +``` + +No ports: the wallet asks the world nothing. + +## API + +| | | +| --- | --- | +| `wallet_money()`, `wallet_can(n)` | what is there, and whether `n` is | +| `wallet_earn(n)` | add | +| `wallet_spend(n) -> bool` | only what is there: false, and nothing spent, when it is not | +| `wallet_lose(n)` | a cost that happens whether or not it can be paid (a rescue); never below zero | +| `wallet_set(n)` | exactly `n` (a starting purse, a correction, the network's word) | +| `wallet_changes() -> Queue` | the facts: `{ before, after }`, one per change | +| `wallet_system() -> System` | `"wallet"`, `PH_COMMIT`: reset, and its save section (`wallet_save`, `wallet_load`) | + +## Tests + +```bash +ludic build packages/ludic.wallet/tests/wallet_test.ludic --headless -o /tmp/wallet_test && /tmp/wallet_test +``` diff --git a/packages/ludic.wallet/index.ludic b/packages/ludic.wallet/index.ludic new file mode 100644 index 00000000..e1cdf14a --- /dev/null +++ b/packages/ludic.wallet/index.ludic @@ -0,0 +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 +numbers float +import "ludic.base" +import "wallet.ludic" diff --git a/packages/ludic.wallet/package.ludic b/packages/ludic.wallet/package.ludic new file mode 100644 index 00000000..4c01c340 --- /dev/null +++ b/packages/ludic.wallet/package.ludic @@ -0,0 +1,5 @@ +# ludic.wallet - money carried: earned, spent, lost, and every change a fact. Uses ludic.base and +# nothing else. See README.md. +package "ludic.wallet" +version "0.1.0" +kind source diff --git a/packages/ludic.wallet/tests/wallet_test.ludic b/packages/ludic.wallet/tests/wallet_test.ludic new file mode 100644 index 00000000..abb880ad --- /dev/null +++ b/packages/ludic.wallet/tests/wallet_test.ludic @@ -0,0 +1,60 @@ +# wallet_test.ludic - earn, spend only what is there, lose down to zero, a fact per change, and +# its own save section +import "ludic.wallet" +import "ludic.base" +program WalletTest { + numbers float + + function changes() -> []MoneyChanged { return q_drain(wallet_changes()) } + function waiting() -> int { return q_len(wallet_changes()) } + + test "earning and spending move the purse, and each says so" { + wallet_reset() + wallet_earn(40) + expect(wallet_spend(15)) + expect_eq(wallet_money(), 25) + let cs = changes() + expect_eq(len(cs), 2) + expect_eq(cs[0].after, 40) + expect_eq(cs[1].before, 40) + expect_eq(cs[1].after, 25) + } + + test "a spend past what is there is refused and changes nothing" { + wallet_reset() + wallet_set(10) + changes() + expect(not wallet_spend(11)) + expect_eq(wallet_money(), 10) + expect_eq(waiting(), 0) + expect(wallet_can(10)) + expect(not wallet_can(11)) + } + + test "a loss happens anyway, but never below zero" { + wallet_reset() + wallet_set(7) + wallet_lose(20) + expect_eq(wallet_money(), 0) + } + + test "setting what is already there is no change" { + wallet_reset() + wallet_set(5) + changes() + wallet_set(5) + expect_eq(waiting(), 0) + } + + test "a save section reads back the same purse" { + wallet_reset() + wallet_set(123) + let root = save_tree() + save_section(root, "wallet", 1, wallet_save()) + let text = save_encode(root) + wallet_reset() + let n = load_section(save_decode(text), "wallet") + wallet_load(n.data, n.version) + expect_eq(wallet_money(), 123) + } +} diff --git a/packages/ludic.wallet/wallet.ludic b/packages/ludic.wallet/wallet.ludic new file mode 100644 index 00000000..9a8162f2 --- /dev/null +++ b/packages/ludic.wallet/wallet.ludic @@ -0,0 +1,62 @@ +# wallet.ludic - the purse, its verbs, its facts and its save section +export property MoneyChanged { + before: int = 0 + after: int = 0 +} + +var wl_money: int = 0 +var wl_changes: Queue = null + +export function wallet_changes() -> Queue { + if wl_changes == null { wl_changes = queue_new("wallet.changes") } + return wl_changes +} + +export function wallet_money() -> int { return wl_money } +export function wallet_can(n: int) -> bool { return wl_money >= n } + +# the purse holds exactly n (a starting purse, a correction, the network's word) +export function wallet_set(n: int) -> void { + if n == wl_money { return } + let c = new MoneyChanged + c.before = wl_money + c.after = n + wl_money = n + q_push(wallet_changes(), c) +} + +export function wallet_earn(n: int) -> void { wallet_set(wl_money + n) } + +# only what is there: false, and nothing spent, when it is not +export function wallet_spend(n: int) -> bool { + if n > wl_money { return false } + wallet_set(wl_money - n) + return true +} + +# a cost that happens whether or not it can be paid (a rescue, a fine); never below zero +export function wallet_lose(n: int) -> void { wallet_set(Math.max(wl_money - n, 0)) } + +export function wallet_reset() -> void { + wl_money = 0 + q_clear(wallet_changes()) +} + +export function wallet_save() -> Val { + let v = Value.object() + sv_put_int(v, "money", wl_money) + return v +} + +export function wallet_load(v: Val, version: int) -> void { + wl_money = sv_int(v, "money", 0) + q_clear(wallet_changes()) +} + +export function wallet_system() -> System { + let s = system_new("wallet", PH_COMMIT) + s.reset = fn wallet_reset + s.save = fn wallet_save + s.load = fn wallet_load + return s +}