feat(client): CanonLog container + maintenance mutators (cap 5, dedup, stack)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
101
client/scripts/canon_log/canon_log.gd
Normal file
101
client/scripts/canon_log/canon_log.gd
Normal file
@@ -0,0 +1,101 @@
|
||||
class_name CanonLog
|
||||
extends RefCounted
|
||||
## The §11 canon log: compact structured state code maintains and injects into
|
||||
## every AI call. This container owns the container-level invariants; leaf rows
|
||||
## own theirs. AI prose never writes here directly — mutators are called by code.
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const MAX_RECENT_EVENTS := 5
|
||||
|
||||
var schema_version: int = SCHEMA_VERSION
|
||||
var player: LogPlayer
|
||||
var location: LogLocation
|
||||
var party: Array[PartyMember] = []
|
||||
var recent_events: Array[String] = []
|
||||
var established_facts: Array[String] = []
|
||||
var active_quests: Array[Quest] = []
|
||||
var humiliations: Array[Humiliation] = []
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
player = LogPlayer.new()
|
||||
location = LogLocation.new()
|
||||
|
||||
|
||||
# ── maintenance mutators (charter §11 maintenance table) ─────────────────────
|
||||
|
||||
func push_event(line: String) -> void:
|
||||
recent_events.append(line)
|
||||
while recent_events.size() > MAX_RECENT_EVENTS:
|
||||
recent_events.pop_front()
|
||||
|
||||
|
||||
func add_fact(text: String) -> void:
|
||||
if text not in established_facts:
|
||||
established_facts.append(text)
|
||||
|
||||
|
||||
func add_humiliation(id: String, text: String, weight: int, turn: int) -> void:
|
||||
humiliations.append(Humiliation.new(id, text, weight, turn))
|
||||
|
||||
|
||||
func set_location(id: String, name: String) -> void:
|
||||
location = LogLocation.new(id, name)
|
||||
|
||||
|
||||
func party_member(id: String) -> PartyMember:
|
||||
for m in party:
|
||||
if m.id == id:
|
||||
return m
|
||||
return null
|
||||
|
||||
|
||||
func adjust_disposition(id: String, delta: int) -> bool:
|
||||
var m := party_member(id)
|
||||
if m == null:
|
||||
return false
|
||||
m.set_disposition(m.disposition + delta)
|
||||
return true
|
||||
|
||||
|
||||
func set_quest_status(id: String, status: String) -> bool:
|
||||
for q in active_quests:
|
||||
if q.id == id:
|
||||
return q.set_status(status)
|
||||
return false
|
||||
|
||||
|
||||
# ── serialisation ────────────────────────────────────────────────────────────
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"schema_version": schema_version,
|
||||
"player": player.to_dict(),
|
||||
"location": location.to_dict(),
|
||||
"party": party.map(func(m): return m.to_dict()),
|
||||
"recent_events": recent_events.duplicate(),
|
||||
"established_facts": established_facts.duplicate(),
|
||||
"active_quests": active_quests.map(func(q): return q.to_dict()),
|
||||
"humiliations": humiliations.map(func(h): return h.to_dict()),
|
||||
}
|
||||
|
||||
|
||||
static func from_dict(d: Dictionary) -> CanonLog:
|
||||
var v := int(d.get("schema_version", -1))
|
||||
if v != SCHEMA_VERSION:
|
||||
push_error("unsupported canon_log schema_version: %s" % v)
|
||||
return null
|
||||
var log := CanonLog.new()
|
||||
log.player = LogPlayer.from_dict(d.get("player", {}))
|
||||
log.location = LogLocation.from_dict(d.get("location", {}))
|
||||
for m in d.get("party", []):
|
||||
log.party.append(PartyMember.from_dict(m))
|
||||
for e in d.get("recent_events", []):
|
||||
log.push_event(e)
|
||||
for f in d.get("established_facts", []):
|
||||
log.add_fact(f)
|
||||
for q in d.get("active_quests", []):
|
||||
log.active_quests.append(Quest.from_dict(q))
|
||||
for h in d.get("humiliations", []):
|
||||
log.humiliations.append(Humiliation.from_dict(h))
|
||||
return log
|
||||
1
client/scripts/canon_log/canon_log.gd.uid
Normal file
1
client/scripts/canon_log/canon_log.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b1crmy8ma5ig4
|
||||
48
client/tests/unit/test_canon_log.gd
Normal file
48
client/tests/unit/test_canon_log.gd
Normal file
@@ -0,0 +1,48 @@
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const CanonLog = preload("res://scripts/canon_log/canon_log.gd")
|
||||
const PartyMember = preload("res://scripts/canon_log/party_member.gd")
|
||||
const Quest = preload("res://scripts/canon_log/quest.gd")
|
||||
|
||||
|
||||
func test_push_event_caps_at_five():
|
||||
var log = CanonLog.new()
|
||||
for i in 7:
|
||||
log.push_event("event %d" % i)
|
||||
assert_eq(log.recent_events.size(), 5)
|
||||
assert_eq(log.recent_events[0], "event 2")
|
||||
assert_eq(log.recent_events[4], "event 6")
|
||||
|
||||
|
||||
func test_add_fact_dedups():
|
||||
var log = CanonLog.new()
|
||||
log.add_fact("the bridge is out")
|
||||
log.add_fact("the bridge is out")
|
||||
assert_eq(log.established_facts.size(), 1)
|
||||
|
||||
|
||||
func test_humiliations_stack():
|
||||
var log = CanonLog.new()
|
||||
log.add_humiliation("h_1", "vomited on a shrine", 7, 3)
|
||||
log.add_humiliation("h_2", "vomited on a shrine", 7, 5)
|
||||
assert_eq(log.humiliations.size(), 2)
|
||||
|
||||
|
||||
func test_adjust_disposition_clamps_and_reports_missing():
|
||||
var log = CanonLog.new()
|
||||
log.party.append(PartyMember.new("brannoc_thane", "Brannoc Thane", 98))
|
||||
assert_true(log.adjust_disposition("brannoc_thane", 50))
|
||||
assert_eq(log.party_member("brannoc_thane").disposition, 100)
|
||||
assert_false(log.adjust_disposition("nobody", 5))
|
||||
|
||||
|
||||
func test_set_quest_status_rejects_bad_enum():
|
||||
var log = CanonLog.new()
|
||||
log.active_quests.append(Quest.new("find_the_ledger", "The Missing Ledger", "active", "Find it"))
|
||||
assert_false(log.set_quest_status("find_the_ledger", "abandoned"))
|
||||
assert_true(log.set_quest_status("find_the_ledger", "complete"))
|
||||
assert_false(log.set_quest_status("no_quest", "active"))
|
||||
|
||||
|
||||
func test_unsupported_schema_version_returns_null():
|
||||
assert_null(CanonLog.from_dict({"schema_version": 2}))
|
||||
1
client/tests/unit/test_canon_log.gd.uid
Normal file
1
client/tests/unit/test_canon_log.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cc3ax10wjoqx6
|
||||
Reference in New Issue
Block a user