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)))