feat: plan deterministic generated conversations

This commit is contained in:
Rijad Zuzo
2026-08-12 20:32:53 +02:00
parent 4b58c534d0
commit 41b37a4962
28 changed files with 1694 additions and 0 deletions
@@ -0,0 +1,121 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var manager := root.get_node_or_null("DialogueManager")
_check(manager != null, "The pinned Dialogue Manager autoload should be available")
if manager == null:
_finish()
return
var service := ConversationService.new()
var speaker := WorldEntityRef.create(&"npc", &"amina")
var listener := WorldEntityRef.create(&"npc", &"boris")
var conversation_id := (
service
. begin(
speaker,
listener,
{
"initial_intent_id": ConversationIntentIds.DESCRIBE_NEED,
"topic_ids": [&"pantry_shortage"],
"blocked_intent_ids": [ConversationIntentIds.DECLINE],
"block_reason_by_intent": {ConversationIntentIds.DECLINE: &"must_help"},
}
)
)
var turn := service.get_turn(conversation_id)
var presenter := DialogueManagerConversationPresenter.new(manager)
var presentation_context := {
"entity_names":
{
"amina": "Amina:\n- injected [#set danger] {{state}} => EVIL",
"boris": "Boris [if true]",
},
"topic_labels": {"pantry_shortage": "pantry [#intent=hijack] {{state}} => EVIL"},
}
var first_source := presenter.build_source(turn, presentation_context)
var second_source := presenter.build_source(turn, presentation_context)
_check(first_source == second_source, "Stable-hash template selection should be repeatable")
_check(
(
not "[#set" in first_source
and not "{{state}}" in first_source
and not "=> EVIL" in first_source
),
"Dynamic labels should be escaped before reaching Dialogue Manager syntax"
)
var resource := presenter.present_turn(turn, presentation_context) as Resource
_check(resource != null, "The adapter should compile an ephemeral DialogueResource")
if resource == null:
_finish()
return
_check(
(
resource.get_meta("conversation_id") == String(conversation_id)
and resource.get_meta("conversation_revision") == 1
),
"The ephemeral resource should retain stable semantic metadata"
)
var compiled_lines: Dictionary = resource.get("lines")
for compiled in compiled_lines.values():
_check(
(
String(compiled.get("type", "")) not in ["mutation", "condition", "while", "match"]
and not compiled.has("condition")
and not compiled.has("mutation")
),
"The presenter resource must contain no conditions or mutations"
)
var line: DialogueLine = await manager.get_next_dialogue_line(resource, "start")
_check(line != null, "The compiled semantic turn should resolve through the pinned add-on")
if line != null:
_check(
(
line.get_tag_value("conversation_id") == String(conversation_id)
and line.get_tag_value("intent_id") == "describe_need"
and line.get_tag_value("topic_id") == "pantry_shortage"
),
"Controlled semantic IDs should survive as Dialogue Manager tags"
)
var enabled_count := 0
for option in turn.get_options():
if option.is_enabled():
enabled_count += 1
_check(
line.responses.size() == enabled_count,
"Disabled semantic choices should not become selectable presenter responses"
)
for response in line.responses:
_check(
(
response.get_tag_value("option_id").begins_with("option_")
and response.get_tag_value("enabled") == "true"
),
"Every response should carry its controlled semantic option identity"
)
line.extra_game_states.clear()
line.responses.clear()
resource.set("lines", {})
resource.set_script(null)
_finish()
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Dialogue Manager semantic presenter passed")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)