From 2a372bff2f7d303ceb42565154ff1d0a8552b913 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Sun, 12 Jul 2026 18:26:22 -0500 Subject: [PATCH] =?UTF-8?q?feat(currency):=20the=20Currency=20value=20type?= =?UTF-8?q?=20=E2=80=94=20copper,=20ratios,=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One integer, stored in copper. The ratios are rules and live in code; prices are content and land in M8. format() suppresses empty denominations so being broke reads as '47c', not '0g 0s 47c'. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8 --- client/scripts/state/currency.gd | 44 ++++++++++++++++++++++++++ client/scripts/state/currency.gd.uid | 1 + client/tests/unit/test_currency.gd | 42 ++++++++++++++++++++++++ client/tests/unit/test_currency.gd.uid | 1 + 4 files changed, 88 insertions(+) create mode 100644 client/scripts/state/currency.gd create mode 100644 client/scripts/state/currency.gd.uid create mode 100644 client/tests/unit/test_currency.gd create mode 100644 client/tests/unit/test_currency.gd.uid diff --git a/client/scripts/state/currency.gd b/client/scripts/state/currency.gd new file mode 100644 index 0000000..4d19d0b --- /dev/null +++ b/client/scripts/state/currency.gd @@ -0,0 +1,44 @@ +class_name Currency +extends RefCounted +## Money. Everything is stored in COPPER — one integer, on GameState.purse_copper. +## The denominations exist so a bounded §6 move can name a unit (give_item(gold) +## is +10,000c); they are NOT stored buckets, so change-making never arises. +## +## The ratios are rules and live here in code. PRICES are content and live in +## /content (roadmap: "the economy is two systems"). Formatting happens only at +## the display edge — this is the one place that knows what "12g 40s 8c" means. + +const COPPER := 1 +const SILVER := 100 +const GOLD := 10000 + +const VALUES := {"copper": COPPER, "silver": SILVER, "gold": GOLD} + + +static func is_currency(item_id: String) -> bool: + return VALUES.has(item_id) + + +static func value(item_id: String) -> int: + return int(VALUES.get(item_id, 0)) + + +static func format(copper: int) -> String: + # Empty denominations are suppressed and at least one unit always shows: + # "0c", "47c", "1g 8c", "12g 40s". "0g 0s 8c" reads as a spreadsheet, not a + # purse — and being broke should read as poverty (§3). + if copper <= 0: + return "0c" + var parts: Array[String] = [] + var left := copper + var gold := left / GOLD # int division + if gold > 0: + parts.append("%dg" % gold) + left -= gold * GOLD + var silver := left / SILVER + if silver > 0: + parts.append("%ds" % silver) + left -= silver * SILVER + if left > 0: + parts.append("%dc" % left) + return " ".join(parts) diff --git a/client/scripts/state/currency.gd.uid b/client/scripts/state/currency.gd.uid new file mode 100644 index 0000000..2f792a9 --- /dev/null +++ b/client/scripts/state/currency.gd.uid @@ -0,0 +1 @@ +uid://cwnflbwacp5jf diff --git a/client/tests/unit/test_currency.gd b/client/tests/unit/test_currency.gd new file mode 100644 index 0000000..7a7c56c --- /dev/null +++ b/client/tests/unit/test_currency.gd @@ -0,0 +1,42 @@ +extends "res://addons/gut/test.gd" + + +func test_format_zero_is_copper(): + assert_eq(Currency.format(0), "0c") + + +func test_format_sub_silver(): + assert_eq(Currency.format(8), "8c") + assert_eq(Currency.format(47), "47c") + + +func test_format_exact_denominations(): + assert_eq(Currency.format(100), "1s") + assert_eq(Currency.format(10000), "1g") + + +func test_format_full_ladder(): + assert_eq(Currency.format(143), "1s 43c") + assert_eq(Currency.format(347), "3s 47c") + assert_eq(Currency.format(124008), "12g 40s 8c") + + +func test_format_skips_empty_middle_and_tail(): + assert_eq(Currency.format(10008), "1g 8c", "an empty silver place is skipped, not zero-padded") + assert_eq(Currency.format(124000), "12g 40s", "an empty copper place is skipped") + + +func test_value_of_each_denomination(): + assert_eq(Currency.value("copper"), 1) + assert_eq(Currency.value("silver"), 100) + assert_eq(Currency.value("gold"), 10000) + + +func test_non_currency_id_is_not_money(): + assert_false(Currency.is_currency("worn_shortsword")) + assert_eq(Currency.value("worn_shortsword"), 0) + + +func test_currency_ids_are_currency(): + for id in ["copper", "silver", "gold"]: + assert_true(Currency.is_currency(id), "%s is money" % id) diff --git a/client/tests/unit/test_currency.gd.uid b/client/tests/unit/test_currency.gd.uid new file mode 100644 index 0000000..11f2f13 --- /dev/null +++ b/client/tests/unit/test_currency.gd.uid @@ -0,0 +1 @@ +uid://dwpdm680q83ws