feat: define regional world state
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
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
|
||||
Reference in New Issue
Block a user