505 lines
26 KiB
GDScript
505 lines
26 KiB
GDScript
extends SceneTree
|
|
|
|
|
|
class DenyingOperationsCoordinator:
|
|
extends OperationsCoordinator
|
|
|
|
func can_start_construction_or_investment() -> Dictionary:
|
|
return {
|
|
&"allowed": false,
|
|
&"reason": &"ARREARS_REMAIN",
|
|
&"arrears_total": 25,
|
|
&"consecutive_insolvency_count": 1,
|
|
}
|
|
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _runtime: Variant
|
|
var _direct_command_id := 900_000
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("MVP: Runtime content, hiring, and progression integration")
|
|
_test_catalog_bootstrap_and_queries()
|
|
_test_progression_resource_and_guest_facts()
|
|
_test_catalog_staff_hire_transaction()
|
|
_test_rank_two_three_and_atomic_failure()
|
|
_test_v3_migration_and_v4_tamper_guards()
|
|
_test_twenty_four_hour_replay()
|
|
_cleanup()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _reset_runtime(seed := 82_001) -> void:
|
|
_cleanup()
|
|
_clock = load("res://autoload/game_clock.gd").new()
|
|
_state = load("res://autoload/game_state.gd").new()
|
|
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
|
|
_clock.set_paused(true)
|
|
_expect(bool(report.get(&"valid", false)), "Runtime initializes with validated content and Progression")
|
|
|
|
|
|
func _cleanup() -> void:
|
|
if _runtime != null:
|
|
_runtime.free()
|
|
if _clock != null:
|
|
_clock.free()
|
|
if _state != null:
|
|
_state.free()
|
|
_runtime = null
|
|
_clock = null
|
|
_state = null
|
|
|
|
|
|
func _test_catalog_bootstrap_and_queries() -> void:
|
|
_reset_runtime(82_001)
|
|
var report: Dictionary = _runtime.get_mvp_content_report()
|
|
_expect(bool(report[&"valid"]), "MVP catalog report is valid")
|
|
_expect(int(report[&"counts"]["guest_classes"]) == 8, "catalog exposes eight Guest classes")
|
|
_expect(int(report[&"counts"]["species"]) == 4, "catalog exposes four independent species")
|
|
_expect(int(report[&"counts"]["staff_candidates"]) == 20, "catalog exposes twenty fixed Staff candidates")
|
|
_expect(int(report[&"counts"]["unique_characters"]) == 2, "catalog exposes both unique characters")
|
|
var catalog_ids: Array[StringName] = _runtime.get_mvp_guest_definition_ids()
|
|
_expect(catalog_ids.size() == 32, "Runtime registers every class/species Guest definition")
|
|
_expect(_runtime.guest_service.get_definitions().size() == 35, "legacy three Guest definitions coexist with the 32 catalog definitions")
|
|
var sorted_ids := catalog_ids.duplicate()
|
|
sorted_ids.sort_custom(func(left: StringName, right: StringName) -> bool: return String(left) < String(right))
|
|
_expect(catalog_ids == sorted_ids, "Runtime catalog Guest IDs use stable canonical order")
|
|
var all_registered := true
|
|
for guest_type_id: StringName in catalog_ids:
|
|
if _runtime.guest_service.get_definition(guest_type_id) == null:
|
|
all_registered = false
|
|
break
|
|
_expect(all_registered, "all 32 catalog Guest definitions are queryable by stable ID")
|
|
|
|
report[&"counts"]["guest_classes"] = 0
|
|
_expect(int(_runtime.get_mvp_content_report()[&"counts"]["guest_classes"]) == 8, "content report query is a defensive copy")
|
|
var candidate: Dictionary = _runtime.get_mvp_staff_candidate_payload(&"staff_akane")
|
|
(candidate[&"options"][&"skills"] as Dictionary)[&"service"] = -1
|
|
_expect(
|
|
int(_runtime.get_mvp_staff_candidate_payload(&"staff_akane")[&"options"][&"skills"][&"service"]) == 68,
|
|
"Staff candidate payload query is a defensive copy"
|
|
)
|
|
var progression_report: Dictionary = _runtime.get_progression_report()
|
|
(progression_report[&"snapshot"][&"metrics"] as Dictionary)["asset_gold"] = 0
|
|
_expect(int(_runtime.progression_service.metrics[&"asset_gold"]) == 5_000, "Progression report query is a defensive copy")
|
|
|
|
|
|
func _test_progression_resource_and_guest_facts() -> void:
|
|
_reset_runtime(82_002)
|
|
var faith: CommandResult = _runtime.submit_command(
|
|
&"change_resources", {&"deltas": {&"faith": 40}},
|
|
&"player", &"", 50, -1, &"mvp.progression.faith"
|
|
)
|
|
_expect(faith.is_success(), "positive Faith resource change succeeds")
|
|
_expect(_state.get_resource(&"faith") == 40, "positive Faith changes live GameState")
|
|
_expect(int(_runtime.progression_service.metrics[&"faith_lifetime"]) == 40, "positive Faith increments lifetime progression atomically")
|
|
var spawned: CommandResult = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"catalog_guest_one",
|
|
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
|
|
&"gold": 50,
|
|
&"options": {&"current_cell": Vector3i.ZERO},
|
|
},
|
|
&"world", &"catalog_guest_one", 40, -1, &"mvp.progression.guest_one"
|
|
)
|
|
_expect(spawned.is_success(), "catalog Guest can spawn through Runtime")
|
|
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 1, "successful Guest spawn increments lifetime Guest count once")
|
|
var duplicate: CommandResult = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"catalog_guest_one",
|
|
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
|
|
&"options": {&"current_cell": Vector3i.ZERO},
|
|
},
|
|
&"world", &"catalog_guest_one", 40, -1, &"mvp.progression.guest_duplicate"
|
|
)
|
|
_expect(not duplicate.is_success(), "duplicate Guest spawn is rejected")
|
|
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 1, "failed Guest spawn cannot increment lifetime progression")
|
|
|
|
_expect(
|
|
_runtime.progression_service.update_metrics({&"faith_lifetime": ProgressionService.MAX_SAFE_INTEGER}),
|
|
"overflow rollback fixture reaches the maximum lifetime Faith"
|
|
)
|
|
var state_before: Dictionary = _state.create_snapshot()
|
|
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
|
|
var progression_before: Dictionary = _runtime.progression_service.create_snapshot()
|
|
var overflow: CommandResult = _runtime._execute_change_resources(_direct_command(
|
|
&"change_resources", {&"deltas": {&"faith": 1}}
|
|
))
|
|
_expect(not overflow.is_success(), "lifetime Faith overflow rejects the resource command")
|
|
_expect(_state.create_snapshot() == state_before, "failed lifetime update restores GameState exactly")
|
|
_expect(_runtime.economy_service.create_snapshot() == economy_before, "failed lifetime update restores Economy exactly")
|
|
_expect(_runtime.progression_service.create_snapshot() == progression_before, "failed lifetime update restores Progression exactly")
|
|
|
|
|
|
func _test_catalog_staff_hire_transaction() -> void:
|
|
_reset_runtime(82_003)
|
|
var bypass: CommandResult = _runtime.submit_command(
|
|
&"register_staff",
|
|
{
|
|
&"staff_id": &"staff_akane",
|
|
&"role_id": &"guard",
|
|
&"options": {&"current_cell": Vector3i.ZERO},
|
|
},
|
|
&"player", &"staff_akane", 80, -1, &"mvp.hire.bypass"
|
|
)
|
|
_expect(not bypass.is_success(), "generic Staff registration cannot bypass a reserved catalog candidate")
|
|
var cash_before: int = _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT)
|
|
var hired: CommandResult = _runtime.submit_command(
|
|
&"hire_staff_candidate",
|
|
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
|
|
&"player", &"staff_akane", 80, -1, &"mvp.hire.akane"
|
|
)
|
|
_expect(hired.is_success(), "validated Staff candidate hires atomically")
|
|
var staff: StaffRuntime = _runtime.staff_service.get_staff(&"staff_akane")
|
|
var shift: ShiftInstance = _runtime.staff_service.get_shift(StringName(hired.details[&"shift_id"]))
|
|
_expect(staff != null and staff.role_id == &"receptionist", "hire uses the catalog role as the sole source of truth")
|
|
_expect(staff != null and staff.daily_wage == 120, "FREE_HIRE retains its catalog daily wage")
|
|
_expect(staff != null and staff.get_employment_kind_name() == &"FREE_HIRE", "ordinary catalog hire records explicit FREE_HIRE employment")
|
|
_expect(staff != null and int(staff.skills[&"service"]) == 68, "hire copies catalog skills without caller inference")
|
|
_expect(staff != null and staff.permitted_task_tags == [&"reception", &"service"], "hire copies canonical permitted Task tags")
|
|
_expect(shift != null and shift.template_id == &"DAY", "hire creates the candidate's preferred Shift")
|
|
_expect(_runtime.staff_service.get_daily_payroll_total() == 120, "hired candidate participates in deterministic daily payroll")
|
|
_expect(
|
|
_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == cash_before - 180,
|
|
"signing cost is paid exactly once through the Gold ledger"
|
|
)
|
|
_expect(_state.get_resource(&"gold") == cash_before - 180, "ledger signing cost synchronizes GameState Gold")
|
|
_expect(_runtime.path_service.has_actor_projection(&"staff:staff_akane"), "hired Staff is projected into Path traffic")
|
|
_expect(_runtime.get_hired_staff_candidate_ids() == [&"staff_akane"], "Runtime records the candidate as permanently hired")
|
|
var saw_hire_event := false
|
|
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
|
if event.event_type == &"STAFF_CANDIDATE_HIRED" and &"staff_akane" in event.entity_ids:
|
|
saw_hire_event = true
|
|
_expect(saw_hire_event, "successful hire publishes a structured DomainEvent")
|
|
|
|
var replay_cash: int = _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT)
|
|
var replay: CommandResult = _runtime.submit_command(
|
|
&"hire_staff_candidate",
|
|
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
|
|
&"player", &"staff_akane", 80, -1, &"mvp.hire.akane"
|
|
)
|
|
_expect(not replay.is_success() and replay.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, "resent hire command is recognized by its idempotency key")
|
|
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == replay_cash, "resent hire never charges the signing cost twice")
|
|
var duplicate: CommandResult = _runtime.submit_command(
|
|
&"hire_staff_candidate",
|
|
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
|
|
&"player", &"staff_akane", 80, -1, &"mvp.hire.akane.other"
|
|
)
|
|
_expect(not duplicate.is_success(), "same candidate cannot be hired under a different request")
|
|
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == replay_cash, "duplicate-candidate rejection spends no Gold")
|
|
var original_coordinator: Variant = _runtime.operations_coordinator
|
|
var original_operations_enabled: bool = bool(_runtime._operations_enabled)
|
|
var staff_before_gate: Dictionary = _runtime.staff_service.create_snapshot()
|
|
var economy_before_gate: Dictionary = _runtime.economy_service.create_snapshot()
|
|
var hired_before_gate: Array[StringName] = _runtime.get_hired_staff_candidate_ids()
|
|
_runtime.operations_coordinator = DenyingOperationsCoordinator.new()
|
|
_runtime._operations_enabled = true
|
|
var gated: CommandResult = _runtime._execute_hire_staff_candidate(_direct_command(
|
|
&"hire_staff_candidate",
|
|
{&"candidate_id": &"staff_tessa", &"current_cell": Vector3i.ZERO}
|
|
))
|
|
_runtime.operations_coordinator = original_coordinator
|
|
_runtime._operations_enabled = original_operations_enabled
|
|
_expect(
|
|
not gated.is_success()
|
|
and StringName(gated.details.get(&"reason", &"")) == &"ARREARS_REMAIN",
|
|
"Operations arrears gate rejects a new wage-bearing Staff contract"
|
|
)
|
|
_expect(
|
|
_runtime.staff_service.create_snapshot() == staff_before_gate
|
|
and _runtime.economy_service.create_snapshot() == economy_before_gate
|
|
and _runtime.get_hired_staff_candidate_ids() == hired_before_gate,
|
|
"rejected Operations-gated hire spends nothing and mutates no Staff history"
|
|
)
|
|
|
|
var staff_before: Dictionary = _runtime.staff_service.create_snapshot()
|
|
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
|
|
var game_state_before: Dictionary = _state.create_snapshot()
|
|
var path_before: Dictionary = _runtime.path_service.create_snapshot()
|
|
var credits_before: Dictionary = _runtime._movement_credits.duplicate(true)
|
|
var events_before: Dictionary = _runtime.domain_events.create_snapshot()
|
|
var ids_before: Dictionary = _runtime.id_generator.create_snapshot()
|
|
var hired_ids_before: Array[StringName] = _runtime.get_hired_staff_candidate_ids()
|
|
var callback := Callable(_runtime, "_on_staff_registered")
|
|
_runtime.staff_service.staff_registered.disconnect(callback)
|
|
var forced_failure: CommandResult = _runtime._execute_hire_staff_candidate(_direct_command(
|
|
&"hire_staff_candidate",
|
|
{&"candidate_id": &"staff_bardo", &"current_cell": Vector3i.ZERO}
|
|
))
|
|
_runtime.staff_service.staff_registered.connect(callback)
|
|
_expect(not forced_failure.is_success(), "missing Path projection forces a post-payment hire failure")
|
|
_expect(_runtime.staff_service.create_snapshot() == staff_before, "failed hire restores Staff and Shift state exactly")
|
|
_expect(_runtime.economy_service.create_snapshot() == economy_before, "failed hire restores the ledger exactly")
|
|
_expect(_state.create_snapshot() == game_state_before, "failed hire restores GameState exactly")
|
|
_expect(_runtime.path_service.create_snapshot() == path_before, "failed hire restores Path exactly")
|
|
_expect(_runtime._movement_credits == credits_before, "failed hire restores movement credits exactly")
|
|
_expect(_runtime.domain_events.create_snapshot() == events_before, "failed hire restores DomainEvents exactly")
|
|
_expect(_runtime.id_generator.create_snapshot() == ids_before, "failed hire restores stable ID counters exactly")
|
|
_expect(_runtime.get_hired_staff_candidate_ids() == hired_ids_before, "failed hire leaves candidate history unchanged")
|
|
var canonical_save: Dictionary = _runtime.create_snapshot()
|
|
var wage_tamper: Dictionary = canonical_save.duplicate(true)
|
|
for raw_staff: Dictionary in wage_tamper[&"systems"][&"runtime"][&"staff"][&"staff"]:
|
|
if String(raw_staff[&"staff_id"]) == "staff_akane":
|
|
raw_staff[&"daily_wage"] = 0
|
|
break
|
|
var live_before: Dictionary = _runtime.create_snapshot()
|
|
_expect(not _runtime.restore_snapshot(wage_tamper), "v4 restore rejects a hired candidate whose catalog wage was altered")
|
|
_expect(_runtime.create_snapshot() == live_before, "catalog Staff crosslink rejection is atomic")
|
|
|
|
|
|
func _test_rank_two_three_and_atomic_failure() -> void:
|
|
_reset_runtime(82_004)
|
|
var rank_before: Dictionary = _runtime.progression_service.create_snapshot()
|
|
var world_before: Dictionary = _runtime.world_service.create_snapshot()
|
|
var unmet: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
|
|
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two_unmet"}
|
|
))
|
|
_expect(not unmet.is_success(), "Rank 2 rejects unmet reputation and asset requirements")
|
|
_expect(_runtime.progression_service.create_snapshot() == rank_before, "failed eligibility check leaves Progression exact")
|
|
_expect(_runtime.world_service.create_snapshot() == world_before, "failed eligibility check leaves World exact")
|
|
|
|
_expect(_runtime._execute_update_progression_metrics(_direct_command(
|
|
&"update_progression_metrics", {&"metrics": {&"reputation": 25}}
|
|
)).is_success(), "Rank 2 reputation fact updates")
|
|
_expect(_runtime._execute_change_resources(_direct_command(
|
|
&"change_resources", {&"deltas": {&"gold": 5_000}}
|
|
)).is_success(), "Rank 2 asset threshold is funded")
|
|
var promoted_two: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
|
|
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two"}
|
|
))
|
|
_expect(promoted_two.is_success(), "eligible Rank 2 promotion succeeds")
|
|
_expect(_runtime.progression_service.current_rank == 2 and _state.world_tree_rank == 2, "Progression and GameState commit Rank 2 together")
|
|
_expect(_runtime.world_service.get_bounds() == ProgressionService.RANK_BOUNDS[2], "Rank 2 commits canonical World bounds")
|
|
var replay_before: Dictionary = _runtime.create_snapshot()
|
|
var replay: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
|
|
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two"}
|
|
))
|
|
_expect(replay.is_success() and replay.is_replay, "same rank request ID replays the original success")
|
|
_expect(_runtime.create_snapshot() == replay_before, "rank request replay is completely read-only")
|
|
var skip: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
|
|
&"set_world_tree_rank", {&"rank": 4, &"request_id": &"rank_four_skip"}
|
|
))
|
|
_expect(not skip.is_success() and _runtime.progression_service.current_rank == 2, "rank progression cannot skip Rank 3")
|
|
var rank_three_guests_ok := true
|
|
var rank_guest_definition: StringName = _runtime.get_mvp_guest_definition_ids()[0]
|
|
for index in range(300):
|
|
var guest_id := StringName("rank_three_guest_%03d" % index)
|
|
var spawned: CommandResult = _runtime._execute_spawn_guest(_direct_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": guest_id,
|
|
&"guest_type_id": rank_guest_definition,
|
|
&"gold": 0,
|
|
&"options": {
|
|
&"current_cell": Vector3i.ZERO,
|
|
&"planned_exit_at": 10_000,
|
|
},
|
|
}
|
|
))
|
|
rank_three_guests_ok = rank_three_guests_ok and spawned.is_success()
|
|
var lifetime_updated: CommandResult = _runtime._execute_update_progression_metrics(_direct_command(
|
|
&"update_progression_metrics",
|
|
{&"metrics": {&"faith_lifetime": 100}}
|
|
))
|
|
_expect(
|
|
rank_three_guests_ok and lifetime_updated.is_success() \
|
|
and int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 300,
|
|
"Rank 3 lifetime facts derive from 300 authoritative Guest records"
|
|
)
|
|
var promoted_three: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
|
|
&"set_world_tree_rank", {&"rank": 3, &"request_id": &"rank_three"}
|
|
))
|
|
_expect(promoted_three.is_success(), "eligible Rank 3 promotion succeeds")
|
|
_expect(_runtime.progression_service.current_rank == 3 and _state.world_tree_rank == 3, "Progression and GameState commit Rank 3 together")
|
|
var saw_rank_event := false
|
|
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
|
if event.event_type == &"WORLD_TREE_RANK_ADVANCED" and int(event.payload.get(&"rank", 0)) == 3:
|
|
saw_rank_event = true
|
|
_expect(saw_rank_event, "successful promotion publishes WORLD_TREE_RANK_ADVANCED")
|
|
|
|
_reset_runtime(82_005)
|
|
_runtime.progression_service.update_metrics({&"reputation": 25})
|
|
_runtime._execute_change_resources(_direct_command(&"change_resources", {&"deltas": {&"gold": 5_000}}))
|
|
_expect(_runtime._expand_world_to_rank(3), "rank apply-failure fixture expands World and Path coherently")
|
|
var atomic_before := {
|
|
&"progression": _runtime.progression_service.create_snapshot(),
|
|
&"world": _runtime.world_service.create_snapshot(),
|
|
&"path": _runtime.path_service.create_snapshot(),
|
|
&"state": _state.create_snapshot(),
|
|
&"credits": _runtime._movement_credits.duplicate(true),
|
|
&"events": _runtime.domain_events.create_snapshot(),
|
|
&"ids": _runtime.id_generator.create_snapshot(),
|
|
}
|
|
var apply_failure: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
|
|
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two_apply_failure"}
|
|
))
|
|
_expect(not apply_failure.is_success(), "World/Path apply failure rejects promotion")
|
|
_expect(_runtime.progression_service.create_snapshot() == atomic_before[&"progression"], "failed rank apply restores Progression exactly")
|
|
_expect(_runtime.world_service.create_snapshot() == atomic_before[&"world"], "failed rank apply restores World exactly")
|
|
_expect(_runtime.path_service.create_snapshot() == atomic_before[&"path"], "failed rank apply restores Path exactly")
|
|
_expect(_state.create_snapshot() == atomic_before[&"state"], "failed rank apply restores GameState exactly")
|
|
_expect(_runtime._movement_credits == atomic_before[&"credits"], "failed rank apply restores movement credits exactly")
|
|
_expect(_runtime.domain_events.create_snapshot() == atomic_before[&"events"], "failed rank apply restores DomainEvents exactly")
|
|
_expect(_runtime.id_generator.create_snapshot() == atomic_before[&"ids"], "failed rank apply restores stable IDs exactly")
|
|
|
|
|
|
func _test_v3_migration_and_v4_tamper_guards() -> void:
|
|
_reset_runtime(82_006)
|
|
_runtime._execute_change_resources(_direct_command(&"change_resources", {&"deltas": {&"faith": 12}}))
|
|
for index in range(2):
|
|
var guest_id := StringName("legacy_guest_%d" % index)
|
|
var spawned: CommandResult = _runtime._execute_spawn_guest(_direct_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": guest_id,
|
|
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[index],
|
|
&"gold": 0,
|
|
&"options": {&"current_cell": Vector3i.ZERO},
|
|
}
|
|
))
|
|
_expect(spawned.is_success(), "legacy migration fixture Guest %d spawns" % index)
|
|
_runtime.guest_service.despawn_guest(&"legacy_guest_1", _clock.absolute_minute)
|
|
var v3: Dictionary = _runtime.create_snapshot()
|
|
var v3_runtime: Dictionary = v3[&"systems"][&"runtime"]
|
|
v3_runtime[&"runtime_schema_version"] = 3
|
|
v3_runtime.erase(&"kitchen_production")
|
|
v3_runtime.erase(&"elevators")
|
|
v3_runtime[&"path"][&"schema_version"] = 3
|
|
v3_runtime[&"path"].erase(&"vertical_walk_blockers")
|
|
v3_runtime.erase(&"operations")
|
|
v3_runtime.erase(&"catalog_manifest")
|
|
v3_runtime.erase(&"progression")
|
|
v3_runtime.erase(&"hired_staff_candidate_ids")
|
|
for infrastructure_key: StringName in [
|
|
&"excavation", &"spatial_subslots", &"route_projection",
|
|
&"facility_infrastructure",
|
|
]:
|
|
v3_runtime.erase(infrastructure_key)
|
|
_expect(_runtime.restore_snapshot(v3), "Runtime v3 Rank 1 snapshot migrates without guessed promotion evidence")
|
|
_expect(_runtime.progression_service.current_rank == 1, "v3 migration creates fresh Rank 1 Progression")
|
|
_expect(int(_runtime.progression_service.metrics[&"asset_gold"]) == _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT), "v3 migration derives asset Gold from the authoritative ledger")
|
|
_expect(int(_runtime.progression_service.metrics[&"faith_lifetime"]) == 12, "v3 migration derives lifetime Faith from current Faith")
|
|
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 2, "v3 migration derives lifetime Guests from active plus persistent records")
|
|
_expect(_runtime.get_mvp_guest_definition_ids().size() == 32, "v3 migration registers the current 32 catalog definitions")
|
|
var migrated: Dictionary = _runtime.create_snapshot()
|
|
_expect(int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 8, "v3 migration emits canonical Runtime schema v8")
|
|
var digest := SimulationDigest.compute(migrated)
|
|
_expect(_runtime.restore_snapshot(migrated), "canonical v8 snapshot restores")
|
|
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "canonical v8 restore is exact")
|
|
|
|
var progressed_legacy: Dictionary = v3.duplicate(true)
|
|
progressed_legacy[&"game_state"][&"world_tree_rank"] = 2
|
|
var live_before: Dictionary = _runtime.create_snapshot()
|
|
_expect(not _runtime.restore_snapshot(progressed_legacy), "v3 Rank 2 snapshot is rejected because promotion evidence is absent")
|
|
_expect(_runtime.create_snapshot() == live_before, "rejected progressed v3 migration is atomic")
|
|
|
|
var corruptions: Array[Dictionary] = []
|
|
var manifest: Dictionary = migrated.duplicate(true)
|
|
manifest[&"systems"][&"runtime"][&"catalog_manifest"][&"guest_definition_ids"][0] = "tampered_guest"
|
|
corruptions.append({&"label": "catalog manifest", &"snapshot": manifest})
|
|
var definition: Dictionary = migrated.duplicate(true)
|
|
var catalog_id := String(_runtime.get_mvp_guest_definition_ids()[0])
|
|
for raw_definition: Dictionary in definition[&"systems"][&"runtime"][&"guests"][&"definitions"]:
|
|
if String(raw_definition[&"guest_type_id"]) == catalog_id:
|
|
raw_definition[&"wealth_min"] = int(raw_definition[&"wealth_min"]) + 1
|
|
break
|
|
corruptions.append({&"label": "catalog Guest definition", &"snapshot": definition})
|
|
var progression: Dictionary = migrated.duplicate(true)
|
|
progression[&"systems"][&"runtime"][&"progression"][&"current_rank"] = 2
|
|
corruptions.append({&"label": "Progression history", &"snapshot": progression})
|
|
var rank: Dictionary = migrated.duplicate(true)
|
|
rank[&"game_state"][&"world_tree_rank"] = 2
|
|
corruptions.append({&"label": "GameState rank", &"snapshot": rank})
|
|
var bounds: Dictionary = migrated.duplicate(true)
|
|
bounds[&"systems"][&"runtime"][&"world"][&"bounds"][&"max_x"] = 13
|
|
corruptions.append({&"label": "World rank bounds", &"snapshot": bounds})
|
|
var hired_manifest: Dictionary = migrated.duplicate(true)
|
|
hired_manifest[&"systems"][&"runtime"][&"hired_staff_candidate_ids"] = ["not_a_candidate"]
|
|
corruptions.append({&"label": "hired candidate manifest", &"snapshot": hired_manifest})
|
|
for corruption: Dictionary in corruptions:
|
|
live_before = _runtime.create_snapshot()
|
|
_expect(
|
|
not _runtime.restore_snapshot(corruption[&"snapshot"])
|
|
and _runtime.create_snapshot() == live_before,
|
|
"%s tamper is rejected atomically" % corruption[&"label"]
|
|
)
|
|
|
|
|
|
func _test_twenty_four_hour_replay() -> void:
|
|
_reset_runtime(82_007)
|
|
var spawn: CommandResult = _runtime._execute_spawn_guest(_direct_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"replay_guest",
|
|
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
|
|
&"gold": 100,
|
|
&"options": {&"current_cell": Vector3i.ZERO, &"planned_exit_at": _clock.absolute_minute + 2_000},
|
|
}
|
|
))
|
|
var hire: CommandResult = _runtime._execute_hire_staff_candidate(_direct_command(
|
|
&"hire_staff_candidate", {&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO}
|
|
))
|
|
_expect(spawn.is_success() and hire.is_success(), "24-hour replay fixture creates catalog Guest and Staff")
|
|
var start: Dictionary = _runtime.create_snapshot()
|
|
var second_clock = load("res://autoload/game_clock.gd").new()
|
|
var second_state = load("res://autoload/game_state.gd").new()
|
|
var second_runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
second_clock.reset(1, 6, 0)
|
|
second_state.reset()
|
|
second_runtime.initialize(second_clock, second_state, 82_007)
|
|
second_clock.set_paused(true)
|
|
_expect(second_runtime.restore_snapshot(start), "24-hour replay clone restores exact v4 start")
|
|
_clock.set_paused(false)
|
|
second_clock.set_paused(false)
|
|
_clock.advance_minutes(1_440)
|
|
second_clock.advance_minutes(1_440)
|
|
_expect(
|
|
SimulationDigest.compute(_runtime.create_snapshot()) \
|
|
== SimulationDigest.compute(second_runtime.create_snapshot()),
|
|
"catalog, Staff hire, Progression, and six-Need state replay exactly for 24 hours"
|
|
)
|
|
second_runtime.free()
|
|
second_clock.free()
|
|
second_state.free()
|
|
|
|
|
|
func _direct_command(command_type: StringName, payload: Dictionary) -> SimulationCommand:
|
|
_direct_command_id += 1
|
|
return SimulationCommand.new(
|
|
_direct_command_id,
|
|
_clock.absolute_minute,
|
|
_clock.absolute_minute,
|
|
100,
|
|
command_type,
|
|
&"test",
|
|
&"",
|
|
payload,
|
|
&""
|
|
)
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|