Files
gamedev-the-steward/tests/animal_food_delivery_test.gd
2026-07-31 00:19:41 +02:00

865 lines
30 KiB
GDScript

extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
await _test_single_two_leg_delivery()
await _test_one_unit_contention()
await _test_legacy_pre_pickup_restore_reroutes()
await _test_legacy_working_restore_reroutes()
await _test_post_pickup_restore_continuation()
_finish()
func _test_single_two_leg_delivery() -> void:
var fixture := await _load_world()
var main_scene: Node = fixture["main"]
var manager: Node = fixture["manager"]
var view: Node = fixture["view"]
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
var pantry: StorageStateRecord = manager.get_pantry()
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
_check(
(
pantry_node != null
and zora_node != null
and dunja_node != null
and pantry != null
and zora_state != null
and dunja_state != null
),
"The delivery proof needs the authored pantry and both bound named goats",
)
if (
pantry_node == null
or zora_node == null
or dunja_node == null
or pantry == null
or zora_state == null
or dunja_state == null
):
await _unload_world(main_scene)
return
_prepare_population(manager, view)
_set_pantry_food(manager, 3.0)
zora_state.set_hunger(72.0)
dunja_state.set_hunger(30.0)
var caretaker: SimNPC = manager.npcs[0]
var origin := zora_node.get_interaction_position()
_prepare_feed_task(manager, view, caretaker, origin)
_check(
manager.resolve_npc_target(caretaker.id, origin),
"A caretaker should resolve a physical route for hungry Zora",
)
_check(
(
caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
and zora_state.get_reserved_by() == caretaker.id
and caretaker.has_travel_target
and caretaker.travel_target_position.is_equal_approx(
pantry_node.get_interaction_position()
)
),
"The first feed leg should retain exact Zora authority while targeting the pantry",
)
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
var observed_villages := []
var observed_food := []
var observed_states := []
manager.village_changed.connect(
func(changed_village):
observed_villages.append(changed_village)
observed_food.append(changed_village.food)
observed_states.append(manager.serialize_state())
)
_arrive_at_current_target(manager, view, caretaker)
var withdrawal_events := _events_for(
manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker.id
)
var observed_record: SimulationStateRecord
var observed_npc: SimNPC
if observed_states.size() == 1:
observed_record = SimulationStateRecord.from_json(String(observed_states[0]))
if observed_record != null:
var observed_npc_record := _find_npc_record(observed_record.npcs, caretaker.id)
if observed_npc_record != null:
observed_npc = observed_npc_record.restore(false)
_check(
(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
and zora_state.get_reserved_by() == caretaker.id
and caretaker.has_travel_target
and caretaker.travel_target_position.is_equal_approx(
zora_node.get_interaction_position()
)
and observed_villages.size() == 1
and observed_villages[0] == manager.village
and is_equal_approx(float(observed_food[0]), pantry_before - 1.0)
and observed_record != null
and observed_npc != null
and is_equal_approx(observed_npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and observed_npc.target_id == SimulationIds.ANIMAL_ZORA
and observed_npc.travel_target_position.is_equal_approx(
zora_node.get_interaction_position()
)
),
(
"Pantry arrival should atomically expose synchronized food, carried inventory, "
+ "and the same claimed Zora delivery waypoint"
),
)
_check(
(
withdrawal_events.size() == 1
and _is_exact_withdrawal(
withdrawal_events[0],
caretaker.id,
pantry_node.get_interaction_position(),
)
),
"The pickup leg should record one exact pantry-to-caretaker transfer at the pantry",
)
var carried_food := _get_carried_food(view, caretaker.id)
_check(
carried_food != null and carried_food.visible,
"Authoritative caretaker inventory should reveal the existing carried-food prop",
)
_check(
is_equal_approx(
(
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
+ caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
),
pantry_before,
),
"Food should be conserved across the pantry pickup boundary",
)
var zora_hunger_before := zora_state.get_hunger()
var dunja_hunger_before := dunja_state.get_hunger()
_arrive_at_current_target(manager, view, caretaker)
_check(
caretaker.task_state == SimNPC.TASK_STATE_WORKING,
"Arrival at exact Zora should begin the feed work without another withdrawal",
)
var work_ticks := int(ceil(caretaker.task_duration))
for _tick in range(work_ticks):
manager.simulate_tick()
var feed_events := _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker.id)
_check(
(
is_equal_approx(
zora_state.get_hunger(),
(
zora_hunger_before
+ AnimalStateRecord.HUNGER_PER_TICK * work_ticks
- AnimalStateRecord.HUNGER_RELIEF
),
)
and is_equal_approx(
dunja_state.get_hunger(),
dunja_hunger_before + AnimalStateRecord.HUNGER_PER_TICK * work_ticks,
)
and zora_state.get_last_fed_tick() == manager.tick_count
and zora_state.get_reserved_by() == -1
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
),
"Delivery completion should consume carried food and mutate only Zora beyond natural hunger",
)
_check(
(
feed_events.size() == 1
and _is_exact_feed(feed_events[0], caretaker.id, SimulationIds.ANIMAL_ZORA, zora_state)
and (
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker.id).size()
== 1
)
),
"The complete delivery should retain one withdrawal followed by one exact inventory feed",
)
_check(
(
feed_events.size() == 1
and is_equal_approx(
(
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
+ caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
+ float(feed_events[0].data["amount"])
),
pantry_before,
)
),
"Exactly one delivered food should account for the unit that leaves tracked holdings",
)
carried_food = _get_carried_food(view, caretaker.id)
_check(
carried_food != null and not carried_food.visible,
"The carried-food prop should disappear after the exact goat receives it",
)
await _unload_world(main_scene)
func _test_one_unit_contention() -> void:
var fixture := await _load_world()
var main_scene: Node = fixture["main"]
var manager: Node = fixture["manager"]
var view: Node = fixture["view"]
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
var pantry: StorageStateRecord = manager.get_pantry()
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
if (
pantry_node == null
or zora_node == null
or dunja_node == null
or pantry == null
or zora_state == null
or dunja_state == null
):
_check(false, "The contention proof needs the authored pantry and paired goats")
await _unload_world(main_scene)
return
_prepare_population(manager, view)
_set_pantry_food(manager, 1.0)
zora_state.set_hunger(72.0)
dunja_state.set_hunger(72.0)
var winner: SimNPC = manager.npcs[0]
var loser: SimNPC = manager.npcs[1]
var shared_origin := zora_node.get_interaction_position()
_prepare_feed_task(manager, view, winner, shared_origin)
_prepare_feed_task(manager, view, loser, shared_origin)
var winner_resolved: bool = manager.resolve_npc_target(winner.id, shared_origin)
var loser_resolved: bool = manager.resolve_npc_target(loser.id, shared_origin)
_check(
(
winner_resolved
and loser_resolved
and winner.target_id == SimulationIds.ANIMAL_ZORA
and loser.target_id == SimulationIds.ANIMAL_DUNJA
and zora_state.get_reserved_by() == winner.id
and dunja_state.get_reserved_by() == loser.id
and winner.travel_target_position.is_equal_approx(
pantry_node.get_interaction_position()
)
and loser.travel_target_position.is_equal_approx(pantry_node.get_interaction_position())
),
"Two caretakers should retain independent exact goat claims while sharing the pantry leg",
)
_arrive_at_current_target(manager, view, winner)
_arrive_at_current_target(manager, view, loser)
var all_withdrawals := _events_of_type(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN)
var loser_blocked := _events_for(manager, SimulationIds.EVENT_TASK_BLOCKED, loser.id)
_check(
(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and is_equal_approx(winner.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and is_equal_approx(loser.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and all_withdrawals.size() == 1
and int(all_withdrawals[0].data["actor_id"]) == winner.id
),
"One pantry unit should produce exactly one winning pickup without duplication",
)
_check(
(
winner.current_task == SimulationIds.ACTION_FEED_ANIMAL
and winner.target_id == SimulationIds.ANIMAL_ZORA
and zora_state.get_reserved_by() == winner.id
and loser.current_task != SimulationIds.ACTION_FEED_ANIMAL
and loser.get_inventory_amount(SimulationIds.RESOURCE_FOOD) == 0.0
and dunja_state.get_reserved_by() == -1
and loser_blocked.size() == 1
and (
StringName(loser_blocked[0].data["source_id"])
== SimulationIds.STORAGE_VILLAGE_PANTRY
)
and (StringName(loser_blocked[0].data["action_id"]) == SimulationIds.ACTION_FEED_ANIMAL)
and is_equal_approx(float(loser_blocked[0].data["required_amount"]), 1.0)
),
"The losing pantry arrival should record the shortfall, release only its claim, and not feed",
)
var zora_hunger_before := zora_state.get_hunger()
var dunja_hunger_before := dunja_state.get_hunger()
_arrive_at_current_target(manager, view, winner)
var work_ticks := int(ceil(winner.task_duration))
for _tick in range(work_ticks):
manager.simulate_tick()
var feed_events := _events_of_type(manager, SimulationIds.EVENT_ANIMAL_FED)
_check(
(
feed_events.size() == 1
and _is_exact_feed(feed_events[0], winner.id, SimulationIds.ANIMAL_ZORA, zora_state)
and is_equal_approx(
zora_state.get_hunger(),
(
zora_hunger_before
+ AnimalStateRecord.HUNGER_PER_TICK * work_ticks
- AnimalStateRecord.HUNGER_RELIEF
),
)
and is_equal_approx(
dunja_state.get_hunger(),
dunja_hunger_before + AnimalStateRecord.HUNGER_PER_TICK * work_ticks,
)
),
"Contention should complete only the winning caretaker's exact Zora delivery",
)
_check(
(
_events_of_type(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN).size() == 1
and is_equal_approx(
(
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
+ winner.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
+ loser.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
+ float(feed_events[0].data["amount"])
),
1.0,
)
),
"One-unit contention should conserve exactly one food through final delivery",
)
await _unload_world(main_scene)
func _test_legacy_pre_pickup_restore_reroutes() -> void:
var fixture := await _load_world()
var main_scene: Node = fixture["main"]
var manager: Node = fixture["manager"]
var view: Node = fixture["view"]
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
var pantry: StorageStateRecord = manager.get_pantry()
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
if pantry_node == null or zora_node == null or pantry == null or zora_state == null:
_check(false, "The legacy-route proof needs the authored pantry and Zora")
await _unload_world(main_scene)
return
_prepare_population(manager, view)
_set_pantry_food(manager, 2.0)
zora_state.set_hunger(72.0)
var caretaker: SimNPC = manager.npcs[0]
var caretaker_id := caretaker.id
var origin := zora_node.get_interaction_position()
_prepare_feed_task(manager, view, caretaker, origin)
_check(
manager.resolve_npc_target(caretaker.id, origin),
"The legacy-route caretaker should establish an exact Zora claim",
)
# Schema v11 predates the pantry leg, so its valid active feed records can
# retain an empty inventory while traveling directly to the claimed goat.
caretaker.travel_target_position = zora_node.get_interaction_position()
caretaker.has_travel_target = true
var legacy_route_json: String = manager.serialize_state()
var legacy_route_checksum: String = manager.get_state_checksum()
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
_check(
manager.restore_state_from_json(legacy_route_json),
"A pre-delivery schema-v11 feed route should remain restorable",
)
_freeze_visuals(view)
caretaker = _find_npc(manager.npcs, caretaker_id)
pantry = manager.get_pantry()
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
_check(
(
caretaker != null
and manager.get_state_checksum() == legacy_route_checksum
and caretaker.travel_target_position.is_equal_approx(
zora_node.get_interaction_position()
)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and zora_state.get_reserved_by() == caretaker_id
),
"Restore should first preserve the valid legacy route and exact claim",
)
if caretaker == null:
await _unload_world(main_scene)
return
_arrive_at_current_target(manager, view, caretaker)
_check(
(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and _events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).is_empty()
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
and zora_state.get_reserved_by() == caretaker_id
and caretaker.travel_target_position.is_equal_approx(
pantry_node.get_interaction_position()
)
),
"Legacy goat arrival should reroute to the pantry without a remote withdrawal",
)
_arrive_at_current_target(manager, view, caretaker)
var withdrawal_events := _events_for(
manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id
)
_check(
(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and withdrawal_events.size() == 1
and _is_exact_withdrawal(
withdrawal_events[0],
caretaker_id,
pantry_node.get_interaction_position(),
)
and caretaker.travel_target_position.is_equal_approx(
zora_node.get_interaction_position()
)
and zora_state.get_reserved_by() == caretaker_id
),
"Legacy continuation should withdraw only after physical pantry arrival",
)
await _unload_world(main_scene)
func _test_legacy_working_restore_reroutes() -> void:
var fixture := await _load_world()
var main_scene: Node = fixture["main"]
var manager: Node = fixture["manager"]
var view: Node = fixture["view"]
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
var pantry: StorageStateRecord = manager.get_pantry()
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
if pantry_node == null or zora_node == null or pantry == null or zora_state == null:
_check(false, "The legacy-working proof needs the authored pantry and Zora")
await _unload_world(main_scene)
return
_prepare_population(manager, view)
_set_pantry_food(manager, 2.0)
zora_state.set_hunger(72.0)
var caretaker: SimNPC = manager.npcs[0]
var caretaker_id := caretaker.id
var origin := zora_node.get_interaction_position()
_prepare_feed_task(manager, view, caretaker, origin)
_check(
manager.resolve_npc_target(caretaker.id, origin),
"The legacy-working caretaker should establish an exact Zora claim",
)
# The old one-leg flow could also be saved after goat arrival, while work
# was already in progress but before pantry-backed completion.
caretaker.task_state = SimNPC.TASK_STATE_WORKING
caretaker.task_progress = 2.0
caretaker.task_complete = false
caretaker.has_travel_target = false
caretaker.travel_target_position = zora_node.get_interaction_position()
var legacy_working_json: String = manager.serialize_state()
var legacy_working_checksum: String = manager.get_state_checksum()
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
_check(
manager.restore_state_from_json(legacy_working_json),
"A working pre-delivery schema-v11 feed state should remain restorable",
)
_freeze_visuals(view)
caretaker = _find_npc(manager.npcs, caretaker_id)
pantry = manager.get_pantry()
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
_check(
(
caretaker != null
and manager.get_state_checksum() == legacy_working_checksum
and caretaker.task_state == SimNPC.TASK_STATE_WORKING
and is_equal_approx(caretaker.task_progress, 2.0)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and zora_state.get_reserved_by() == caretaker_id
),
"Restore should initially preserve the legacy working state and checksum",
)
if caretaker == null:
await _unload_world(main_scene)
return
manager.simulate_tick()
_check(
(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and _events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).is_empty()
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
and caretaker.has_travel_target
and caretaker.travel_target_position.is_equal_approx(
pantry_node.get_interaction_position()
)
and is_equal_approx(caretaker.task_progress, 2.0)
and zora_state.get_reserved_by() == caretaker_id
),
"The first resumed tick should preserve work progress and reroute to the pantry",
)
_arrive_at_current_target(manager, view, caretaker)
_check(
(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.travel_target_position.is_equal_approx(
zora_node.get_interaction_position()
)
and is_equal_approx(caretaker.task_progress, 2.0)
and zora_state.get_reserved_by() == caretaker_id
),
"Recovered work should pick up at the pantry and return to the same Zora claim",
)
_arrive_at_current_target(manager, view, caretaker)
_check(
(
caretaker.task_state == SimNPC.TASK_STATE_WORKING
and is_equal_approx(caretaker.task_progress, 2.0)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and zora_state.get_reserved_by() == caretaker_id
),
"Legacy work should resume beside Zora without losing its saved progress",
)
await _unload_world(main_scene)
func _test_post_pickup_restore_continuation() -> void:
var fixture := await _load_world()
var main_scene: Node = fixture["main"]
var manager: Node = fixture["manager"]
var view: Node = fixture["view"]
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
var pantry: StorageStateRecord = manager.get_pantry()
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
if pantry_node == null or zora_node == null or pantry == null or zora_state == null:
_check(false, "The restore proof needs the authored pantry and Zora")
await _unload_world(main_scene)
return
_prepare_population(manager, view)
_set_pantry_food(manager, 2.0)
zora_state.set_hunger(72.0)
var caretaker: SimNPC = manager.npcs[0]
var caretaker_id := caretaker.id
var origin := zora_node.get_interaction_position()
_prepare_feed_task(manager, view, caretaker, origin)
_check(
manager.resolve_npc_target(caretaker.id, origin),
"The restore caretaker should resolve Zora's two-leg delivery",
)
_arrive_at_current_target(manager, view, caretaker)
var midpoint_json: String = manager.serialize_state()
var midpoint_checksum: String = manager.get_state_checksum()
var midpoint_position := caretaker.position
var midpoint_destination := caretaker.travel_target_position
var midpoint_pantry := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
_check(
(
caretaker.target_id == SimulationIds.ANIMAL_ZORA
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.has_travel_target
and caretaker.travel_target_position.is_equal_approx(
zora_node.get_interaction_position()
)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and zora_state.get_reserved_by() == caretaker.id
and (
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker.id).size()
== 1
)
and _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker.id).is_empty()
),
"The save point should be after one real pickup and before the exact goat delivery",
)
_arrive_at_current_target(manager, view, caretaker)
var continuation_ticks := int(ceil(caretaker.task_duration))
for _tick in range(continuation_ticks):
manager.simulate_tick()
var uninterrupted_checksum: String = manager.get_state_checksum()
var uninterrupted_feed_events := _events_for(
manager, SimulationIds.EVENT_ANIMAL_FED, caretaker_id
)
_check(
(
uninterrupted_feed_events.size() == 1
and (
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).size()
== 1
)
),
"The uninterrupted control should finish with one pickup and one feed",
)
_check(
manager.restore_state_from_json(midpoint_json),
"A post-pickup in-progress feed state should pass the complete save schema",
)
_freeze_visuals(view)
caretaker = _find_npc(manager.npcs, caretaker_id)
pantry = manager.get_pantry()
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
var restored_visual := _get_visual(view, caretaker_id)
var restored_carried_food := _get_carried_food(view, caretaker_id)
_check(
(
caretaker != null
and pantry != null
and zora_state != null
and manager.get_state_checksum() == midpoint_checksum
and caretaker.position.is_equal_approx(midpoint_position)
and caretaker.travel_target_position.is_equal_approx(midpoint_destination)
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
and caretaker.has_travel_target
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), midpoint_pantry)
and zora_state.get_reserved_by() == caretaker_id
and restored_visual != null
and restored_visual.global_position.is_equal_approx(midpoint_position)
and restored_visual.get("current_target") == midpoint_destination
and restored_carried_food != null
and restored_carried_food.visible
and (
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).size()
== 1
)
and _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker_id).is_empty()
),
"Restore should retain exact claim, route, carried food, pickup history, and checksum",
)
if caretaker == null:
await _unload_world(main_scene)
return
_arrive_at_current_target(manager, view, caretaker)
for _tick in range(continuation_ticks):
manager.simulate_tick()
var restored_feed_events := _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker_id)
_check(
(
manager.get_state_checksum() == uninterrupted_checksum
and (
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).size()
== 1
)
and restored_feed_events.size() == 1
and _is_exact_feed(
restored_feed_events[0],
caretaker_id,
SimulationIds.ANIMAL_ZORA,
zora_state,
)
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), midpoint_pantry)
),
"Restored continuation should match uninterrupted state without replaying either transfer",
)
await _unload_world(main_scene)
func _load_world() -> Dictionary:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
for _frame in 3:
await physics_frame
var manager: Node = main_scene.get_node("SimulationManager")
manager.set_process(false)
var view: Node = main_scene.get_node("WorldViewManager")
_freeze_visuals(view)
return {"main": main_scene, "manager": manager, "view": view}
func _unload_world(main_scene: Node) -> void:
main_scene.queue_free()
await process_frame
await physics_frame
func _prepare_population(manager: Node, view: Node) -> void:
for npc in manager.npcs:
manager.release_npc_reservation(npc.id)
npc.inventory.clear()
npc.hunger = 20.0
npc.energy = 100.0
npc.is_starving = false
npc.starvation_ticks = 0
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
npc.start_working()
(
manager
. emit_signal(
"npc_inventory_changed",
npc,
SimulationIds.RESOURCE_FOOD,
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
)
)
_freeze_visuals(view)
func _prepare_feed_task(manager: Node, view: Node, npc: SimNPC, origin: Vector3) -> void:
manager.release_npc_reservation(npc.id)
npc.inventory.clear()
npc.target_id = &""
npc.has_travel_target = false
npc.position = origin
npc.set_task(SimulationIds.ACTION_FEED_ANIMAL)
var visual := _get_visual(view, npc.id)
if visual != null:
visual.global_position = origin
manager.synchronize_npc_position(npc.id, origin)
func _set_pantry_food(manager: Node, amount: float) -> void:
var pantry: StorageStateRecord = manager.get_pantry()
(
pantry
. withdraw(
SimulationIds.RESOURCE_FOOD,
pantry.get_amount(SimulationIds.RESOURCE_FOOD),
)
)
pantry.deposit(SimulationIds.RESOURCE_FOOD, amount)
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
func _arrive_at_current_target(manager: Node, view: Node, npc: SimNPC) -> void:
var visual := _get_visual(view, npc.id)
if visual != null:
visual.set_physics_process(false)
visual.global_position = npc.travel_target_position
manager.synchronize_npc_position(npc.id, npc.travel_target_position)
manager.notify_npc_arrived(npc.id)
func _freeze_visuals(view: Node) -> void:
for visual in view.active_npc_visuals.values():
visual.set_physics_process(false)
func _get_visual(view: Node, npc_id: int) -> Node3D:
return view.active_npc_visuals.get(npc_id) as Node3D
func _get_carried_food(view: Node, npc_id: int) -> MeshInstance3D:
var visual := _get_visual(view, npc_id)
if visual == null:
return null
return visual.get_node("CarriedFood") as MeshInstance3D
func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC:
for npc in npcs:
if npc.id == npc_id:
return npc
return null
func _find_npc_record(records: Array[NPCStateRecord], npc_id: int) -> NPCStateRecord:
for record in records:
if int(record.data["id"]) == npc_id:
return record
return null
func _events_for(
manager: Node, event_type: StringName, actor_id: int
) -> Array[EconomicEventRecord]:
var matching: Array[EconomicEventRecord] = []
for event in manager.economic_events:
if (
StringName(event.data["event_type"]) == event_type
and int(event.data["actor_id"]) == actor_id
):
matching.append(event)
return matching
func _events_of_type(manager: Node, event_type: StringName) -> Array[EconomicEventRecord]:
var matching: Array[EconomicEventRecord] = []
for event in manager.economic_events:
if StringName(event.data["event_type"]) == event_type:
matching.append(event)
return matching
func _is_exact_withdrawal(
event: EconomicEventRecord, actor_id: int, pantry_position: Vector3
) -> bool:
return (
event != null
and int(event.data["actor_id"]) == actor_id
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
and (StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id))
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and is_equal_approx(float(event.data["amount"]), 1.0)
and event.get_world_position().is_equal_approx(pantry_position)
)
func _is_exact_feed(
event: EconomicEventRecord,
actor_id: int,
animal_id: StringName,
animal_state: AnimalStateRecord,
) -> bool:
return (
event != null
and animal_state != null
and int(event.data["actor_id"]) == actor_id
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
and StringName(event.data["destination_id"]) == animal_id
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and is_equal_approx(float(event.data["amount"]), 1.0)
and event.get_world_position().is_equal_approx(animal_state.get_position())
)
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Animal food delivery passed")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)