extends GutTest func test_canonical_primitive_round_trip_rebuilds_catalog_indexes() -> void: var state := _fixture_state() var saved := state.to_dictionary() var restored := RegionalWorldState.from_dictionary(saved) assert_not_null(restored) assert_true(_is_primitive_only(saved)) assert_eq(restored.to_dictionary(), saved) assert_eq(restored.checksum(), state.checksum()) assert_true(restored.has_global_id(&"group_blue_caravan")) assert_eq(restored.get_entity_type(&"group_blue_caravan"), &"mobile_group") assert_eq(restored.get_all_persons().size(), 3) assert_eq(saved["locations"][0]["location_id"], "location_jajce") assert_eq(saved["locations"][1]["location_id"], "location_travnik") assert_eq(saved["mobile_groups"][0]["cargo_ledger"].keys(), ["food", "wood"]) assert_eq(_person_ids(restored.get_persons_in_group(&"group_blue_caravan")), [&"person_ana"]) assert_eq( _cohort_ids(restored.get_cohorts_in_group(&"group_blue_caravan")), [&"cohort_carriers"] ) assert_eq( _route_ids(restored.get_routes_for_location(&"location_jajce")), [&"route_jajce_travnik"] ) assert_eq( _group_ids(restored.get_mobile_groups_on_route(&"route_jajce_travnik")), [&"group_blue_caravan"] ) assert_same( restored.get_diplomatic_relation_between(&"polity_travnik", &"polity_jajce"), restored.get_diplomatic_relation(&"relation_jajce_travnik") ) func test_equal_local_coordinates_remain_scoped_to_their_locations() -> void: var state := _fixture_state() var jajce_people := state.get_persons_at_location(&"location_jajce") var travnik_people := state.get_persons_at_location(&"location_travnik") assert_eq(_person_ids(jajce_people), [&"person_boris"]) assert_eq(_person_ids(travnik_people), [&"person_cemal"]) assert_eq(jajce_people[0].get_location_address().get_position(), Vector3(1.0, 0.0, 1.0)) assert_eq(travnik_people[0].get_location_address().get_position(), Vector3(1.0, 0.0, 1.0)) assert_ne( jajce_people[0].get_location_address().index_key(), travnik_people[0].get_location_address().index_key() ) var before := state.to_dictionary() var wrong_world := before.duplicate(true) for person_data: Dictionary in wrong_world["persons"]: if person_data["person_id"] == "person_boris": person_data["location_address"]["world_id"] = "another_world" assert_false(state.restore_from_dictionary(wrong_world)) assert_true(_errors_contain(state.get_last_validation_errors(), "cross-world")) assert_eq(state.to_dictionary(), before) func test_duplicate_and_orphan_memberships_are_rejected_transactionally() -> void: var state := _fixture_state() var before := state.to_dictionary() var duplicate := before.duplicate(true) duplicate["persons"].append(duplicate["persons"][0].duplicate(true)) assert_false(state.restore_from_dictionary(duplicate)) assert_true(_errors_contain(state.get_last_validation_errors(), "duplicate global ID")) assert_eq(state.to_dictionary(), before) var orphan := before.duplicate(true) for person_data: Dictionary in orphan["persons"]: if person_data["person_id"] == "person_ana": person_data["mobile_group_id"] = "missing_caravan" assert_false(state.restore_from_dictionary(orphan)) assert_true(_errors_contain(state.get_last_validation_errors(), "missing_caravan")) assert_eq(state.to_dictionary(), before) var duplicate_owner := before.duplicate(true) var second_group := MobileGroupStateRecord.create_on_route( &"group_second_caravan", &"caravan", "Second caravan", &"polity_jajce", &"route_jajce_travnik", &"location_jajce", &"location_travnik", 20, 32, [&"person_ana"] ) duplicate_owner["mobile_groups"].append(second_group.to_dictionary()) assert_false(state.restore_from_dictionary(duplicate_owner)) assert_true(_errors_contain(state.get_last_validation_errors(), "multiple mobile groups")) assert_eq(state.to_dictionary(), before) func test_cargo_and_cohort_payloads_are_conservation_friendly() -> void: assert_null( MobileGroupStateRecord.create_at_location( &"overloaded", &"caravan", "Overloaded", &"polity_jajce", _location_address(&"location_jajce"), [], {}, {&"food": 3.0}, 2.0 ) ) assert_null( SettlementStateRecord.create( &"bad_settlement", &"location_jajce", "Bad", &"polity_jajce", 0, &"founders", 0, {&"food": -1.0} ) ) var state := _fixture_state() var caravan := state.get_mobile_group(&"group_blue_caravan") var settlement := state.get_settlement(&"settlement_jajce") assert_eq(caravan.get_cohort_counts(), {"cohort_carriers": 3}) assert_eq(caravan.get_cargo_ledger(), {"food": 5.0, "wood": 2.0}) assert_eq(caravan.get_last_cargo_event_id(), 41) var food_before := settlement.get_stockpile_amount(&"food") + caravan.get_cargo_amount(&"food") var changed_settlement := settlement.with_stockpile_ledger({&"food": 22.0, &"wood": 12.0}, 57) var changed_cargo := caravan.with_cargo_ledger({&"food": 8.0, &"wood": 2.0}, 57) assert_not_null(changed_settlement) assert_not_null(changed_cargo) assert_eq(changed_cargo.get_last_cargo_event_id(), 57) assert_eq(changed_cargo.get_cargo_ledger(), {"food": 8.0, "wood": 2.0}) assert_true(57 in changed_cargo.get_history_event_ids()) assert_true(57 in changed_settlement.get_history_event_ids()) assert_eq( changed_settlement.get_stockpile_amount(&"food") + changed_cargo.get_cargo_amount(&"food"), food_before ) assert_eq(caravan.get_cargo_ledger(), {"food": 5.0, "wood": 2.0}) var mismatch := state.to_dictionary() mismatch["mobile_groups"][0]["cohort_counts"]["cohort_carriers"] = 4 assert_false(state.restore_from_dictionary(mismatch)) assert_true(_errors_contain(state.get_last_validation_errors(), "count disagrees")) func test_route_membership_requires_exact_endpoints_duration_and_no_address() -> void: var state := _fixture_state() var before := state.to_dictionary() var bad_duration := before.duplicate(true) bad_duration["mobile_groups"][0]["arrival_tick"] = 33 assert_false(state.restore_from_dictionary(bad_duration)) assert_true(_errors_contain(state.get_last_validation_errors(), "duration")) assert_eq(state.to_dictionary(), before) var bad_direction := before.duplicate(true) bad_direction["routes"][0]["bidirectional"] = false bad_direction["mobile_groups"][0]["departure_location_id"] = "location_travnik" bad_direction["mobile_groups"][0]["arrival_location_id"] = "location_jajce" assert_false(state.restore_from_dictionary(bad_direction)) assert_true(_errors_contain(state.get_last_validation_errors(), "direction/endpoints")) assert_eq(state.to_dictionary(), before) func _fixture_state() -> RegionalWorldState: var jajce := LocationStateRecord.create( &"location_jajce", &"regional_bosnia", &"settlement", "Jajce", _location_address(&"location_jajce"), [8, 2] ) var travnik := LocationStateRecord.create( &"location_travnik", &"regional_bosnia", &"settlement", "Travnik", _location_address(&"location_travnik") ) var settlement_jajce := SettlementStateRecord.create( &"settlement_jajce", &"location_jajce", "Jajce", &"polity_jajce", 2, &"founders_jajce", 0, {&"wood": 12.0, &"food": 25.0}, [12, 3] ) var settlement_travnik := SettlementStateRecord.create( &"settlement_travnik", &"location_travnik", "Travnik", &"polity_travnik", 1, &"founders_travnik", 1, {&"food": 18.0} ) var route := RouteStateRecord.create( &"route_jajce_travnik", &"location_jajce", &"location_travnik", 12, true, [19] ) var caravan := MobileGroupStateRecord.create_on_route( &"group_blue_caravan", &"caravan", "Blue caravan", &"polity_jajce", &"route_jajce_travnik", &"location_jajce", &"location_travnik", 20, 32, [&"person_ana"], {&"cohort_carriers": 3}, {&"wood": 2.0, &"food": 5.0}, 10.0, 41, [41, 18] ) var ana := PersonStateRecord.create_in_group( &"person_ana", "Ana", &"merchant", &"polity_jajce", &"location_jajce", &"group_blue_caravan" ) var boris := PersonStateRecord.create_at_location( &"person_boris", "Boris", &"steward", &"polity_jajce", &"location_jajce", SpatialAddress.create(&"regional_bosnia", &"location_jajce", Vector3(1.0, 0.0, 1.0)) ) var cemal := PersonStateRecord.create_at_location( &"person_cemal", "Cemal", &"steward", &"polity_travnik", &"location_travnik", SpatialAddress.create(&"regional_bosnia", &"location_travnik", Vector3(1.0, 0.0, 1.0)) ) var carriers := PopulationCohortRecord.create_in_group( &"cohort_carriers", &"adult_carriers", &"polity_jajce", 3, &"group_blue_caravan" ) var residents := PopulationCohortRecord.create_at_location( &"cohort_travnik_residents", &"residents", &"polity_travnik", 20, &"location_travnik" ) var jajce_polity := PolityStateRecord.create( &"polity_jajce", "Jajce council", &"location_jajce", [&"settlement_jajce"] ) var travnik_polity := PolityStateRecord.create( &"polity_travnik", "Travnik council", &"location_travnik", [&"settlement_travnik"] ) var relation := DiplomaticRelationRecord.create( &"relation_jajce_travnik", &"polity_travnik", &"polity_jajce", &"trade", 0.4, 5, [20] ) var locations: Array[LocationStateRecord] = [travnik, jajce] var settlements: Array[SettlementStateRecord] = [settlement_travnik, settlement_jajce] var routes: Array[RouteStateRecord] = [route] var mobile_groups: Array[MobileGroupStateRecord] = [caravan] var persons: Array[PersonStateRecord] = [cemal, ana, boris] var cohorts: Array[PopulationCohortRecord] = [residents, carriers] var polities: Array[PolityStateRecord] = [travnik_polity, jajce_polity] var relations: Array[DiplomaticRelationRecord] = [relation] return RegionalWorldState.create( &"regional_bosnia", locations, settlements, routes, mobile_groups, persons, cohorts, polities, relations ) func _location_address(location_id: StringName) -> SpatialAddress: return SpatialAddress.create(&"regional_bosnia", location_id, Vector3.ZERO) func _is_primitive_only(value: Variant) -> bool: match typeof(value): TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING: return true TYPE_ARRAY: for item: Variant in value: if not _is_primitive_only(item): return false return true TYPE_DICTIONARY: for key: Variant in value: if not key is String or not _is_primitive_only(value[key]): return false return true return false func _errors_contain(errors: Array[String], fragment: String) -> bool: for error: String in errors: if fragment in error: return true return false func _person_ids(records: Array[PersonStateRecord]) -> Array[StringName]: var ids: Array[StringName] = [] for record: PersonStateRecord in records: ids.append(record.get_person_id()) return ids func _cohort_ids(records: Array[PopulationCohortRecord]) -> Array[StringName]: var ids: Array[StringName] = [] for record: PopulationCohortRecord in records: ids.append(record.get_cohort_id()) return ids func _route_ids(records: Array[RouteStateRecord]) -> Array[StringName]: var ids: Array[StringName] = [] for record: RouteStateRecord in records: ids.append(record.get_route_id()) return ids func _group_ids(records: Array[MobileGroupStateRecord]) -> Array[StringName]: var ids: Array[StringName] = [] for record: MobileGroupStateRecord in records: ids.append(record.get_group_id()) return ids