Files
gamedev-the-steward/tests/unit/test_regional_simulation_facade.gd
T
2026-08-23 17:20:41 +02:00

288 lines
12 KiB
GDScript

extends GutTest
const CARGO_AMOUNT := 3.0
func test_jajce_route_bootstrap_has_stable_required_ids_and_independent_stock() -> void:
var facade := RegionalSimulationFacade.create_jajce_route(4242, 7)
assert_not_null(facade)
assert_eq(facade.get_world_seed(), 4242)
assert_eq(facade.get_last_processed_tick(), 7)
assert_eq(facade.get_active_location_id(), SimulationIds.REGIONAL_LOCATION_JAJCE)
assert_eq(
facade.to_dictionary().keys(),
[
"schema_version",
"world_seed",
"last_processed_tick",
"active_location_id",
"caravan_service",
]
)
var service := facade.get_service()
var state := service.get_world_state()
assert_eq(state.get_world_id(), SimulationIds.REGIONAL_WORLD_BOSNIA)
assert_not_null(state.get_location(RegionalSimulationFacade.LOCATION_JAJCE))
assert_not_null(state.get_location(RegionalSimulationFacade.LOCATION_TRAVNIK))
assert_not_null(state.get_settlement(RegionalSimulationFacade.SETTLEMENT_JAJCE))
assert_not_null(state.get_settlement(RegionalSimulationFacade.SETTLEMENT_TRAVNIK))
assert_not_null(state.get_route(RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK))
assert_not_null(state.get_person(RegionalSimulationFacade.PERSON_ANA))
assert_not_null(state.get_population_cohort(RegionalSimulationFacade.COHORT_CARRIERS))
assert_not_null(state.get_polity(RegionalSimulationFacade.POLITY_JAJCE))
assert_not_null(state.get_polity(RegionalSimulationFacade.POLITY_TRAVNIK))
assert_not_null(state.get_diplomatic_relation(RegionalSimulationFacade.RELATION_JAJCE_TRAVNIK))
var route := state.get_route(RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK)
assert_true(
route.connects(
RegionalSimulationFacade.LOCATION_JAJCE, RegionalSimulationFacade.LOCATION_TRAVNIK
)
)
assert_eq(route.get_travel_ticks(), RegionalSimulationFacade.ROUTE_TRAVEL_TICKS)
var caravan := state.get_mobile_group(RegionalSimulationFacade.CARAVAN_BLUE)
assert_true(caravan.is_at_location())
assert_eq(caravan.get_current_location_id(), RegionalSimulationFacade.LOCATION_TRAVNIK)
assert_eq(caravan.get_headcount(), 4)
assert_eq(caravan.get_cargo_ledger(), {})
assert_eq(
state.get_settlement(RegionalSimulationFacade.SETTLEMENT_JAJCE).get_stockpile_ledger(), {}
)
assert_eq(
state.get_settlement(RegionalSimulationFacade.SETTLEMENT_TRAVNIK).get_stockpile_amount(
SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS
),
RegionalSimulationFacade.TRAVNIK_INITIAL_TRADE_GOODS
)
assert_eq(
service.get_scheduler().get_cursor()["last_drain_tick"], facade.get_last_processed_tick()
)
_assert_regional_stock_is_independent(state)
func test_departure_and_arrival_use_facade_tick_and_conserve_regional_authority() -> void:
var facade := RegionalSimulationFacade.create_jajce_route(4242, 10)
var state := facade.get_service().get_world_state()
var goods_before := _regional_trade_goods(state)
var headcount_before := _regional_headcount(state)
assert_true(_depart(facade, CARGO_AMOUNT, &"loaded"))
var caravan := state.get_mobile_group(RegionalSimulationFacade.CARAVAN_BLUE)
assert_true(caravan.is_on_route())
assert_eq(caravan.get_departure_tick(), 10)
assert_eq(caravan.get_arrival_tick(), 10 + RegionalSimulationFacade.ROUTE_TRAVEL_TICKS)
assert_eq(caravan.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS), CARGO_AMOUNT)
assert_eq(_regional_trade_goods(state), goods_before)
assert_eq(_regional_headcount(state), headcount_before)
assert_true(facade.advance_to_tick(caravan.get_arrival_tick() - 1, &"unloaded"))
assert_true(caravan.is_on_route())
assert_true(facade.advance_to_tick(caravan.get_arrival_tick(), &"always"))
state = facade.get_service().get_world_state()
caravan = state.get_mobile_group(RegionalSimulationFacade.CARAVAN_BLUE)
assert_true(caravan.is_at_location())
assert_eq(caravan.get_current_location_id(), RegionalSimulationFacade.LOCATION_JAJCE)
assert_eq(caravan.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS), 0.0)
assert_eq(
state.get_settlement(RegionalSimulationFacade.SETTLEMENT_JAJCE).get_stockpile_amount(
SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS
),
RegionalSimulationFacade.JAJCE_INITIAL_TRADE_GOODS + CARGO_AMOUNT
)
assert_eq(_regional_trade_goods(state), goods_before)
assert_eq(_regional_headcount(state), headcount_before)
assert_eq(facade.get_service().get_event_store().size(), 4)
_assert_regional_stock_is_independent(state)
func test_ticks_are_monotonic_and_equal_tick_is_idempotent() -> void:
var facade := RegionalSimulationFacade.create_jajce_route()
assert_true(facade.advance_to_tick(4))
var checksum_at_four := facade.checksum()
assert_true(facade.advance_to_tick(4))
assert_eq(facade.checksum(), checksum_at_four)
assert_false(facade.advance_to_tick(3))
assert_true("cannot move backward" in facade.get_last_error())
assert_eq(facade.get_last_processed_tick(), 4)
assert_eq(facade.checksum(), checksum_at_four)
func test_advance_rolls_back_all_ticks_when_a_scheduled_handler_fails() -> void:
var facade := RegionalSimulationFacade.create_jajce_route()
assert_not_null(
facade.get_service().get_scheduler().schedule_new(
&"unsupported_job", &"unsupported_job", 2, 0, RegionalSimulationFacade.CARAVAN_BLUE
)
)
var before := facade.to_dictionary()
assert_false(facade.advance_to_tick(2))
assert_true("no registered handler" in facade.get_last_error())
assert_eq(facade.to_dictionary(), before)
assert_eq(facade.get_last_processed_tick(), 0)
func test_save_restore_mid_route_continues_exactly() -> void:
var uninterrupted := RegionalSimulationFacade.create_jajce_route(4242, 3)
assert_true(_depart(uninterrupted, 4.0))
assert_true(uninterrupted.advance_to_tick(8))
var saved := uninterrupted.to_dictionary()
var restored := RegionalSimulationFacade.from_dictionary(saved)
assert_not_null(restored)
assert_eq(restored.to_dictionary(), saved)
assert_eq(restored.checksum(), uninterrupted.checksum())
assert_eq(restored.get_last_processed_tick(), 8)
var arrival_tick := 3 + RegionalSimulationFacade.ROUTE_TRAVEL_TICKS
assert_true(uninterrupted.advance_to_tick(arrival_tick, &"always"))
assert_true(restored.advance_to_tick(arrival_tick, &"never"))
assert_eq(restored.to_dictionary(), uninterrupted.to_dictionary())
assert_eq(restored.checksum(), uninterrupted.checksum())
func test_presentation_modes_are_inert_observers() -> void:
var always := _complete_route(&"always", &"always")
var never := _complete_route(&"never", &"never")
var loaded_unloaded := _complete_route(&"loaded", &"unloaded")
assert_eq(always, never)
assert_eq(always, loaded_unloaded)
func test_departure_rejects_local_or_mixed_resource_ledgers_without_mutation() -> void:
var facade := RegionalSimulationFacade.create_jajce_route()
var before := facade.to_dictionary()
assert_false(
facade.depart_caravan(
RegionalSimulationFacade.CARAVAN_BLUE,
RegionalSimulationFacade.SETTLEMENT_JAJCE,
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK,
{SimulationIds.RESOURCE_FOOD: 1.0}
)
)
assert_true("only the trade-goods" in facade.get_last_error())
assert_eq(facade.to_dictionary(), before)
assert_false(
(
facade
. depart_caravan(
RegionalSimulationFacade.CARAVAN_BLUE,
RegionalSimulationFacade.SETTLEMENT_JAJCE,
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK,
{
SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS: 1.0,
SimulationIds.RESOURCE_WOOD: 1.0,
}
)
)
)
assert_eq(facade.to_dictionary(), before)
func test_restore_rejects_malformed_or_wrong_scope_envelopes() -> void:
var facade := RegionalSimulationFacade.create_jajce_route(4242, 5)
var saved := facade.to_dictionary()
assert_null(RegionalSimulationFacade.from_dictionary({}))
var missing_field := saved.duplicate(true)
missing_field.erase("caravan_service")
assert_null(RegionalSimulationFacade.from_dictionary(missing_field))
var extra_field := saved.duplicate(true)
extra_field["unexpected"] = true
assert_null(RegionalSimulationFacade.from_dictionary(extra_field))
var wrong_schema := saved.duplicate(true)
wrong_schema["schema_version"] = RegionalSimulationFacade.SCHEMA_VERSION + 1
assert_null(RegionalSimulationFacade.from_dictionary(wrong_schema))
var coerced_schema := saved.duplicate(true)
coerced_schema["schema_version"] = str(RegionalSimulationFacade.SCHEMA_VERSION)
assert_null(RegionalSimulationFacade.from_dictionary(coerced_schema))
var coerced_seed := saved.duplicate(true)
coerced_seed["world_seed"] = "4242"
assert_null(RegionalSimulationFacade.from_dictionary(coerced_seed))
var negative_tick := saved.duplicate(true)
negative_tick["last_processed_tick"] = -1
assert_null(RegionalSimulationFacade.from_dictionary(negative_tick))
var coerced_tick := saved.duplicate(true)
coerced_tick["last_processed_tick"] = "5"
assert_null(RegionalSimulationFacade.from_dictionary(coerced_tick))
var cursor_mismatch := saved.duplicate(true)
cursor_mismatch["last_processed_tick"] = 6
assert_null(RegionalSimulationFacade.from_dictionary(cursor_mismatch))
var wrong_active_location := saved.duplicate(true)
wrong_active_location["active_location_id"] = String(RegionalSimulationFacade.LOCATION_TRAVNIK)
assert_null(RegionalSimulationFacade.from_dictionary(wrong_active_location))
var malformed_service := saved.duplicate(true)
malformed_service["caravan_service"] = {}
assert_null(RegionalSimulationFacade.from_dictionary(malformed_service))
var wrong_world := saved.duplicate(true)
wrong_world["caravan_service"]["world_state"]["world_id"] = "another_world"
for location: Dictionary in wrong_world["caravan_service"]["world_state"]["locations"]:
location["world_id"] = "another_world"
location["address"]["world_id"] = "another_world"
for group: Dictionary in wrong_world["caravan_service"]["world_state"]["mobile_groups"]:
if group["location_address"] != null:
group["location_address"]["world_id"] = "another_world"
assert_null(RegionalSimulationFacade.from_dictionary(wrong_world))
var unsupported_stock := saved.duplicate(true)
for settlement: Dictionary in unsupported_stock["caravan_service"]["world_state"]["settlements"]:
if settlement["settlement_id"] == String(RegionalSimulationFacade.SETTLEMENT_TRAVNIK):
settlement["stockpile_ledger"][String(SimulationIds.RESOURCE_FOOD)] = 1.0
assert_null(RegionalSimulationFacade.from_dictionary(unsupported_stock))
var forged_total := saved.duplicate(true)
for settlement: Dictionary in forged_total["caravan_service"]["world_state"]["settlements"]:
if settlement["settlement_id"] == String(RegionalSimulationFacade.SETTLEMENT_TRAVNIK):
settlement["stockpile_ledger"][String(RegionalSimulationFacade.TRADE_GOODS)] = 5.0
assert_null(RegionalSimulationFacade.from_dictionary(forged_total))
func _depart(
facade: RegionalSimulationFacade, amount: float, presentation_mode: StringName = &"never"
) -> bool:
return facade.depart_jajce_delivery(amount, presentation_mode)
func _complete_route(
departure_presentation: StringName, arrival_presentation: StringName
) -> Dictionary:
var facade := RegionalSimulationFacade.create_jajce_route()
assert_true(_depart(facade, CARGO_AMOUNT, departure_presentation))
assert_true(
facade.advance_to_tick(RegionalSimulationFacade.ROUTE_TRAVEL_TICKS, arrival_presentation)
)
return facade.to_dictionary()
func _regional_trade_goods(state: RegionalWorldState) -> float:
var total := 0.0
for settlement: SettlementStateRecord in state.get_all_settlements():
total += settlement.get_stockpile_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS)
for group: MobileGroupStateRecord in state.get_all_mobile_groups():
total += group.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS)
return total
func _regional_headcount(state: RegionalWorldState) -> int:
var total := 0
for group: MobileGroupStateRecord in state.get_all_mobile_groups():
total += group.get_headcount()
for cohort: PopulationCohortRecord in state.get_all_population_cohorts():
if cohort.is_at_location():
total += cohort.get_count()
for person: PersonStateRecord in state.get_all_persons():
if person.is_at_location():
total += 1
return total
func _assert_regional_stock_is_independent(state: RegionalWorldState) -> void:
for settlement: SettlementStateRecord in state.get_all_settlements():
assert_false(settlement.get_stockpile_ledger().has(String(SimulationIds.RESOURCE_FOOD)))
assert_false(settlement.get_stockpile_ledger().has(String(SimulationIds.RESOURCE_WOOD)))
for group: MobileGroupStateRecord in state.get_all_mobile_groups():
assert_false(group.get_cargo_ledger().has(String(SimulationIds.RESOURCE_FOOD)))
assert_false(group.get_cargo_ledger().has(String(SimulationIds.RESOURCE_WOOD)))