360 lines
14 KiB
GDScript
360 lines
14 KiB
GDScript
extends GutTest
|
|
|
|
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
|
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
|
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
|
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
|
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
|
|
|
|
|
|
func test_simulation_clock_caps_frame_work_without_discarding_backlog() -> void:
|
|
var clock := SimulationClock.new(1.0)
|
|
clock.cycle_duration_seconds = 100.0
|
|
|
|
assert_eq(clock.advance(10.0, 3), 3)
|
|
assert_eq(clock.elapsed_ticks, 3)
|
|
assert_eq(clock.accumulator, 7.0)
|
|
assert_true(is_equal_approx(clock.time_of_day(), 0.03))
|
|
assert_eq(clock.advance(0.0, 3), 3)
|
|
assert_eq(clock.elapsed_ticks, 6)
|
|
assert_eq(clock.accumulator, 4.0)
|
|
assert_true(is_equal_approx(clock.time_of_day(), 0.06))
|
|
|
|
var fractional_clock := SimulationClock.new(1.0)
|
|
fractional_clock.cycle_duration_seconds = 100.0
|
|
assert_eq(fractional_clock.advance(0.25, 3), 0)
|
|
assert_true(is_equal_approx(fractional_clock.time_of_day(), 0.0025))
|
|
|
|
|
|
func test_player_attack_uses_definition_reach_and_unscaled_realtime_cooldown() -> void:
|
|
var conflict := ConflictSystem.new()
|
|
conflict.configure(null, 0.1)
|
|
conflict.initialize_factions()
|
|
conflict.set_player_combatant_position(Vector3.ZERO)
|
|
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
|
assert_not_null(sword)
|
|
var wolf_id := conflict.spawn_wolf(Vector3(sword.reach + 0.01, 8.0, 0.0))
|
|
var wolf := conflict.get_combatant(wolf_id)
|
|
|
|
assert_eq(conflict.player_attack(wolf_id), 0.0)
|
|
wolf.set_position(Vector3(sword.reach - 0.01, 8.0, 0.0))
|
|
assert_eq(conflict.player_attack(wolf_id), sword.damage)
|
|
assert_false(conflict.is_player_attack_ready())
|
|
conflict.advance(1)
|
|
assert_false(conflict.is_player_attack_ready())
|
|
conflict.advance_realtime(sword.attack_cooldown - 0.01)
|
|
assert_false(conflict.is_player_attack_ready())
|
|
conflict.advance_realtime(0.02)
|
|
assert_true(conflict.is_player_attack_ready())
|
|
|
|
|
|
func test_player_dash_duration_and_cooldown_are_enforced_separately() -> void:
|
|
var controller := PlayerCombatController.new()
|
|
var player := CharacterBody3D.new()
|
|
controller.player = player
|
|
player.add_child(controller)
|
|
add_child_autofree(player)
|
|
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
|
|
|
assert_true(is_equal_approx(controller.attack_range, sword.reach))
|
|
controller.call("_perform_dash")
|
|
assert_true(controller.is_dashing())
|
|
assert_true(
|
|
is_equal_approx(controller.get_dash_cooldown_remaining(), controller.dash_cooldown_seconds)
|
|
)
|
|
controller.call("_advance_timers", controller.dash_duration + 0.01)
|
|
assert_false(controller.is_dashing())
|
|
controller.call("_perform_dash")
|
|
assert_false(controller.is_dashing())
|
|
controller.call("_advance_timers", controller.get_dash_cooldown_remaining())
|
|
controller.call("_perform_dash")
|
|
assert_true(controller.is_dashing())
|
|
|
|
|
|
func test_combat_cooldown_uses_the_configured_simulation_tick() -> void:
|
|
var combatant := CombatantStateRecord.create(
|
|
&"cooldown_test",
|
|
SimulationIds.COMBATANT_KIND_PLAYER,
|
|
SimulationIds.FACTION_VILLAGE,
|
|
"Cooldown Tester",
|
|
Vector3.ZERO,
|
|
100.0,
|
|
SimulationIds.ITEM_SWORD
|
|
)
|
|
|
|
combatant.mark_attacked(0.3)
|
|
assert_eq(combatant.get_attack_cooldown(), 2)
|
|
combatant.tick_cooldown()
|
|
combatant.tick_cooldown()
|
|
assert_true(combatant.is_attack_ready())
|
|
combatant.mark_attacked(1.2)
|
|
assert_eq(combatant.get_attack_cooldown(), 1)
|
|
|
|
|
|
func test_storage_never_accepts_more_than_its_capacity() -> void:
|
|
var storage := StorageStateRecord.create(&"test_storage", {"food": 4.5}, 5.0)
|
|
|
|
assert_eq(storage.deposit(SimulationIds.RESOURCE_FOOD, 2.0), 0.5)
|
|
assert_eq(storage.get_amount(SimulationIds.RESOURCE_FOOD), 5.0)
|
|
assert_eq(storage.get_available_capacity(), 0.0)
|
|
|
|
|
|
func test_economy_moves_inventory_into_authoritative_storage() -> void:
|
|
var village := SimVillage.new()
|
|
village.food = 3.0
|
|
var economy := VillageEconomyScript.new()
|
|
economy.configure(village, false)
|
|
economy.initialize_storage()
|
|
var npc := SimNPC.new(7, "Tester", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
|
npc.add_inventory(SimulationIds.RESOURCE_FOOD, 2.0)
|
|
|
|
assert_eq(economy.deposit_inventory(npc, SimulationIds.RESOURCE_FOOD), 2.0)
|
|
assert_eq(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
assert_eq(economy.get_pantry().get_amount(SimulationIds.RESOURCE_FOOD), 5.0)
|
|
assert_eq(village.food, 5.0)
|
|
|
|
|
|
func test_failed_food_consumption_preserves_fractional_inventory() -> void:
|
|
var village := SimVillage.new()
|
|
var economy := VillageEconomyScript.new()
|
|
economy.configure(village, false)
|
|
var npc := SimNPC.new(8, "Fractional", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
|
npc.hunger = 50.0
|
|
npc.add_inventory(SimulationIds.RESOURCE_FOOD, 0.5)
|
|
var event_count := [0]
|
|
economy.economic_event_requested.connect(func(_a, _b, _c, _d, _e, _f): event_count[0] += 1)
|
|
|
|
assert_false(economy.consume_npc_food(npc))
|
|
assert_eq(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.5)
|
|
assert_eq(npc.hunger, 55.0)
|
|
assert_eq(event_count[0], 0)
|
|
|
|
|
|
func test_event_log_preserves_order_and_derives_consumption_rate() -> void:
|
|
var event_log := SimulationEventLogScript.new()
|
|
event_log.record_narrative(10, SimulationIds.EVENT_TASK_STARTED, 4, &"", "Study")
|
|
event_log.record_economic(
|
|
20,
|
|
SimulationIds.EVENT_ITEM_CONSUMED,
|
|
4,
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
&"consumed",
|
|
SimulationIds.RESOURCE_FOOD,
|
|
1.0
|
|
)
|
|
|
|
assert_eq(event_log.events.size(), 2)
|
|
assert_eq(int(event_log.events[0].data["event_id"]), 0)
|
|
assert_eq(int(event_log.events[1].data["event_id"]), 1)
|
|
assert_eq(event_log.get_for_actor(4).size(), 2)
|
|
assert_eq(float(event_log.get_consumption_rates(200)["food_per_day"]), 1.0)
|
|
var legacy_event_data := event_log.events[1].to_dictionary()
|
|
legacy_event_data["schema_version"] = EconomicEventRecord.LEGACY_SCHEMA_VERSION
|
|
legacy_event_data.erase("world_position")
|
|
var migrated_event := EconomicEventRecord.from_dictionary(legacy_event_data)
|
|
assert_not_null(migrated_event)
|
|
assert_eq(migrated_event.get_world_position(), Vector3.ZERO)
|
|
|
|
|
|
func test_food_aid_builds_directed_trust_once_per_event() -> void:
|
|
var contributor := SimNPC.new(0, "Amina", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
|
var observer := SimNPC.new(1, "Tarik", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
|
contributor.home_position = Vector3.ZERO
|
|
observer.home_position = Vector3(1.0, 0.0, 0.0)
|
|
observer.hunger = 85.0
|
|
var villagers: Array[SimNPC] = [contributor, observer]
|
|
var relationship_system := RelationshipSystemScript.new()
|
|
relationship_system.initialize_households(villagers)
|
|
var deposit_event := EconomicEventRecord.create(
|
|
7,
|
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
|
12,
|
|
contributor.id,
|
|
SimulationIds.npc_inventory_id(contributor.id),
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
2.0
|
|
)
|
|
var later_deposit_event := EconomicEventRecord.create(
|
|
8,
|
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
|
13,
|
|
contributor.id,
|
|
SimulationIds.npc_inventory_id(contributor.id),
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
1.0
|
|
)
|
|
|
|
var knower_ids: Array[int] = [contributor.id, observer.id]
|
|
relationship_system.apply_event(deposit_event, villagers, knower_ids)
|
|
relationship_system.apply_event(later_deposit_event, villagers, knower_ids)
|
|
relationship_system.apply_event(deposit_event, villagers, knower_ids)
|
|
var relationship: RelationshipStateRecord = relationship_system.get_relationship(
|
|
observer.id, contributor.id
|
|
)
|
|
assert_eq(relationship.get_trust(), 0.8)
|
|
assert_eq(relationship.get_last_trust_cause_event_id(), 8)
|
|
assert_eq(
|
|
relationship_system.get_relationship(contributor.id, observer.id).get_trust(),
|
|
RelationshipStateRecord.NEUTRAL_TRUST
|
|
)
|
|
|
|
|
|
func test_event_knowledge_is_spatial_directed_and_idempotent() -> void:
|
|
var actor := SimNPC.new(0, "Amina", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
|
var nearby := SimNPC.new(1, "Tarik", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
|
var distant := SimNPC.new(2, "Jasmin", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
|
actor.position = Vector3.ZERO
|
|
nearby.position = Vector3(4.0, 0.0, 0.0)
|
|
distant.position = Vector3(20.0, 0.0, 0.0)
|
|
var villagers: Array[SimNPC] = [actor, nearby, distant]
|
|
var event := EconomicEventRecord.create(
|
|
12,
|
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
|
30,
|
|
actor.id,
|
|
SimulationIds.npc_inventory_id(actor.id),
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
2.0,
|
|
Vector3.ZERO
|
|
)
|
|
actor.position = Vector3(100.0, 0.0, 0.0)
|
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
|
|
|
assert_eq(knowledge_system.observe_event(event, villagers).size(), 2)
|
|
assert_eq(knowledge_system.observe_event(event, villagers).size(), 0)
|
|
assert_true(knowledge_system.knows_event(actor.id, 12))
|
|
assert_true(knowledge_system.knows_event(nearby.id, 12))
|
|
assert_false(knowledge_system.knows_event(distant.id, 12))
|
|
assert_eq(knowledge_system.get_knowers(12), [actor.id, nearby.id])
|
|
assert_eq(
|
|
knowledge_system.get_record(actor.id, 12).get_acquisition_method(),
|
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
|
)
|
|
assert_eq(
|
|
knowledge_system.get_record(nearby.id, 12).get_acquisition_method(),
|
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
|
)
|
|
|
|
|
|
func test_event_communication_preserves_first_provenance_and_stops_after_one_hop() -> void:
|
|
var actor := SimNPC.new(0, "Amina", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
|
var witness := SimNPC.new(1, "Tarik", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
|
var listener := SimNPC.new(2, "Jasmin", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
|
var relay_target := SimNPC.new(3, "Elma", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
|
actor.position = Vector3.ZERO
|
|
witness.position = Vector3(2.0, 0.0, 0.0)
|
|
listener.position = Vector3(20.0, 0.0, 0.0)
|
|
relay_target.position = Vector3(20.0, 0.0, 1.0)
|
|
var villagers: Array[SimNPC] = [actor, witness, listener, relay_target]
|
|
var event := EconomicEventRecord.create(
|
|
21,
|
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
|
40,
|
|
actor.id,
|
|
SimulationIds.npc_inventory_id(actor.id),
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
2.0,
|
|
Vector3.ZERO
|
|
)
|
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
|
knowledge_system.observe_event(event, villagers)
|
|
|
|
assert_null(knowledge_system.communicate_event(event, listener.id, relay_target.id, 50))
|
|
var communicated := knowledge_system.communicate_event(event, witness.id, listener.id, 50)
|
|
assert_not_null(communicated)
|
|
assert_eq(
|
|
communicated.get_acquisition_method(), SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
|
)
|
|
assert_eq(communicated.get_source_npc_id(), witness.id)
|
|
assert_eq(communicated.get_acquired_tick(), 50)
|
|
assert_eq(
|
|
communicated.get_source_acquisition_method(), SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
|
)
|
|
assert_null(knowledge_system.communicate_event(event, actor.id, listener.id, 50))
|
|
assert_eq(knowledge_system.get_record(listener.id, event.data["event_id"]), communicated)
|
|
assert_null(knowledge_system.communicate_event(event, listener.id, relay_target.id, 50))
|
|
assert_false(knowledge_system.knows_event(relay_target.id, int(event.data["event_id"])))
|
|
var actor_gap_system := EventKnowledgeSystemScript.new()
|
|
var direct_witness_records: Array[KnownEventStateRecord] = [
|
|
KnownEventStateRecord.create(
|
|
witness.id, int(event.data["event_id"]), SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
|
)
|
|
]
|
|
actor_gap_system.restore(direct_witness_records)
|
|
assert_null(actor_gap_system.communicate_event(event, witness.id, actor.id, 50))
|
|
|
|
|
|
func test_knowledge_retention_is_bounded_stable_and_importance_ranked() -> void:
|
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
|
var records: Array[KnownEventStateRecord] = [
|
|
KnownEventStateRecord.create(0, 30, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 1),
|
|
KnownEventStateRecord.create(0, 20, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 2),
|
|
KnownEventStateRecord.create(0, 10, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 3),
|
|
KnownEventStateRecord.create(0, 5, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 4),
|
|
]
|
|
knowledge_system.restore(records)
|
|
var no_lasting: Array[KnownEventStateRecord] = []
|
|
var capacity_forgotten := knowledge_system.maintain_retention(4, 100, no_lasting, false)
|
|
|
|
assert_eq(capacity_forgotten.size(), 1)
|
|
assert_eq(capacity_forgotten[0].get_event_id(), 30)
|
|
assert_eq(knowledge_system.get_known_event_ids(0, 0), [20, 10, 5])
|
|
var lasting: Array[KnownEventStateRecord] = [knowledge_system.get_record(0, 20)]
|
|
assert_eq(knowledge_system.maintain_retention(101, 100, lasting, true).size(), 0)
|
|
assert_true(knowledge_system.knows_event(0, 20))
|
|
assert_eq(knowledge_system.maintain_retention(103, 100, lasting, true).size(), 1)
|
|
assert_false(knowledge_system.knows_event(0, 10))
|
|
assert_true(knowledge_system.knows_event(0, 20))
|
|
|
|
var ranked_system := EventKnowledgeSystemScript.new()
|
|
var ranked_records: Array[KnownEventStateRecord] = [
|
|
KnownEventStateRecord.create(1, 10, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 5),
|
|
KnownEventStateRecord.create(1, 20, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 20),
|
|
KnownEventStateRecord.create(
|
|
1,
|
|
30,
|
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
|
2,
|
|
20,
|
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
|
),
|
|
KnownEventStateRecord.create(1, 40, SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY, -1, 10),
|
|
]
|
|
ranked_system.restore(ranked_records)
|
|
var lasting_event_ids: Array[int] = [10]
|
|
assert_eq(ranked_system.get_retained_event_ids(1, lasting_event_ids, 0), [10, 30, 20, 40])
|
|
assert_eq(ranked_system.get_retained_event_ids(1, lasting_event_ids, 2), [10, 30])
|
|
assert_eq(ranked_system.get_communicable_event_ids(1, lasting_event_ids), [10, 20])
|
|
|
|
var objective_events: Array[EconomicEventRecord] = []
|
|
for event_id in [10, 20, 30, 40]:
|
|
objective_events.append(
|
|
EconomicEventRecord.create(
|
|
event_id,
|
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
|
0,
|
|
0,
|
|
SimulationIds.npc_inventory_id(0),
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
1.0
|
|
)
|
|
)
|
|
var manager := SimulationManagerScript.new()
|
|
manager.event_log.restore(objective_events, 41)
|
|
manager.event_knowledge_system.restore(ranked_records)
|
|
var relationships: Array[RelationshipStateRecord] = [
|
|
RelationshipStateRecord.create(1, 0, 0.5, 0.65, 10)
|
|
]
|
|
manager.relationship_system.restore(relationships)
|
|
var retained_events: Array[EconomicEventRecord] = manager.get_retained_memory_events(1, 3)
|
|
var retained_ids: Array[int] = []
|
|
for event in retained_events:
|
|
retained_ids.append(int(event.data["event_id"]))
|
|
assert_eq(retained_ids, [10, 30, 20])
|
|
manager.free()
|