extends GutTest const ORIGIN_FOOD := 20.0 const DESTINATION_FOOD := 4.0 const CARGO_FOOD := 6.0 const DEPARTURE_TICK := 10 const ARRIVAL_TICK := 15 func test_departure_and_arrival_conserve_cargo_headcount_and_record_exact_facts() -> void: var service := _fixture_service() var headcount_before := _regional_headcount(service.get_world_state()) var food_before := _regional_food(service.get_world_state()) assert_true(_depart(service, &"never")) assert_eq(service.get_scheduler().size(), 1) assert_true(service.get_world_state().get_mobile_group(&"group_caravan").is_on_route()) assert_eq( service.get_world_state().get_settlement(&"settlement_origin").get_stockpile_amount( &"food" ), ORIGIN_FOOD - CARGO_FOOD ) assert_eq( service.get_world_state().get_mobile_group(&"group_caravan").get_cargo_amount(&"food"), CARGO_FOOD ) assert_eq(_event_types(service.get_event_store()), _departure_event_types()) assert_eq(_regional_food(service.get_world_state()), food_before) assert_eq(_regional_headcount(service.get_world_state()), headcount_before) var executed := service.process_due(ARRIVAL_TICK, 1, &"always") assert_eq(executed.size(), 1) assert_eq(executed[0].get_due_tick(), ARRIVAL_TICK) assert_true(service.get_scheduler().is_empty()) var group := service.get_world_state().get_mobile_group(&"group_caravan") assert_true(group.is_at_location()) assert_eq(group.get_current_location_id(), &"location_destination") assert_eq(group.get_cargo_amount(&"food"), 0.0) assert_eq( service.get_world_state().get_settlement(&"settlement_destination").get_stockpile_amount( &"food" ), DESTINATION_FOOD + CARGO_FOOD ) assert_eq(_regional_food(service.get_world_state()), food_before) assert_eq(_regional_headcount(service.get_world_state()), headcount_before) assert_eq(_event_types(service.get_event_store()), _all_event_types()) _assert_causal_event_chain(service.get_event_store()) func test_budgets_one_eight_and_unlimited_finish_with_identical_state_and_events() -> void: var budget_one := _run_complete(1, &"never") var budget_eight := _run_complete(8, &"never") var unlimited := _run_complete(RegionalJobScheduler.UNLIMITED_BUDGET, &"never") assert_eq(budget_one, budget_eight) assert_eq(budget_one, unlimited) assert_eq(budget_one["event_types"], _all_event_types()) func test_save_load_mid_route_continues_exactly() -> void: var uninterrupted := _fixture_service() assert_true(_depart(uninterrupted, &"never")) var saved := uninterrupted.to_dictionary() var restored := RegionalCaravanService.from_dictionary(saved) assert_not_null(restored) assert_eq(restored.to_dictionary(), saved) assert_eq(restored.get_next_event_id(), 2) assert_eq(restored.get_registered_job_types(), [RegionalCaravanService.JOB_TYPE_ARRIVAL]) assert_eq(restored.process_due(ARRIVAL_TICK, 1).size(), 1) assert_eq(uninterrupted.process_due(ARRIVAL_TICK, 1).size(), 1) assert_eq(restored.to_dictionary(), uninterrupted.to_dictionary()) assert_eq(restored.checksum(), uninterrupted.checksum()) var stale_cursor := restored.to_dictionary() stale_cursor["next_event_id"] = 3 assert_null(RegionalCaravanService.from_dictionary(stale_cursor)) var skipped_cursor := restored.to_dictionary() skipped_cursor["next_event_id"] = 9 assert_null(RegionalCaravanService.from_dictionary(skipped_cursor)) var coerced_cursor := restored.to_dictionary() coerced_cursor["next_event_id"] = "4" assert_null(RegionalCaravanService.from_dictionary(coerced_cursor)) func test_presentation_loading_modes_are_inert_observers() -> void: var always := _run_complete(1, &"always") var never := _run_complete(1, &"never") var load_unload := _fixture_service() assert_true(_depart(load_unload, &"loaded")) assert_eq(load_unload.process_due(ARRIVAL_TICK, 1, &"unloaded").size(), 1) var load_unload_result := _result(load_unload) assert_eq(always, never) assert_eq(always, load_unload_result) func test_invalid_and_duplicate_jobs_roll_back_without_partial_authority() -> void: var service := _fixture_service() assert_not_null( service.get_scheduler().schedule_new( &"occupied_arrival", RegionalCaravanService.JOB_TYPE_ARRIVAL, 99, RegionalCaravanService.JOB_PHASE_ARRIVAL, &"group_caravan", {}, &"caravan_arrival:group_caravan" ) ) var before := service.to_dictionary() assert_false(_depart(service, &"never")) assert_eq(service.to_dictionary(), before) assert_true("conflicts" in service.get_last_error()) var invalid_service := _fixture_service() assert_true(_depart(invalid_service, &"never")) var scheduled := invalid_service.get_scheduler().get_all_sorted()[0] var bad_data := scheduled.to_dictionary() bad_data["payload"]["destination_settlement_id"] = "missing_settlement" assert_not_null(invalid_service.get_scheduler().cancel(scheduled.get_job_id())) assert_true( invalid_service.get_scheduler().schedule(ScheduledJobRecord.from_dictionary(bad_data)) ) var invalid_before := invalid_service.to_dictionary() assert_true(invalid_service.process_due(ARRIVAL_TICK, 1).is_empty()) assert_eq(invalid_service.to_dictionary(), invalid_before) assert_true("failed atomically" in invalid_service.get_last_error()) var forged_service := _fixture_service() assert_true(_depart(forged_service, &"never")) var forged_envelope := forged_service.to_dictionary() forged_envelope["event_store"]["events"][1]["event_type"] = "unrelated_fact" assert_null(RegionalCaravanService.from_dictionary(forged_envelope)) func test_restore_rejects_a_duplicate_but_unrelated_departure_pair() -> void: var service := _fixture_service() assert_true(_depart(service, &"never")) var envelope := service.to_dictionary() var duplicate_load: Dictionary = envelope["event_store"]["events"][0].duplicate(true) var duplicate_depart: Dictionary = envelope["event_store"]["events"][1].duplicate(true) duplicate_load["event_id"] = 2 duplicate_depart["event_id"] = 3 duplicate_depart["payload"]["cause_event_id"] = 2 envelope["event_store"]["events"].append(duplicate_load) envelope["event_store"]["events"].append(duplicate_depart) envelope["scheduler"]["jobs"][0]["payload"]["depart_event_id"] = 3 envelope["next_event_id"] = 4 assert_not_null(WorldEventStore.from_dictionary(envelope["event_store"])) assert_not_null(RegionalJobScheduler.from_dictionary(envelope["scheduler"])) assert_null(RegionalCaravanService.from_dictionary(envelope)) func _fixture_service() -> RegionalCaravanService: var origin_location := LocationStateRecord.create( &"location_origin", &"regional_world", &"settlement", "Origin", _address(&"location_origin") ) var destination_location := LocationStateRecord.create( &"location_destination", &"regional_world", &"settlement", "Destination", _address(&"location_destination") ) var origin := SettlementStateRecord.create( &"settlement_origin", &"location_origin", "Origin", &"polity_origin", 1, &"founders_origin", 0, {&"food": ORIGIN_FOOD} ) var destination := SettlementStateRecord.create( &"settlement_destination", &"location_destination", "Destination", &"polity_destination", 1, &"founders_destination", 0, {&"food": DESTINATION_FOOD} ) var route := RouteStateRecord.create( &"route_trade", &"location_origin", &"location_destination", 5 ) var group := MobileGroupStateRecord.create_at_location( &"group_caravan", &"caravan", "Caravan", &"polity_origin", _address(&"location_origin"), [&"person_merchant"], {&"cohort_guards": 2}, {}, 10.0 ) var person := PersonStateRecord.create_in_group( &"person_merchant", "Merchant", &"merchant", &"polity_origin", &"location_origin", &"group_caravan" ) var guards := PopulationCohortRecord.create_in_group( &"cohort_guards", &"guards", &"polity_origin", 2, &"group_caravan" ) var origin_polity := PolityStateRecord.create( &"polity_origin", "Origin polity", &"location_origin", [&"settlement_origin"] ) var destination_polity := PolityStateRecord.create( &"polity_destination", "Destination polity", &"location_destination", [&"settlement_destination"] ) var locations: Array[LocationStateRecord] = [destination_location, origin_location] var settlements: Array[SettlementStateRecord] = [destination, origin] var routes: Array[RouteStateRecord] = [route] var groups: Array[MobileGroupStateRecord] = [group] var persons: Array[PersonStateRecord] = [person] var cohorts: Array[PopulationCohortRecord] = [guards] var polities: Array[PolityStateRecord] = [destination_polity, origin_polity] var relations: Array[DiplomaticRelationRecord] = [] var world_state := RegionalWorldState.create( &"regional_world", locations, settlements, routes, groups, persons, cohorts, polities, relations ) return RegionalCaravanService.create( world_state, RegionalJobScheduler.new(), WorldEventStore.new() ) func _depart(service: RegionalCaravanService, presentation_mode: StringName) -> bool: return service.depart( &"group_caravan", &"settlement_origin", &"settlement_destination", &"route_trade", {&"food": CARGO_FOOD}, DEPARTURE_TICK, presentation_mode ) func _run_complete(budget: int, presentation_mode: StringName) -> Dictionary: var service := _fixture_service() assert_true(_depart(service, presentation_mode)) assert_eq(service.process_due(ARRIVAL_TICK, budget, presentation_mode).size(), 1) return _result(service) func _result(service: RegionalCaravanService) -> Dictionary: return { "world_state": service.get_world_state().to_dictionary(), "scheduler": service.get_scheduler().to_dictionary(), "events": service.get_event_store().to_dictionary(), "next_event_id": service.get_next_event_id(), "event_types": _event_types(service.get_event_store()), } func _address(location_id: StringName) -> SpatialAddress: return SpatialAddress.create(&"regional_world", location_id, Vector3.ZERO) func _regional_food(state: RegionalWorldState) -> float: var total := 0.0 for settlement: SettlementStateRecord in state.get_all_settlements(): total += settlement.get_stockpile_amount(&"food") for group: MobileGroupStateRecord in state.get_all_mobile_groups(): total += group.get_cargo_amount(&"food") 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 _event_types(store: WorldEventStore) -> Array[StringName]: var types: Array[StringName] = [] for event: WorldEventRecord in store.get_all(): types.append(event.get_event_type()) return types func _departure_event_types() -> Array[StringName]: return [ RegionalCaravanService.EVENT_CARGO_LOADED, RegionalCaravanService.EVENT_DEPARTED, ] func _all_event_types() -> Array[StringName]: return [ RegionalCaravanService.EVENT_CARGO_LOADED, RegionalCaravanService.EVENT_DEPARTED, RegionalCaravanService.EVENT_ARRIVED, RegionalCaravanService.EVENT_CARGO_DEPOSITED, ] func _assert_causal_event_chain(store: WorldEventStore) -> void: var events := store.get_all() assert_eq(events[0].get_payload()["cause_event_id"], -1) assert_eq(events[1].get_payload()["cause_event_id"], events[0].get_event_id()) assert_eq(events[2].get_payload()["cause_event_id"], events[1].get_event_id()) assert_eq(events[3].get_payload()["cause_event_id"], events[2].get_event_id()) for event: WorldEventRecord in events: assert_eq(event.get_payload()["cargo_ledger"], {"food": CARGO_FOOD}) assert_eq(event.get_payload()["headcount"], 3) assert_not_null(event.get_participant(RegionalCaravanService.ROLE_CARAVAN)) assert_not_null(event.get_participant(RegionalCaravanService.ROLE_ROUTE)) assert_not_null(event.get_participant(RegionalCaravanService.ROLE_ORIGIN)) assert_not_null(event.get_participant(RegionalCaravanService.ROLE_DESTINATION)) var caravan_ref := WorldEntityRef.create(&"mobile_group", &"group_caravan") assert_eq(store.get_for_participant(caravan_ref).size(), 4) assert_eq(store.get_for_location(_address(&"location_origin")).size(), 2) assert_eq(store.get_for_location(_address(&"location_destination")).size(), 2)