Files
gamedev-the-steward/tests/animal_pair_vertical_slice_test.gd
T
2026-07-30 23:03:15 +02:00

281 lines
9.4 KiB
GDScript

extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
var manager: Node = main_scene.get_node("SimulationManager")
manager.set_process(false)
for _frame in 3:
await physics_frame
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
var shared_pasture := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
var dunja_shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_SHELTER)
var zora_shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_ZORA_SHELTER)
_check(
(
AnimalNode.get_all().size() == 2
and manager.animal_care.states.size() == 2
and dunja_node != null
and zora_node != null
and dunja_state != null
and zora_state != null
and dunja_node.state == dunja_state
and zora_node.state == zora_state
),
"The real main scene should bind exactly two stable named-goat records",
)
_check(
shared_pasture != null and dunja_shelter != null and zora_shelter != null,
"The pair proof needs one shared pasture and one authored shelter per goat",
)
if (
dunja_node == null
or zora_node == null
or dunja_state == null
or zora_state == null
or shared_pasture == null
or dunja_shelter == null
or zora_shelter == null
):
_finish()
return
for npc in manager.npcs:
_park_npc(npc)
var adapter := manager.active_world_adapter as ActiveWorldAdapter
var dunja_sites: Array[Dictionary] = adapter.get_animal_routine_sites(
SimulationIds.SPECIES_GOAT, SimulationIds.ANIMAL_DUNJA
)
var zora_sites: Array[Dictionary] = adapter.get_animal_routine_sites(
SimulationIds.SPECIES_GOAT, SimulationIds.ANIMAL_ZORA
)
_check(
(
(
_site_ids(dunja_sites)
== [
SimulationIds.ANIMAL_SITE_DUNJA_PASTURE,
SimulationIds.ANIMAL_SITE_DUNJA_SHELTER,
]
)
and (
_site_ids(zora_sites)
== [
SimulationIds.ANIMAL_SITE_DUNJA_PASTURE,
SimulationIds.ANIMAL_SITE_ZORA_SHELTER,
]
)
),
"Routine discovery should expose the shared pasture plus only each goat's own shelter",
)
zora_state.set_hunger(30.0)
_check(
zora_state.schedule_routine_at(manager.tick_count),
"Zora's routine should be schedulable immediately from her authored shelter",
)
manager.simulate_tick()
_check(
(
zora_state.has_active_travel_target()
and zora_state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_ZORA_SHELTER
and (zora_state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
and (zora_state.get_travel_target_position().is_equal_approx(
shared_pasture.get_routine_position()
))
),
"Zora should deterministically choose the shared pasture from her own shelter",
)
_check(
zora_state.complete_travel(shared_pasture.get_routine_position(), manager.tick_count),
"The routine proof should be able to complete Zora's pasture leg directly",
)
_check(
zora_state.schedule_routine_at(manager.tick_count),
"Zora's return leg should be schedulable without waiting for long navigation",
)
manager.simulate_tick()
_check(
(
zora_state.has_active_travel_target()
and (zora_state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
and (zora_state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_ZORA_SHELTER)
and (zora_state.get_travel_target_position().is_equal_approx(
zora_shelter.get_routine_position()
))
),
"Zora should return to her own shelter rather than entering Dunja's shelter",
)
zora_state.cancel_travel(manager.tick_count + AnimalCareSystem.ROUTINE_RETRY_TICKS)
dunja_state.set_hunger(72.0)
zora_state.set_hunger(72.0)
var zora_caretaker: SimNPC = manager.npcs[0]
var dunja_caretaker: SimNPC = manager.npcs[1]
_prepare_feed_task(manager, zora_caretaker)
_prepare_feed_task(manager, dunja_caretaker)
var shared_origin := zora_node.get_interaction_position()
zora_caretaker.position = shared_origin
dunja_caretaker.position = shared_origin
_check(
manager.resolve_npc_target(zora_caretaker.id, shared_origin),
"The first caretaker should resolve a real hungry animal target",
)
_check(
(
zora_caretaker.target_id == SimulationIds.ANIMAL_ZORA
and zora_state.get_reserved_by() == zora_caretaker.id
and dunja_state.get_reserved_by() == -1
),
"The first caretaker should claim the nearest hungry goat, Zora, by exact ID",
)
_check(
manager.resolve_npc_target(dunja_caretaker.id, shared_origin),
"The second caretaker should resolve the remaining available hungry goat",
)
_check(
(
dunja_caretaker.target_id == SimulationIds.ANIMAL_DUNJA
and dunja_state.get_reserved_by() == dunja_caretaker.id
and zora_state.get_reserved_by() == zora_caretaker.id
),
"Simultaneous hunger should retain two independent exact animal reservations",
)
var claimed_json: String = manager.serialize_state()
var claimed_checksum: String = manager.get_state_checksum()
var zora_caretaker_id := zora_caretaker.id
var dunja_caretaker_id := dunja_caretaker.id
_check(
manager.restore_state_from_json(claimed_json),
"A save containing both exact feed claims should pass the complete schema",
)
dunja_state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
zora_caretaker = _find_npc(manager.npcs, zora_caretaker_id)
dunja_caretaker = _find_npc(manager.npcs, dunja_caretaker_id)
_check(
(
manager.get_state_checksum() == claimed_checksum
and zora_node.state == zora_state
and dunja_node.state == dunja_state
and zora_caretaker != null
and dunja_caretaker != null
and zora_caretaker.target_id == SimulationIds.ANIMAL_ZORA
and dunja_caretaker.target_id == SimulationIds.ANIMAL_DUNJA
and zora_state.get_reserved_by() == zora_caretaker_id
and dunja_state.get_reserved_by() == dunja_caretaker_id
),
"Restore should retain both claims, bindings, target IDs, and the exact checksum",
)
if zora_caretaker == null or dunja_caretaker == null:
_finish()
return
var pantry: StorageStateRecord = manager.get_pantry()
var pantry_food_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
var zora_hunger_before := zora_state.get_hunger()
var zora_fed_tick_before := zora_state.get_last_fed_tick()
var dunja_hunger_before := dunja_state.get_hunger()
var dunja_fed_tick_before := dunja_state.get_last_fed_tick()
var dunja_reservation_before := dunja_state.get_reserved_by()
_check(
manager.feed_animal(SimulationIds.ANIMAL_ZORA, zora_caretaker_id),
"The restored exact Zora claim should complete one real feed operation",
)
_check(
(
is_equal_approx(
zora_state.get_hunger(), zora_hunger_before - AnimalStateRecord.HUNGER_RELIEF
)
and zora_state.get_last_fed_tick() == manager.tick_count
and zora_state.get_last_fed_tick() != zora_fed_tick_before
and zora_state.get_reserved_by() == -1
and is_equal_approx(dunja_state.get_hunger(), dunja_hunger_before)
and dunja_state.get_last_fed_tick() == dunja_fed_tick_before
and dunja_state.get_reserved_by() == dunja_reservation_before
),
"Feeding Zora should mutate only Zora's hunger, feed tick, and reservation",
)
_check(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_food_before - 1.0),
"Feeding exactly one goat should consume exactly one pantry food",
)
var feed_event := _latest_feed_event(manager)
_check(
(
feed_event != null
and int(feed_event.data["actor_id"]) == zora_caretaker_id
and StringName(feed_event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
and StringName(feed_event.data["destination_id"]) == SimulationIds.ANIMAL_ZORA
and StringName(feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and is_equal_approx(float(feed_event.data["amount"]), 1.0)
and feed_event.get_world_position().is_equal_approx(zora_state.get_position())
),
"The feed event should record the exact caretaker, pantry, Zora destination, and amount",
)
_finish()
func _park_npc(npc: SimNPC) -> void:
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
npc.start_working()
func _prepare_feed_task(manager: Node, npc: SimNPC) -> void:
manager.release_npc_reservation(npc.id)
npc.target_id = &""
npc.has_travel_target = false
npc.set_task(SimulationIds.ACTION_FEED_ANIMAL)
func _site_ids(candidates: Array[Dictionary]) -> Array[StringName]:
var ids: Array[StringName] = []
for candidate in candidates:
ids.append(StringName(candidate["site_id"]))
return ids
func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC:
for npc in npcs:
if npc.id == npc_id:
return npc
return null
func _latest_feed_event(manager: Node) -> EconomicEventRecord:
for index in range(manager.economic_events.size() - 1, -1, -1):
var event := manager.economic_events[index] as EconomicEventRecord
if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED:
return event
return null
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Two-goat vertical slice passed")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)