refactor(contract)!: class_id -> calling_id, and the log gains race_id
class_id is the rulebook's word; the world has callings — that was the whole §17
reconcile. The contract the Narrator reads should say what the world says. There
are no saves (M9) and no deployed clients, so this is as cheap as it will ever be
and strictly more expensive every milestone after.
race_id reaches the log because the AI must be able to describe the player:
api/app/prompts.py rendered 'a sellsword' and now renders 'a beastfolk cutpurse'.
It humanizes the id (the model must never read 'hedge_mage') and picks the right
article ('an elf', not 'a elf').
LogPlayer's hardcoded CLASSES const is deleted — the roster is Callings.IDS. A
parity test reads the JSON Schema from the client and asserts its enum equals the
code, so the two sides of the HTTP boundary cannot drift.
The deserter allowed three callings that no longer exist. He now allows all seven;
a thematic gate is a content decision for the Greywater authoring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
This commit is contained in:
@@ -26,20 +26,20 @@ func _build(creation: Dictionary) -> Dictionary:
|
||||
|
||||
|
||||
func test_construct_succeeds():
|
||||
var res := _build({"name": "Aldric", "class_id": "sellsword"})
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
|
||||
|
||||
func test_numeric_luck_absent_from_log():
|
||||
var res := _build({"name": "Aldric", "class_id": "sellsword"})
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
var pd: Dictionary = res["log"].player.to_dict()
|
||||
assert_false("luck" in pd)
|
||||
assert_eq(pd.keys().size(), 3)
|
||||
assert_eq(pd.keys().size(), 4)
|
||||
assert_true(res["state"].luck >= Luck.MIN and res["state"].luck <= Luck.MAX)
|
||||
|
||||
|
||||
func test_inventory_and_dispo_land_in_state_not_log():
|
||||
var res := _build({"name": "Aldric", "class_id": "sellsword"})
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
assert_eq(res["state"].inventory.get("worn_shortsword", 0), 1)
|
||||
assert_eq(res["state"].purse_copper, 47, "the deserter is down to his last coin")
|
||||
assert_false("copper" in res["state"].inventory, "money is never an inventory row")
|
||||
@@ -47,20 +47,20 @@ func test_inventory_and_dispo_land_in_state_not_log():
|
||||
|
||||
|
||||
func test_companion_dispositions_from_overrides():
|
||||
var res := _build({"name": "Aldric", "class_id": "sellsword"})
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
assert_eq(res["log"].party_member("brannoc_thane").disposition, 40)
|
||||
assert_eq(res["log"].party_member("cadwyn_vell").disposition, 15)
|
||||
|
||||
|
||||
func test_situation_and_facts_seeded():
|
||||
var res := _build({"name": "Aldric", "class_id": "sellsword"})
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
assert_eq(res["log"].recent_events.size(), 2)
|
||||
assert_true("the player deserted the Iron Kettle mercenary company" in res["log"].established_facts)
|
||||
assert_eq(res["log"].humiliations.size(), 0)
|
||||
|
||||
|
||||
func test_active_quest_resolved_from_world():
|
||||
var res := _build({"name": "Aldric", "class_id": "sellsword"})
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
assert_eq(res["log"].active_quests.size(), 1)
|
||||
assert_eq(res["log"].active_quests[0].id, "find_the_ledger")
|
||||
assert_eq(res["log"].active_quests[0].status, "active")
|
||||
@@ -69,12 +69,12 @@ func test_active_quest_resolved_from_world():
|
||||
func test_null_start_quest_yields_no_quest():
|
||||
var o := _deserter()
|
||||
o["start_quest_id"] = null
|
||||
var res := NewGame.construct(o, world, {"name": "X", "class_id": "sellsword"}, _rng())
|
||||
var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng())
|
||||
assert_eq(res["log"].active_quests.size(), 0)
|
||||
|
||||
|
||||
func test_class_not_allowed_is_rejected():
|
||||
var res := _build({"name": "X", "class_id": "berserker"})
|
||||
var res := _build({"name": "X", "race_id": "human", "calling_id": "berserker"})
|
||||
assert_false(res["ok"])
|
||||
assert_true(res["errors"].size() > 0)
|
||||
assert_null(res["log"])
|
||||
@@ -83,18 +83,18 @@ func test_class_not_allowed_is_rejected():
|
||||
func test_unresolved_origin_is_rejected():
|
||||
var o := _deserter()
|
||||
o["start_location_id"] = "nowhere"
|
||||
var res := NewGame.construct(o, world, {"name": "X", "class_id": "sellsword"}, _rng())
|
||||
var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng())
|
||||
assert_false(res["ok"])
|
||||
assert_true("unresolved ref: location:nowhere" in res["errors"])
|
||||
|
||||
|
||||
func test_empty_name_is_rejected():
|
||||
var missing_name := _build({"class_id": "sellsword"})
|
||||
var missing_name := _build({"race_id": "human", "calling_id": "sellsword"})
|
||||
assert_false(missing_name["ok"])
|
||||
assert_true(missing_name["errors"].size() > 0)
|
||||
assert_null(missing_name["log"])
|
||||
|
||||
var whitespace_name := _build({"name": " ", "class_id": "sellsword"})
|
||||
var whitespace_name := _build({"name": " ", "race_id": "human", "calling_id": "sellsword"})
|
||||
assert_false(whitespace_name["ok"])
|
||||
assert_true(whitespace_name["errors"].size() > 0)
|
||||
assert_null(whitespace_name["log"])
|
||||
@@ -104,7 +104,7 @@ func test_non_companion_override_goes_to_state_not_log():
|
||||
world.npcs["oda_fenn"] = {"id": "oda_fenn", "name": "Oda Fenn", "role": "npc"}
|
||||
var o := _deserter()
|
||||
o["disposition_overrides"] = {"brannoc_thane": 40, "oda_fenn": -25}
|
||||
var res := NewGame.construct(o, world, {"name": "X", "class_id": "sellsword"}, _rng())
|
||||
var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng())
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
assert_eq(res["state"].npc_dispositions["oda_fenn"], -25)
|
||||
assert_null(res["log"].party_member("oda_fenn"))
|
||||
|
||||
Reference in New Issue
Block a user