281 lines
9.4 KiB
GDScript
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)
|