feat: add bounded npc memory retention
This commit is contained in:
@@ -76,7 +76,9 @@ events into `RelationshipSystem`. When an NPC arrives beside a worker at the
|
|||||||
same non-storage activity site, the manager may transfer one direct known fact
|
same non-storage activity site, the manager may transfer one direct known fact
|
||||||
and applies its consequence only to that newly informed listener. It exposes
|
and applies its consequence only to that newly informed listener. It exposes
|
||||||
knowledge, provenance, relationship, and cause queries without moving social
|
knowledge, provenance, relationship, and cause queries without moving social
|
||||||
authority into UI.
|
authority into UI. After consequences are known, it protects current causal
|
||||||
|
facts, enforces the recent-memory cap, and runs age review from authoritative
|
||||||
|
simulation ticks.
|
||||||
|
|
||||||
### VillageEconomy
|
### VillageEconomy
|
||||||
|
|
||||||
@@ -101,6 +103,10 @@ authority into UI.
|
|||||||
legacy, with a stable source NPC ID for direct communication;
|
legacy, with a stable source NPC ID for direct communication;
|
||||||
- permits one performed/witnessed fact to cross one co-located social hop, but
|
- permits one performed/witnessed fact to cross one co-located social hop, but
|
||||||
does not relay communicated or ambiguous legacy facts;
|
does not relay communicated or ambiguous legacy facts;
|
||||||
|
- orders communication by lasting relationship relevance, then acquisition
|
||||||
|
recency;
|
||||||
|
- retains current relationship causes and at most three other recent facts per
|
||||||
|
NPC, expiring day-old recent facts at deterministic review boundaries;
|
||||||
- captures evidence only when the event happens, never from current positions
|
- captures evidence only when the event happens, never from current positions
|
||||||
during restore;
|
during restore;
|
||||||
- answers deterministic known-event and knower queries without formatting
|
- answers deterministic known-event and knower queries without formatting
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ SimulationClock
|
|||||||
-> ActionTargetResolver resolves a stable target ID
|
-> ActionTargetResolver resolves a stable target ID
|
||||||
-> VillageEconomy performs inventory/storage transactions
|
-> VillageEconomy performs inventory/storage transactions
|
||||||
-> SimulationEventLog records completed facts
|
-> SimulationEventLog records completed facts
|
||||||
-> EventKnowledgeSystem records actor/nearby knowledge and bounded transfers
|
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
|
||||||
-> RelationshipSystem applies evidence-gated social consequences
|
-> RelationshipSystem applies evidence-gated social consequences
|
||||||
-> WorldViewManager presents travel and NPC state
|
-> WorldViewManager presents travel and NPC state
|
||||||
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
||||||
@@ -34,7 +34,8 @@ would otherwise obscure that lifecycle:
|
|||||||
history queries, and rate calculations;
|
history queries, and rate calculations;
|
||||||
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
|
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
|
||||||
known objective events, immutable acquisition provenance, proximity witnesses
|
known objective events, immutable acquisition provenance, proximity witnesses
|
||||||
at record time, and one-hop direct fact transfer;
|
at record time, one-hop direct fact transfer, and deterministic recent-fact
|
||||||
|
retention;
|
||||||
- `simulation/relationships/RelationshipSystem.gd` owns directed relationship
|
- `simulation/relationships/RelationshipSystem.gd` owns directed relationship
|
||||||
queries, event-driven trust changes, and deterministic social tie-breaking;
|
queries, event-driven trust changes, and deterministic social tie-breaking;
|
||||||
- `simulation/persistence/` owns save-slot file safety;
|
- `simulation/persistence/` owns save-slot file safety;
|
||||||
|
|||||||
@@ -695,11 +695,16 @@ Completed:
|
|||||||
can hear one direct fact from a nearby worker at the same non-storage
|
can hear one direct fact from a nearby worker at the same non-storage
|
||||||
activity; the inspector names the speaker, the original event is not
|
activity; the inspector names the speaker, the original event is not
|
||||||
replayed, and heard facts do not cascade yet.
|
replayed, and heard facts do not cascade yet.
|
||||||
|
23. Bounded memory retention: acquisition time now drives fact recency; current
|
||||||
|
relationship causes remain lasting while each NPC keeps only three other
|
||||||
|
recent facts. A deterministic daily-sized review fades old routine facts,
|
||||||
|
conversations prefer lasting evidence, and the inspector shows both memory
|
||||||
|
counts and the displayed fact's retention class.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Add bounded importance/retention rules for known facts, as sequenced in
|
1. Add a compact retained-memory/person-history view and one visible social
|
||||||
`LEARNING_ROADMAP.md`.
|
reaction cue, as sequenced in `LEARNING_ROADMAP.md`.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||||
mechanics. The next proof is a beautiful stage for the systems that already
|
mechanics. The next proof is a beautiful stage for the systems that already
|
||||||
|
|||||||
+21
-10
@@ -37,7 +37,7 @@ transfer occurred.
|
|||||||
|
|
||||||
## Persistence and determinism
|
## Persistence and determinism
|
||||||
|
|
||||||
`SimulationStateRecord` schema v6 stores the ordered event stream,
|
`SimulationStateRecord` schema v7 stores the ordered event stream,
|
||||||
`next_event_id`, directed relationships that may reference an exact event, and
|
`next_event_id`, directed relationships that may reference an exact event, and
|
||||||
per-NPC known-event references with first-acquisition provenance. Schema v1 and
|
per-NPC known-event references with first-acquisition provenance. Schema v1 and
|
||||||
v2 saves migrate to an empty stream beginning at ID zero. Parsing rejects
|
v2 saves migrate to an empty stream beginning at ID zero. Parsing rejects
|
||||||
@@ -63,6 +63,15 @@ performed/witnessed facts can cross this one social hop, so this is not yet a
|
|||||||
rumour cascade. Relationship appraisal uses the listener's state when the fact
|
rumour cascade. Relationship appraisal uses the listener's state when the fact
|
||||||
is acquired, not a reconstruction of their needs when the old event occurred.
|
is acquired, not a reconstruction of their needs when the old event occurred.
|
||||||
|
|
||||||
|
Known facts are now bounded without deleting objective history. A fact is
|
||||||
|
lasting while it remains that NPC's current trust cause; other facts are recent.
|
||||||
|
An NPC keeps at most three recent facts, and a deterministic review once per
|
||||||
|
simulated-day-sized interval removes recent facts at least one day old. Current
|
||||||
|
causes remain so `Relationship` and `Because` never point at forgotten
|
||||||
|
evidence. Conversations prefer lasting direct facts, then the most recently
|
||||||
|
acquired direct fact. Communicated provenance snapshots how the speaker knew
|
||||||
|
the event, so a listener's memory stays valid after the speaker forgets.
|
||||||
|
|
||||||
The food-loop regression verifies this chain:
|
The food-loop regression verifies this chain:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -78,15 +87,17 @@ save/restore continuity.
|
|||||||
`NpcVisual` shows a small food sack whenever its authoritative inventory
|
`NpcVisual` shows a small food sack whenever its authoritative inventory
|
||||||
contains food. A newly loaded visual derives the same state directly from the
|
contains food. A newly loaded visual derives the same state directly from the
|
||||||
NPC record, so unloading presentation does not lose the fact.
|
NPC record, so unloading presentation does not lose the fact.
|
||||||
|
The selected-NPC inspector shows lasting/recent counts and labels the displayed
|
||||||
|
fact with both acquisition provenance and retention class.
|
||||||
|
|
||||||
## Deliberate limits
|
## Deliberate limits
|
||||||
|
|
||||||
The stream is currently kept in full for the small simulation garden. Before
|
The objective stream is currently kept in full for the small simulation
|
||||||
large populations or long-running worlds, add measured retention,
|
garden; only per-NPC known references are bounded. Before large populations or
|
||||||
archival/summary rules, and query indexes. Proximity is the only current direct
|
long-running worlds, add measured archival/summary rules and query indexes.
|
||||||
witness rule, and communication is intentionally one-hop and tied to shared
|
Proximity is the only current direct witness rule, and communication is
|
||||||
activity arrival. Line of sight, hearing, acquisition timestamps, multi-hop
|
intentionally one-hop and tied to shared activity arrival. Line of sight,
|
||||||
rumours, secrecy, false beliefs, multi-event causal graphs, and memory
|
hearing, personalized reinforcement/decay, multi-hop rumours, secrecy, false
|
||||||
retention belong in later event/history slices. They should extend this record
|
beliefs, and multi-event causal graphs belong in later event/history slices.
|
||||||
family without making prose authoritative or recomputing old evidence from
|
They should extend this record family without making prose authoritative or
|
||||||
current positions.
|
recomputing old evidence from current positions.
|
||||||
|
|||||||
@@ -684,10 +684,25 @@ The first communicated-knowledge slice is complete:
|
|||||||
malformed-provenance rejection, event-history invariance, save/checksum, and
|
malformed-provenance rejection, event-history invariance, save/checksum, and
|
||||||
informed-choice regressions cover the complete slice.
|
informed-choice regressions cover the complete slice.
|
||||||
|
|
||||||
|
The first importance/retention slice is complete:
|
||||||
|
|
||||||
|
- acquisition ticks order facts by when an NPC learned them rather than by the
|
||||||
|
objective event ID;
|
||||||
|
- a current relationship cause is lasting, while each NPC keeps at most three
|
||||||
|
other recent facts;
|
||||||
|
- a deterministic daily-sized review forgets recent facts at least one day old
|
||||||
|
without deleting objective event history or rolling back trust;
|
||||||
|
- communication prefers lasting direct evidence, and a source-method snapshot
|
||||||
|
keeps listener provenance valid after the teller forgets;
|
||||||
|
- the inspector shows lasting/recent counts, while schema-v7 migration,
|
||||||
|
boundary, capacity, cause-protection, save/checksum, and informed-choice
|
||||||
|
regressions cover the slice.
|
||||||
|
|
||||||
This advances the first Milestone 6 evidence-to-choice path without claiming
|
This advances the first Milestone 6 evidence-to-choice path without claiming
|
||||||
full memory or belief simulation. The practical next slice is importance and
|
full belief simulation. The practical next sequence is a compact retained
|
||||||
retention for known facts before expanding into distortion, secrecy, rumours,
|
memory/person-history view and a visible social-reaction cue, then one bounded
|
||||||
or opportunity generation.
|
Milestone 7 rumour/opportunity proof without generalized distortion or
|
||||||
|
free-form dialogue.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+16
-10
@@ -415,8 +415,8 @@ Focused `RefCounted` collaborators keep rule ownership visible:
|
|||||||
village resource views;
|
village resource views;
|
||||||
- `SimulationEventLog` owns deterministic event history and queries;
|
- `SimulationEventLog` owns deterministic event history and queries;
|
||||||
- `EventKnowledgeSystem` owns per-NPC known-event references, immutable
|
- `EventKnowledgeSystem` owns per-NPC known-event references, immutable
|
||||||
first-acquisition provenance, actor/nearby evidence capture, and bounded
|
first-acquisition time/provenance, actor/nearby evidence capture, bounded
|
||||||
one-hop communication;
|
one-hop communication, and deterministic recent-memory retention;
|
||||||
- `RelationshipSystem` owns directed relationship state, event-driven trust
|
- `RelationshipSystem` owns directed relationship state, event-driven trust
|
||||||
consequences, and deterministic social queries.
|
consequences, and deterministic social queries.
|
||||||
|
|
||||||
@@ -523,7 +523,7 @@ NpcVisual navigates through the active world
|
|||||||
│ ├── definitions/ Stable IDs and custom definition resources
|
│ ├── definitions/ Stable IDs and custom definition resources
|
||||||
│ ├── economy/ Inventory and storage transactions
|
│ ├── economy/ Inventory and storage transactions
|
||||||
│ ├── events/ Ordered event history and queries
|
│ ├── events/ Ordered event history and queries
|
||||||
│ ├── knowledge/ Per-NPC facts, provenance, and bounded transfer
|
│ ├── knowledge/ Per-NPC facts, provenance, transfer, and retention
|
||||||
│ ├── persistence/ Validated local save-slot storage
|
│ ├── persistence/ Validated local save-slot storage
|
||||||
│ ├── relationships/ Directed social consequences and queries
|
│ ├── relationships/ Directed social consequences and queries
|
||||||
│ └── state/ Versioned simulation-state records
|
│ └── state/ Versioned simulation-state records
|
||||||
@@ -533,6 +533,7 @@ NpcVisual navigates through the active world
|
|||||||
│ ├── deterministic_simulation_test.gd
|
│ ├── deterministic_simulation_test.gd
|
||||||
│ ├── food_storage_loop_test.gd
|
│ ├── food_storage_loop_test.gd
|
||||||
│ ├── jajce_world_scaffold_test.gd
|
│ ├── jajce_world_scaffold_test.gd
|
||||||
|
│ ├── knowledge_retention_consequence_test.gd
|
||||||
│ ├── jajce_runtime_integration_test.gd
|
│ ├── jajce_runtime_integration_test.gd
|
||||||
│ ├── npc_visual_lifecycle_test.gd
|
│ ├── npc_visual_lifecycle_test.gd
|
||||||
│ ├── relationship_consequence_test.gd
|
│ ├── relationship_consequence_test.gd
|
||||||
@@ -571,7 +572,7 @@ These are expected prototype constraints, not necessarily isolated bugs:
|
|||||||
gathering use `ResourceNode` instances with no fallback, food transfer uses
|
gathering use `ResourceNode` instances with no fallback, food transfer uses
|
||||||
the typed pantry `StorageNode`, and patrol/study/rest use `ActivitySite`.
|
the typed pantry `StorageNode`, and patrol/study/rest use `ActivitySite`.
|
||||||
- Current NPC, village, resource, storage, event, knowledge, relationship,
|
- Current NPC, village, resource, storage, event, knowledge, relationship,
|
||||||
clock, and RNG state serialize through world schema v6. F5/F9 provide one
|
clock, and RNG state serialize through world schema v7. F5/F9 provide one
|
||||||
validated local quicksave; a save menu, metadata, and player-transform
|
validated local quicksave; a save menu, metadata, and player-transform
|
||||||
persistence remain deferred.
|
persistence remain deferred.
|
||||||
- Simulation-owned resource records retain live amounts, reservations, and
|
- Simulation-owned resource records retain live amounts, reservations, and
|
||||||
@@ -586,12 +587,14 @@ These are expected prototype constraints, not necessarily isolated bugs:
|
|||||||
located items.
|
located items.
|
||||||
- NPCs have home positions, schedule periods, carried food/wood, and directed
|
- NPCs have home positions, schedule periods, carried food/wood, and directed
|
||||||
familiarity/trust. They retain direct or one-hop communicated knowledge of a
|
familiarity/trust. They retain direct or one-hop communicated knowledge of a
|
||||||
food deposit with first-acquisition provenance, but do not yet have wider
|
food deposit with first-acquisition provenance. Current trust causes are
|
||||||
social dimensions, goals, line-of-sight/hearing evidence, importance/decay,
|
lasting; other knowledge is capped and reviewed after one simulated day.
|
||||||
false beliefs, or multi-hop rumours.
|
NPCs do not yet have wider social dimensions, goals, line-of-sight/hearing
|
||||||
|
evidence, personalized reinforcement/decay, false beliefs, or multi-hop
|
||||||
|
rumours.
|
||||||
- The reason inspector exposes current decisions, utility rejections, one exact
|
- The reason inspector exposes current decisions, utility rejections, one exact
|
||||||
relationship cause, and the latest known fact with its speaker when
|
relationship cause, memory counts, and the latest known fact with its speaker
|
||||||
communicated, but deeper historical decision traces are not yet retained.
|
and retention class, but a broader person-history view is not yet present.
|
||||||
- Active navigation is used as if all agents are local; no simulation LOD exists.
|
- Active navigation is used as if all agents are local; no simulation LOD exists.
|
||||||
- Unloaded traveling NPCs preserve their state but do not yet advance through
|
- Unloaded traveling NPCs preserve their state but do not yet advance through
|
||||||
abstract travel time.
|
abstract travel time.
|
||||||
@@ -836,7 +839,10 @@ shared non-storage activity, an already-working direct knower can tell one fact
|
|||||||
to an arriving nearby villager. The same event remains visible as known fact
|
to an arriving nearby villager. The same event remains visible as known fact
|
||||||
and relationship cause, the inspector names the speaker, and the listener's
|
and relationship cause, the inspector names the speaker, and the listener's
|
||||||
future work can change without duplicating event history. Importance and
|
future work can change without duplicating event history. Importance and
|
||||||
retention for known facts is the next systems slice.
|
retention now keep current relationship causes lasting, cap other memories,
|
||||||
|
and forget routine facts at deterministic daily-sized reviews without deleting
|
||||||
|
the objective event. A compact retained-memory/person-history view and visible
|
||||||
|
social-reaction cue are the next systems slice.
|
||||||
|
|
||||||
The remaining simulation-garden target still aims for:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Current contract
|
## Current contract
|
||||||
|
|
||||||
`SimulationStateRecord` is the versioned JSON boundary for the current
|
`SimulationStateRecord` is the versioned JSON boundary for the current
|
||||||
simulation. The current world schema is v6 and captures:
|
simulation. The current world schema is v7 and captures:
|
||||||
|
|
||||||
- simulation seed, tick interval, tick count, clock remainder, and elapsed
|
- simulation seed, tick interval, tick count, clock remainder, and elapsed
|
||||||
clock ticks;
|
clock ticks;
|
||||||
@@ -18,14 +18,15 @@ simulation. The current world schema is v6 and captures:
|
|||||||
- directed relationship records with familiarity, trust, and the stable event
|
- directed relationship records with familiarity, trust, and the stable event
|
||||||
ID that last changed trust;
|
ID that last changed trust;
|
||||||
- per-NPC known-event records that reference objective event history without
|
- per-NPC known-event records that reference objective event history without
|
||||||
copying it, including first-acquisition method and communicator provenance.
|
copying it, including first-acquisition method, acquisition tick, and
|
||||||
|
historical communicator provenance.
|
||||||
|
|
||||||
The top-level identity is:
|
The top-level identity is:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema": "the_steward.simulation",
|
"schema": "the_steward.simulation",
|
||||||
"schema_version": 6
|
"schema_version": 7
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ its future random sequence.
|
|||||||
It also verifies clock remainder, resource amount/reservation/enabled
|
It also verifies clock remainder, resource amount/reservation/enabled
|
||||||
round-tripping, presentation unload/rebind, directed relationship/cause
|
round-tripping, presentation unload/rebind, directed relationship/cause
|
||||||
round-tripping, divergent known-event state, communicated provenance, and
|
round-tripping, divergent known-event state, communicated provenance, and
|
||||||
rejection of unsupported schemas.
|
deterministic retention boundaries, and rejection of unsupported schemas.
|
||||||
|
|
||||||
NPCStateRecord v2 adds the resolved travel destination and whether it is
|
NPCStateRecord v2 adds the resolved travel destination and whether it is
|
||||||
active. Nested v1 NPC records migrate explicitly with no invented active
|
active. Nested v1 NPC records migrate explicitly with no invented active
|
||||||
@@ -120,10 +121,31 @@ event.
|
|||||||
World schema v5 migrates actor-owned knowledge to `performed`; non-actor
|
World schema v5 migrates actor-owned knowledge to `performed`; non-actor
|
||||||
knowledge becomes `legacy` because v5 may contain either a proximity witness or
|
knowledge becomes `legacy` because v5 may contain either a proximity witness or
|
||||||
an older implied relationship fact. Parsing never invents that missing
|
an older implied relationship fact. Parsing never invents that missing
|
||||||
provenance. Current communicated records require a distinct existing source
|
provenance. At runtime, communication requires a distinct source who currently
|
||||||
who knows the same event through `performed` or `witnessed` acquisition. This
|
knows the event through `performed` or `witnessed` acquisition. Persisted
|
||||||
enforces the current one-hop boundary and makes validation independent of array
|
records validate that direct-method snapshot even after the source forgets.
|
||||||
ordering.
|
This enforces the current one-hop boundary and makes validation independent of
|
||||||
|
array ordering.
|
||||||
|
|
||||||
|
SimulationStateRecord v7 / KnownEventStateRecord v3 adds `acquired_tick` and a
|
||||||
|
snapshot of the direct speaker's acquisition method. Direct facts use the
|
||||||
|
objective event tick; newly communicated facts use the conversation tick. The
|
||||||
|
speaker snapshot keeps historical provenance valid even if the speaker later
|
||||||
|
forgets their own copy, while runtime communication still requires a currently
|
||||||
|
known performed/witnessed fact and remains one-hop.
|
||||||
|
|
||||||
|
World schema v6 migrates direct acquisition to the objective event tick.
|
||||||
|
Communicated and ambiguous legacy facts receive a fresh retention lease at the
|
||||||
|
saved simulation tick because their exact acquisition time was not previously
|
||||||
|
stored. The v6 source record supplies the persisted direct-method snapshot.
|
||||||
|
|
||||||
|
Importance is intentionally derived rather than duplicated: a fact is
|
||||||
|
`lasting` while it is the current trust cause for one of that knower's directed
|
||||||
|
relationships; every other fact is `recent`. Current causal facts are protected
|
||||||
|
so relationship state remains explainable. Each NPC keeps at most three recent
|
||||||
|
facts, and recent facts at least one simulated day old are removed at the next
|
||||||
|
deterministic daily-sized review. The objective event stream is never pruned by
|
||||||
|
this rule.
|
||||||
|
|
||||||
## Resource authority
|
## Resource authority
|
||||||
|
|
||||||
@@ -161,11 +183,11 @@ This phase does not yet provide:
|
|||||||
|
|
||||||
- a save-slot menu, metadata, thumbnails, autosaves, or multiple profiles;
|
- a save-slot menu, metadata, thumbnails, autosaves, or multiple profiles;
|
||||||
- migrations from any historical world schema other than the explicitly
|
- migrations from any historical world schema other than the explicitly
|
||||||
supported v1–v5 layouts;
|
supported v1–v6 layouts;
|
||||||
- player inventory or player relationship records;
|
- player inventory or player relationship records;
|
||||||
- broader relationship dimensions, line-of-sight/hearing evidence,
|
- broader relationship dimensions, line-of-sight/hearing evidence,
|
||||||
acquisition timestamps, importance/retention, memory decay, or multi-hop
|
continuous or personalized memory decay, reinforcement, false beliefs, or
|
||||||
rumours;
|
multi-hop rumours;
|
||||||
- persistence for the player transform or presentation-only scene state.
|
- persistence for the player transform or presentation-only scene state.
|
||||||
|
|
||||||
Those features should build on this boundary rather than inventing parallel
|
Those features should build on this boundary rather than inventing parallel
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ signal npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult)
|
|||||||
signal relationship_changed(relationship: RelationshipStateRecord, cause_event: EconomicEventRecord)
|
signal relationship_changed(relationship: RelationshipStateRecord, cause_event: EconomicEventRecord)
|
||||||
signal event_knowledge_changed(knower_id: int, event: EconomicEventRecord)
|
signal event_knowledge_changed(knower_id: int, event: EconomicEventRecord)
|
||||||
signal event_knowledge_transferred(speaker_id: int, listener_id: int, event: EconomicEventRecord)
|
signal event_knowledge_transferred(speaker_id: int, listener_id: int, event: EconomicEventRecord)
|
||||||
|
signal event_knowledge_forgotten(knower_id: int, event: EconomicEventRecord)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
|
|
||||||
@@ -158,6 +159,8 @@ func simulate_tick() -> void:
|
|||||||
|
|
||||||
if village_was_changed:
|
if village_was_changed:
|
||||||
village_changed.emit(village)
|
village_changed.emit(village)
|
||||||
|
if tick_count % get_knowledge_review_interval() == 0:
|
||||||
|
_maintain_event_knowledge(true)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print(village.get_summary())
|
print(village.get_summary())
|
||||||
@@ -600,13 +603,15 @@ func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bo
|
|||||||
var listener := _find_npc_by_id(listener_id)
|
var listener := _find_npc_by_id(listener_id)
|
||||||
if not _can_communicate_at_shared_activity(speaker, listener):
|
if not _can_communicate_at_shared_activity(speaker, listener):
|
||||||
return false
|
return false
|
||||||
var known_event_ids := event_knowledge_system.get_known_event_ids(speaker.id, 0)
|
var known_event_ids := event_knowledge_system.get_communicable_event_ids(
|
||||||
for index in range(known_event_ids.size() - 1, -1, -1):
|
speaker.id, _get_lasting_event_ids(speaker.id)
|
||||||
var event := event_log.get_by_id(known_event_ids[index])
|
)
|
||||||
|
for event_id in known_event_ids:
|
||||||
|
var event := event_log.get_by_id(event_id)
|
||||||
if event == null:
|
if event == null:
|
||||||
continue
|
continue
|
||||||
var learned_record := event_knowledge_system.communicate_event(
|
var learned_record := event_knowledge_system.communicate_event(
|
||||||
event, speaker.id, listener.id
|
event, speaker.id, listener.id, tick_count
|
||||||
)
|
)
|
||||||
if learned_record == null:
|
if learned_record == null:
|
||||||
continue
|
continue
|
||||||
@@ -648,13 +653,51 @@ func _can_communicate_at_shared_activity(speaker: SimNPC, listener: SimNPC) -> b
|
|||||||
func _apply_new_event_knowledge(
|
func _apply_new_event_knowledge(
|
||||||
known_event: KnownEventStateRecord, event: EconomicEventRecord
|
known_event: KnownEventStateRecord, event: EconomicEventRecord
|
||||||
) -> void:
|
) -> void:
|
||||||
event_knowledge_changed.emit(known_event.get_knower_id(), event)
|
|
||||||
var newly_informed_ids: Array[int] = [known_event.get_knower_id()]
|
var newly_informed_ids: Array[int] = [known_event.get_knower_id()]
|
||||||
var changed_relationships: Array[RelationshipStateRecord] = relationship_system.apply_event(
|
var changed_relationships: Array[RelationshipStateRecord] = relationship_system.apply_event(
|
||||||
event, npcs, newly_informed_ids
|
event, npcs, newly_informed_ids
|
||||||
)
|
)
|
||||||
|
event_knowledge_changed.emit(known_event.get_knower_id(), event)
|
||||||
for relationship in changed_relationships:
|
for relationship in changed_relationships:
|
||||||
relationship_changed.emit(relationship, event)
|
relationship_changed.emit(relationship, event)
|
||||||
|
_maintain_event_knowledge(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _maintain_event_knowledge(expire_by_age: bool) -> void:
|
||||||
|
var forgotten := event_knowledge_system.maintain_retention(
|
||||||
|
tick_count, get_knowledge_review_interval(), _get_lasting_knowledge_records(), expire_by_age
|
||||||
|
)
|
||||||
|
for known_event in forgotten:
|
||||||
|
var event := event_log.get_by_id(known_event.get_event_id())
|
||||||
|
if event != null:
|
||||||
|
event_knowledge_forgotten.emit(known_event.get_knower_id(), event)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_lasting_knowledge_records() -> Array[KnownEventStateRecord]:
|
||||||
|
var lasting: Array[KnownEventStateRecord] = []
|
||||||
|
var seen := {}
|
||||||
|
for relationship in relationship_system.get_all_sorted():
|
||||||
|
var event_id := relationship.get_last_trust_cause_event_id()
|
||||||
|
if event_id == RelationshipStateRecord.NO_CAUSE_EVENT:
|
||||||
|
continue
|
||||||
|
var record := event_knowledge_system.get_record(relationship.get_observer_id(), event_id)
|
||||||
|
if record == null:
|
||||||
|
continue
|
||||||
|
var key := "%d:%d" % [record.get_knower_id(), record.get_event_id()]
|
||||||
|
if seen.has(key):
|
||||||
|
continue
|
||||||
|
seen[key] = true
|
||||||
|
lasting.append(record)
|
||||||
|
return lasting
|
||||||
|
|
||||||
|
|
||||||
|
func _get_lasting_event_ids(npc_id: int) -> Array[int]:
|
||||||
|
var event_ids: Array[int] = []
|
||||||
|
for record in _get_lasting_knowledge_records():
|
||||||
|
if record.get_knower_id() == npc_id:
|
||||||
|
event_ids.append(record.get_event_id())
|
||||||
|
event_ids.sort()
|
||||||
|
return event_ids
|
||||||
|
|
||||||
|
|
||||||
func _find_npc_by_id(npc_id: int) -> SimNPC:
|
func _find_npc_by_id(npc_id: int) -> SimNPC:
|
||||||
@@ -695,6 +738,24 @@ func get_known_event_record(npc_id: int, event_id: int) -> KnownEventStateRecord
|
|||||||
return event_knowledge_system.get_record(npc_id, event_id)
|
return event_knowledge_system.get_record(npc_id, event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func is_known_event_lasting(npc_id: int, event_id: int) -> bool:
|
||||||
|
return event_id in _get_lasting_event_ids(npc_id)
|
||||||
|
|
||||||
|
|
||||||
|
func get_memory_summary(npc_id: int) -> Dictionary:
|
||||||
|
var total := event_knowledge_system.get_known_event_ids(npc_id, 0).size()
|
||||||
|
var lasting := _get_lasting_event_ids(npc_id).size()
|
||||||
|
return {"lasting": lasting, "recent": maxi(total - lasting, 0)}
|
||||||
|
|
||||||
|
|
||||||
|
func get_knowledge_review_interval() -> int:
|
||||||
|
var active_cycle_duration := (
|
||||||
|
clock.cycle_duration_seconds if clock != null else cycle_duration_seconds
|
||||||
|
)
|
||||||
|
var active_tick_interval := clock.tick_interval if clock != null else tick_interval
|
||||||
|
return maxi(roundi(active_cycle_duration / maxf(active_tick_interval, 0.0001)), 1)
|
||||||
|
|
||||||
|
|
||||||
func get_current_speed() -> String:
|
func get_current_speed() -> String:
|
||||||
return "%.2fx" % SPEED_LEVELS[speed_index]
|
return "%.2fx" % SPEED_LEVELS[speed_index]
|
||||||
|
|
||||||
@@ -1018,6 +1079,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
|||||||
npcs.append(npc_record.restore(debug_logs))
|
npcs.append(npc_record.restore(debug_logs))
|
||||||
relationship_system.restore(record.relationships)
|
relationship_system.restore(record.relationships)
|
||||||
event_knowledge_system.restore(record.event_knowledge)
|
event_knowledge_system.restore(record.event_knowledge)
|
||||||
|
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
||||||
|
|
||||||
wander_random_sources.clear()
|
wander_random_sources.clear()
|
||||||
var wander_streams: Array = record.simulation["wander_random_streams"]
|
var wander_streams: Array = record.simulation["wander_random_streams"]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class_name EventKnowledgeSystem
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const WITNESS_RADIUS := 10.0
|
const WITNESS_RADIUS := 10.0
|
||||||
|
const MAX_RECENT_FACTS_PER_NPC := 3
|
||||||
|
|
||||||
var known_events: Dictionary = {}
|
var known_events: Dictionary = {}
|
||||||
|
|
||||||
@@ -14,7 +15,10 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
|||||||
if actor == null:
|
if actor == null:
|
||||||
return learned
|
return learned
|
||||||
var event_id := int(event.data["event_id"])
|
var event_id := int(event.data["event_id"])
|
||||||
var actor_record := _remember(actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
var acquired_tick := int(event.data["tick"])
|
||||||
|
var actor_record := _remember(
|
||||||
|
actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, acquired_tick
|
||||||
|
)
|
||||||
if actor_record != null:
|
if actor_record != null:
|
||||||
learned.append(actor_record)
|
learned.append(actor_record)
|
||||||
var witness_radius_squared := WITNESS_RADIUS * WITNESS_RADIUS
|
var witness_radius_squared := WITNESS_RADIUS * WITNESS_RADIUS
|
||||||
@@ -25,7 +29,7 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
|||||||
if npc.position.distance_squared_to(event_position) > witness_radius_squared:
|
if npc.position.distance_squared_to(event_position) > witness_radius_squared:
|
||||||
continue
|
continue
|
||||||
var witness_record := _remember(
|
var witness_record := _remember(
|
||||||
npc.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
npc.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, acquired_tick
|
||||||
)
|
)
|
||||||
if witness_record != null:
|
if witness_record != null:
|
||||||
learned.append(witness_record)
|
learned.append(witness_record)
|
||||||
@@ -34,7 +38,7 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
|||||||
|
|
||||||
|
|
||||||
func communicate_event(
|
func communicate_event(
|
||||||
event: EconomicEventRecord, speaker_id: int, listener_id: int
|
event: EconomicEventRecord, speaker_id: int, listener_id: int, acquired_tick: int
|
||||||
) -> KnownEventStateRecord:
|
) -> KnownEventStateRecord:
|
||||||
if (
|
if (
|
||||||
event == null
|
event == null
|
||||||
@@ -56,7 +60,12 @@ func communicate_event(
|
|||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
return _remember(
|
return _remember(
|
||||||
listener_id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, speaker_id
|
listener_id,
|
||||||
|
event_id,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
acquired_tick,
|
||||||
|
speaker_id,
|
||||||
|
speaker_record.get_acquisition_method()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,11 +87,14 @@ func get_knowers(event_id: int) -> Array[int]:
|
|||||||
|
|
||||||
|
|
||||||
func get_known_event_ids(knower_id: int, max_count: int = 3) -> Array[int]:
|
func get_known_event_ids(knower_id: int, max_count: int = 3) -> Array[int]:
|
||||||
var event_ids: Array[int] = []
|
var records: Array[KnownEventStateRecord] = []
|
||||||
for record in known_events.values():
|
for record in known_events.values():
|
||||||
if record.get_knower_id() == knower_id:
|
if record.get_knower_id() == knower_id:
|
||||||
event_ids.append(record.get_event_id())
|
records.append(record)
|
||||||
event_ids.sort()
|
records.sort_custom(_sort_by_acquisition)
|
||||||
|
var event_ids: Array[int] = []
|
||||||
|
for record in records:
|
||||||
|
event_ids.append(record.get_event_id())
|
||||||
if max_count <= 0 or event_ids.size() <= max_count:
|
if max_count <= 0 or event_ids.size() <= max_count:
|
||||||
return event_ids
|
return event_ids
|
||||||
var recent_ids: Array[int] = []
|
var recent_ids: Array[int] = []
|
||||||
@@ -91,6 +103,67 @@ func get_known_event_ids(knower_id: int, max_count: int = 3) -> Array[int]:
|
|||||||
return recent_ids
|
return recent_ids
|
||||||
|
|
||||||
|
|
||||||
|
func get_communicable_event_ids(knower_id: int, lasting_event_ids: Array[int]) -> Array[int]:
|
||||||
|
var ranked: Array[Dictionary] = []
|
||||||
|
for record in known_events.values():
|
||||||
|
if record.get_knower_id() != knower_id:
|
||||||
|
continue
|
||||||
|
if record.get_acquisition_method() not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS:
|
||||||
|
continue
|
||||||
|
(
|
||||||
|
ranked
|
||||||
|
. append(
|
||||||
|
{
|
||||||
|
"event_id": record.get_event_id(),
|
||||||
|
"acquired_tick": record.get_acquired_tick(),
|
||||||
|
"lasting": record.get_event_id() in lasting_event_ids,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ranked.sort_custom(_sort_communicable)
|
||||||
|
var event_ids: Array[int] = []
|
||||||
|
for item in ranked:
|
||||||
|
event_ids.append(int(item["event_id"]))
|
||||||
|
return event_ids
|
||||||
|
|
||||||
|
|
||||||
|
func maintain_retention(
|
||||||
|
current_tick: int,
|
||||||
|
max_recent_age: int,
|
||||||
|
lasting_records: Array[KnownEventStateRecord],
|
||||||
|
expire_by_age: bool
|
||||||
|
) -> Array[KnownEventStateRecord]:
|
||||||
|
var lasting_keys := {}
|
||||||
|
for record in lasting_records:
|
||||||
|
lasting_keys[_key(record.get_knower_id(), record.get_event_id())] = true
|
||||||
|
var removal_keys := {}
|
||||||
|
var recent_by_knower := {}
|
||||||
|
for record in known_events.values():
|
||||||
|
var key := _key(record.get_knower_id(), record.get_event_id())
|
||||||
|
if lasting_keys.has(key):
|
||||||
|
continue
|
||||||
|
if expire_by_age and current_tick - record.get_acquired_tick() >= max_recent_age:
|
||||||
|
removal_keys[key] = true
|
||||||
|
continue
|
||||||
|
var recent: Array = recent_by_knower.get(record.get_knower_id(), [])
|
||||||
|
recent.append(record)
|
||||||
|
recent_by_knower[record.get_knower_id()] = recent
|
||||||
|
for recent in recent_by_knower.values():
|
||||||
|
recent.sort_custom(_sort_by_acquisition)
|
||||||
|
var excess: int = recent.size() - MAX_RECENT_FACTS_PER_NPC
|
||||||
|
for index in range(maxi(excess, 0)):
|
||||||
|
var record := recent[index] as KnownEventStateRecord
|
||||||
|
removal_keys[_key(record.get_knower_id(), record.get_event_id())] = true
|
||||||
|
var forgotten: Array[KnownEventStateRecord] = []
|
||||||
|
for record in get_all_sorted():
|
||||||
|
var key := _key(record.get_knower_id(), record.get_event_id())
|
||||||
|
if not removal_keys.has(key):
|
||||||
|
continue
|
||||||
|
known_events.erase(key)
|
||||||
|
forgotten.append(record)
|
||||||
|
return forgotten
|
||||||
|
|
||||||
|
|
||||||
func get_all_sorted() -> Array[KnownEventStateRecord]:
|
func get_all_sorted() -> Array[KnownEventStateRecord]:
|
||||||
var records: Array[KnownEventStateRecord] = []
|
var records: Array[KnownEventStateRecord] = []
|
||||||
for record in known_events.values():
|
for record in known_events.values():
|
||||||
@@ -109,13 +182,20 @@ func _remember(
|
|||||||
knower_id: int,
|
knower_id: int,
|
||||||
event_id: int,
|
event_id: int,
|
||||||
acquisition_method: StringName,
|
acquisition_method: StringName,
|
||||||
source_npc_id: int = KnownEventStateRecord.NO_SOURCE_NPC_ID
|
acquired_tick: int,
|
||||||
|
source_npc_id: int = KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||||
|
source_acquisition_method: StringName = &""
|
||||||
) -> KnownEventStateRecord:
|
) -> KnownEventStateRecord:
|
||||||
var key := _key(knower_id, event_id)
|
var key := _key(knower_id, event_id)
|
||||||
if known_events.has(key):
|
if known_events.has(key):
|
||||||
return null
|
return null
|
||||||
var record := KnownEventStateRecord.create(
|
var record := KnownEventStateRecord.create(
|
||||||
knower_id, event_id, acquisition_method, source_npc_id
|
knower_id,
|
||||||
|
event_id,
|
||||||
|
acquisition_method,
|
||||||
|
source_npc_id,
|
||||||
|
acquired_tick,
|
||||||
|
source_acquisition_method
|
||||||
)
|
)
|
||||||
known_events[key] = record
|
known_events[key] = record
|
||||||
return record
|
return record
|
||||||
@@ -144,3 +224,19 @@ static func _sort_records(first: KnownEventStateRecord, second: KnownEventStateR
|
|||||||
if first.get_knower_id() != second.get_knower_id():
|
if first.get_knower_id() != second.get_knower_id():
|
||||||
return first.get_knower_id() < second.get_knower_id()
|
return first.get_knower_id() < second.get_knower_id()
|
||||||
return first.get_event_id() < second.get_event_id()
|
return first.get_event_id() < second.get_event_id()
|
||||||
|
|
||||||
|
|
||||||
|
static func _sort_by_acquisition(
|
||||||
|
first: KnownEventStateRecord, second: KnownEventStateRecord
|
||||||
|
) -> bool:
|
||||||
|
if first.get_acquired_tick() != second.get_acquired_tick():
|
||||||
|
return first.get_acquired_tick() < second.get_acquired_tick()
|
||||||
|
return first.get_event_id() < second.get_event_id()
|
||||||
|
|
||||||
|
|
||||||
|
static func _sort_communicable(first: Dictionary, second: Dictionary) -> bool:
|
||||||
|
if bool(first["lasting"]) != bool(second["lasting"]):
|
||||||
|
return bool(first["lasting"])
|
||||||
|
if int(first["acquired_tick"]) != int(second["acquired_tick"]):
|
||||||
|
return int(first["acquired_tick"]) > int(second["acquired_tick"])
|
||||||
|
return int(first["event_id"]) > int(second["event_id"])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class_name KnownEventStateRecord
|
class_name KnownEventStateRecord
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_VERSION := 2
|
const SCHEMA_VERSION := 3
|
||||||
const NO_SOURCE_NPC_ID := -1
|
const NO_SOURCE_NPC_ID := -1
|
||||||
|
|
||||||
const VALID_ACQUISITION_METHODS := [
|
const VALID_ACQUISITION_METHODS := [
|
||||||
@@ -10,6 +10,10 @@ const VALID_ACQUISITION_METHODS := [
|
|||||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||||
]
|
]
|
||||||
|
const DIRECT_ACQUISITION_METHODS := [
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
]
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
|
|
||||||
@@ -22,7 +26,9 @@ static func create(
|
|||||||
knower_id: int,
|
knower_id: int,
|
||||||
event_id: int,
|
event_id: int,
|
||||||
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||||
source_npc_id: int = NO_SOURCE_NPC_ID
|
source_npc_id: int = NO_SOURCE_NPC_ID,
|
||||||
|
acquired_tick: int = 0,
|
||||||
|
source_acquisition_method: StringName = &""
|
||||||
) -> KnownEventStateRecord:
|
) -> KnownEventStateRecord:
|
||||||
return (
|
return (
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
@@ -33,6 +39,8 @@ static func create(
|
|||||||
"event_id": event_id,
|
"event_id": event_id,
|
||||||
"acquisition_method": String(acquisition_method),
|
"acquisition_method": String(acquisition_method),
|
||||||
"source_npc_id": source_npc_id,
|
"source_npc_id": source_npc_id,
|
||||||
|
"acquired_tick": acquired_tick,
|
||||||
|
"source_acquisition_method": String(source_acquisition_method),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -41,22 +49,45 @@ static func create(
|
|||||||
static func from_dictionary(record_data: Dictionary) -> KnownEventStateRecord:
|
static func from_dictionary(record_data: Dictionary) -> KnownEventStateRecord:
|
||||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
if not record_data.has_all(["knower_id", "event_id", "acquisition_method", "source_npc_id"]):
|
if not (
|
||||||
|
record_data
|
||||||
|
. has_all(
|
||||||
|
[
|
||||||
|
"knower_id",
|
||||||
|
"event_id",
|
||||||
|
"acquisition_method",
|
||||||
|
"source_npc_id",
|
||||||
|
"acquired_tick",
|
||||||
|
"source_acquisition_method",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
):
|
||||||
return null
|
return null
|
||||||
var knower_id := int(record_data["knower_id"])
|
var knower_id := int(record_data["knower_id"])
|
||||||
var event_id := int(record_data["event_id"])
|
var event_id := int(record_data["event_id"])
|
||||||
var acquisition_method := StringName(record_data["acquisition_method"])
|
var acquisition_method := StringName(record_data["acquisition_method"])
|
||||||
var source_npc_id := int(record_data["source_npc_id"])
|
var source_npc_id := int(record_data["source_npc_id"])
|
||||||
if knower_id < 0 or event_id < 0:
|
var acquired_tick := int(record_data["acquired_tick"])
|
||||||
|
var source_acquisition_method := StringName(record_data["source_acquisition_method"])
|
||||||
|
if knower_id < 0 or event_id < 0 or acquired_tick < 0:
|
||||||
return null
|
return null
|
||||||
if acquisition_method not in VALID_ACQUISITION_METHODS:
|
if acquisition_method not in VALID_ACQUISITION_METHODS:
|
||||||
return null
|
return null
|
||||||
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
if source_npc_id < 0 or source_npc_id == knower_id:
|
if source_npc_id < 0 or source_npc_id == knower_id:
|
||||||
return null
|
return null
|
||||||
elif source_npc_id != NO_SOURCE_NPC_ID:
|
if source_acquisition_method not in DIRECT_ACQUISITION_METHODS:
|
||||||
|
return null
|
||||||
|
elif source_npc_id != NO_SOURCE_NPC_ID or not source_acquisition_method.is_empty():
|
||||||
return null
|
return null
|
||||||
return create(knower_id, event_id, acquisition_method, source_npc_id)
|
return create(
|
||||||
|
knower_id,
|
||||||
|
event_id,
|
||||||
|
acquisition_method,
|
||||||
|
source_npc_id,
|
||||||
|
acquired_tick,
|
||||||
|
source_acquisition_method
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func get_knower_id() -> int:
|
func get_knower_id() -> int:
|
||||||
@@ -75,5 +106,13 @@ func get_source_npc_id() -> int:
|
|||||||
return int(data["source_npc_id"])
|
return int(data["source_npc_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_acquired_tick() -> int:
|
||||||
|
return int(data["acquired_tick"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_source_acquisition_method() -> StringName:
|
||||||
|
return StringName(data["source_acquisition_method"])
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
return data.duplicate(true)
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ class_name SimulationStateRecord
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_NAME := "the_steward.simulation"
|
const SCHEMA_NAME := "the_steward.simulation"
|
||||||
const SCHEMA_VERSION := 6
|
const SCHEMA_VERSION := 7
|
||||||
const LEGACY_SCHEMA_VERSION := 1
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||||
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
|
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
|
||||||
const PREVIOUS_SCHEMA_VERSION := 5
|
const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
|
||||||
|
const PREVIOUS_SCHEMA_VERSION := 6
|
||||||
|
|
||||||
var simulation: Dictionary
|
var simulation: Dictionary
|
||||||
var village: VillageStateRecord
|
var village: VillageStateRecord
|
||||||
@@ -76,6 +77,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
EVENT_LEGACY_SCHEMA_VERSION,
|
EVENT_LEGACY_SCHEMA_VERSION,
|
||||||
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||||
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
||||||
|
PROVENANCE_LEGACY_SCHEMA_VERSION,
|
||||||
PREVIOUS_SCHEMA_VERSION,
|
PREVIOUS_SCHEMA_VERSION,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
@@ -114,6 +116,23 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
]
|
]
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
|
var saved_tick_interval := float(simulation_data["tick_interval"])
|
||||||
|
var saved_tick_count := int(simulation_data["tick_count"])
|
||||||
|
var saved_clock_accumulator := float(simulation_data["clock_accumulator"])
|
||||||
|
var saved_clock_elapsed_ticks := int(simulation_data["clock_elapsed_ticks"])
|
||||||
|
if (
|
||||||
|
not is_finite(saved_tick_interval)
|
||||||
|
or saved_tick_interval <= 0.0
|
||||||
|
or saved_tick_count < 0
|
||||||
|
or not is_finite(saved_clock_accumulator)
|
||||||
|
or saved_clock_accumulator < 0.0
|
||||||
|
or saved_clock_elapsed_ticks < 0
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
if simulation_data.has("cycle_duration_seconds"):
|
||||||
|
var saved_cycle_duration := float(simulation_data["cycle_duration_seconds"])
|
||||||
|
if not is_finite(saved_cycle_duration) or saved_cycle_duration <= 0.0:
|
||||||
|
return null
|
||||||
var wander_streams = simulation_data["wander_random_streams"]
|
var wander_streams = simulation_data["wander_random_streams"]
|
||||||
if not wander_streams is Array:
|
if not wander_streams is Array:
|
||||||
return null
|
return null
|
||||||
@@ -209,7 +228,6 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
var knowledge_keys := {}
|
var knowledge_keys := {}
|
||||||
var knowledge_records_by_key := {}
|
|
||||||
for item in knowledge_data:
|
for item in knowledge_data:
|
||||||
if not item is Dictionary:
|
if not item is Dictionary:
|
||||||
return null
|
return null
|
||||||
@@ -224,11 +242,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
if knowledge_keys.has(knowledge_key):
|
if knowledge_keys.has(knowledge_key):
|
||||||
return null
|
return null
|
||||||
knowledge_keys[knowledge_key] = true
|
knowledge_keys[knowledge_key] = true
|
||||||
knowledge_records_by_key[knowledge_key] = known_event_record
|
|
||||||
record.event_knowledge.append(known_event_record)
|
record.event_knowledge.append(known_event_record)
|
||||||
for known_event_record in record.event_knowledge:
|
for known_event_record in record.event_knowledge:
|
||||||
if not _is_valid_knowledge_provenance(
|
if not _is_valid_knowledge_provenance(
|
||||||
known_event_record, npc_ids, event_records_by_id, knowledge_records_by_key
|
known_event_record, npc_ids, event_records_by_id, int(record.simulation["tick_count"])
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -279,7 +296,7 @@ static func _is_valid_knowledge_provenance(
|
|||||||
known_event: KnownEventStateRecord,
|
known_event: KnownEventStateRecord,
|
||||||
npc_ids: Dictionary,
|
npc_ids: Dictionary,
|
||||||
event_records_by_id: Dictionary,
|
event_records_by_id: Dictionary,
|
||||||
knowledge_records_by_key: Dictionary
|
current_tick: int
|
||||||
) -> bool:
|
) -> bool:
|
||||||
var event := event_records_by_id.get(known_event.get_event_id()) as EconomicEventRecord
|
var event := event_records_by_id.get(known_event.get_event_id()) as EconomicEventRecord
|
||||||
if event == null:
|
if event == null:
|
||||||
@@ -287,6 +304,10 @@ static func _is_valid_knowledge_provenance(
|
|||||||
var method := known_event.get_acquisition_method()
|
var method := known_event.get_acquisition_method()
|
||||||
var knower_id := known_event.get_knower_id()
|
var knower_id := known_event.get_knower_id()
|
||||||
var actor_id := int(event.data["actor_id"])
|
var actor_id := int(event.data["actor_id"])
|
||||||
|
var event_tick := int(event.data["tick"])
|
||||||
|
var acquired_tick := known_event.get_acquired_tick()
|
||||||
|
if acquired_tick < event_tick or acquired_tick > current_tick:
|
||||||
|
return false
|
||||||
if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
||||||
return true
|
return true
|
||||||
if (
|
if (
|
||||||
@@ -297,29 +318,22 @@ static func _is_valid_knowledge_provenance(
|
|||||||
return false
|
return false
|
||||||
match method:
|
match method:
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||||
return knower_id == actor_id
|
return knower_id == actor_id and acquired_tick == event_tick
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
||||||
return knower_id != actor_id
|
return knower_id != actor_id and acquired_tick == event_tick
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
if knower_id == actor_id:
|
if knower_id == actor_id:
|
||||||
return false
|
return false
|
||||||
var source_npc_id := known_event.get_source_npc_id()
|
var source_npc_id := known_event.get_source_npc_id()
|
||||||
if not npc_ids.has(source_npc_id):
|
if not npc_ids.has(source_npc_id):
|
||||||
return false
|
return false
|
||||||
var source_record := (
|
var source_method := known_event.get_source_acquisition_method()
|
||||||
knowledge_records_by_key.get("%d:%d" % [source_npc_id, known_event.get_event_id()])
|
if (
|
||||||
as KnownEventStateRecord
|
(source_method == SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||||
)
|
!= (source_npc_id == actor_id)
|
||||||
return (
|
):
|
||||||
source_record != null
|
return false
|
||||||
and (
|
return true
|
||||||
source_record.get_acquisition_method()
|
|
||||||
in [
|
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
@@ -368,10 +382,13 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
|||||||
]
|
]
|
||||||
):
|
):
|
||||||
migrated["event_knowledge"] = _migrate_relationship_causes(
|
migrated["event_knowledge"] = _migrate_relationship_causes(
|
||||||
migrated.get("relationships", [])
|
migrated.get("relationships", []),
|
||||||
|
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
|
||||||
)
|
)
|
||||||
migrated["event_knowledge"] = _migrate_known_event_provenance(
|
migrated["event_knowledge"] = _migrate_known_event_retention(
|
||||||
migrated.get("event_knowledge", []), migrated.get("economic_events", [])
|
migrated.get("event_knowledge", []),
|
||||||
|
migrated.get("economic_events", []),
|
||||||
|
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
|
||||||
)
|
)
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
@@ -417,7 +434,9 @@ static func _relationship_dictionary_sort(first: Dictionary, second: Dictionary)
|
|||||||
return int(first["subject_id"]) < int(second["subject_id"])
|
return int(first["subject_id"]) < int(second["subject_id"])
|
||||||
|
|
||||||
|
|
||||||
static func _migrate_relationship_causes(relationship_data: Variant) -> Array[Dictionary]:
|
static func _migrate_relationship_causes(
|
||||||
|
relationship_data: Variant, migration_tick: int
|
||||||
|
) -> Array[Dictionary]:
|
||||||
var migrated_knowledge: Array[Dictionary] = []
|
var migrated_knowledge: Array[Dictionary] = []
|
||||||
var known_keys := {}
|
var known_keys := {}
|
||||||
if not relationship_data is Array:
|
if not relationship_data is Array:
|
||||||
@@ -438,7 +457,17 @@ static func _migrate_relationship_causes(relationship_data: Variant) -> Array[Di
|
|||||||
continue
|
continue
|
||||||
known_keys[knowledge_key] = true
|
known_keys[knowledge_key] = true
|
||||||
migrated_knowledge.append(
|
migrated_knowledge.append(
|
||||||
KnownEventStateRecord.create(observer_id, cause_event_id).to_dictionary()
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(
|
||||||
|
observer_id,
|
||||||
|
cause_event_id,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||||
|
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||||
|
migration_tick
|
||||||
|
)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
||||||
return migrated_knowledge
|
return migrated_knowledge
|
||||||
@@ -450,17 +479,21 @@ static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) ->
|
|||||||
return int(first["event_id"]) < int(second["event_id"])
|
return int(first["event_id"]) < int(second["event_id"])
|
||||||
|
|
||||||
|
|
||||||
static func _migrate_known_event_provenance(knowledge_data: Variant, event_data: Variant) -> Array:
|
static func _migrate_known_event_retention(
|
||||||
|
knowledge_data: Variant, event_data: Variant, migration_tick: int
|
||||||
|
) -> Array:
|
||||||
if not knowledge_data is Array:
|
if not knowledge_data is Array:
|
||||||
return []
|
return []
|
||||||
var actor_by_event_id := {}
|
var event_by_id := {}
|
||||||
if event_data is Array:
|
if event_data is Array:
|
||||||
for event_item in event_data:
|
for event_item in event_data:
|
||||||
if not event_item is Dictionary:
|
if not event_item is Dictionary:
|
||||||
continue
|
continue
|
||||||
actor_by_event_id[int(event_item.get("event_id", -1))] = int(
|
event_by_id[int(event_item.get("event_id", -1))] = event_item
|
||||||
event_item.get("actor_id", -1)
|
var old_knowledge_by_key := {}
|
||||||
)
|
for item in knowledge_data:
|
||||||
|
if item is Dictionary and item.has_all(["knower_id", "event_id"]):
|
||||||
|
old_knowledge_by_key["%d:%d" % [int(item["knower_id"]), int(item["event_id"])]] = item
|
||||||
var migrated_knowledge: Array = []
|
var migrated_knowledge: Array = []
|
||||||
var can_sort := true
|
var can_sort := true
|
||||||
for item in knowledge_data:
|
for item in knowledge_data:
|
||||||
@@ -471,17 +504,48 @@ static func _migrate_known_event_provenance(knowledge_data: Variant, event_data:
|
|||||||
if int(item.get("schema_version", -1)) == KnownEventStateRecord.SCHEMA_VERSION:
|
if int(item.get("schema_version", -1)) == KnownEventStateRecord.SCHEMA_VERSION:
|
||||||
migrated_knowledge.append(item.duplicate(true))
|
migrated_knowledge.append(item.duplicate(true))
|
||||||
continue
|
continue
|
||||||
if int(item.get("schema_version", -1)) != 1 or not item.has_all(["knower_id", "event_id"]):
|
var nested_version := int(item.get("schema_version", -1))
|
||||||
|
if nested_version not in [1, 2] or not item.has_all(["knower_id", "event_id"]):
|
||||||
migrated_knowledge.append(item.duplicate(true))
|
migrated_knowledge.append(item.duplicate(true))
|
||||||
can_sort = false
|
can_sort = false
|
||||||
continue
|
continue
|
||||||
var knower_id := int(item["knower_id"])
|
var knower_id := int(item["knower_id"])
|
||||||
var event_id := int(item["event_id"])
|
var event_id := int(item["event_id"])
|
||||||
var acquisition_method := SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
var event_item := event_by_id.get(event_id, {}) as Dictionary
|
||||||
if int(actor_by_event_id.get(event_id, -2)) == knower_id:
|
var event_tick := int(event_item.get("tick", migration_tick))
|
||||||
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
var event_actor_id := int(event_item.get("actor_id", -2))
|
||||||
|
var acquisition_method := StringName(
|
||||||
|
item.get("acquisition_method", SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY)
|
||||||
|
)
|
||||||
|
var source_npc_id := int(item.get("source_npc_id", KnownEventStateRecord.NO_SOURCE_NPC_ID))
|
||||||
|
if nested_version == 1:
|
||||||
|
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
||||||
|
source_npc_id = KnownEventStateRecord.NO_SOURCE_NPC_ID
|
||||||
|
if event_actor_id == knower_id:
|
||||||
|
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||||
|
var acquired_tick := migration_tick
|
||||||
|
if acquisition_method in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS:
|
||||||
|
acquired_tick = event_tick
|
||||||
|
var source_acquisition_method := &""
|
||||||
|
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
|
var source_item: Variant = old_knowledge_by_key.get(
|
||||||
|
"%d:%d" % [source_npc_id, event_id], {}
|
||||||
|
)
|
||||||
|
if source_item is Dictionary:
|
||||||
|
source_acquisition_method = StringName(source_item.get("acquisition_method", &""))
|
||||||
migrated_knowledge.append(
|
migrated_knowledge.append(
|
||||||
KnownEventStateRecord.create(knower_id, event_id, acquisition_method).to_dictionary()
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(
|
||||||
|
knower_id,
|
||||||
|
event_id,
|
||||||
|
acquisition_method,
|
||||||
|
source_npc_id,
|
||||||
|
acquired_tick,
|
||||||
|
source_acquisition_method
|
||||||
|
)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if can_sort:
|
if can_sort:
|
||||||
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func _run() -> void:
|
|||||||
contributor.position = Vector3.ZERO
|
contributor.position = Vector3.ZERO
|
||||||
witness.position = Vector3(4.0, 0.0, 0.0)
|
witness.position = Vector3(4.0, 0.0, 0.0)
|
||||||
witness.hunger = 85.0
|
witness.hunger = 85.0
|
||||||
|
listener.hunger = 20.0
|
||||||
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
@@ -64,7 +65,8 @@ func _run() -> void:
|
|||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
||||||
and "(witnessed)" in inspector_label.text
|
and "witnessed · lasting" in inspector_label.text
|
||||||
|
and "Memory: 1 lasting · 0 recent" in inspector_label.text
|
||||||
and "Relationship: %s" % contributor.npc_name in inspector_label.text
|
and "Relationship: %s" % contributor.npc_name in inspector_label.text
|
||||||
and "Because:" in inspector_label.text
|
and "Because:" in inspector_label.text
|
||||||
),
|
),
|
||||||
@@ -86,7 +88,8 @@ func _run() -> void:
|
|||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
||||||
and "(heard from %s)" % witness.npc_name in inspector_label.text
|
and "heard from %s · recent" % witness.npc_name in inspector_label.text
|
||||||
|
and "Memory: 0 lasting · 1 recent" in inspector_label.text
|
||||||
),
|
),
|
||||||
"NPC inspector should identify who communicated a known fact"
|
"NPC inspector should identify who communicated a known fact"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var forgotten_pairs: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var manager := _create_manager()
|
||||||
|
var contributor: SimNPC = manager.npcs[0]
|
||||||
|
var witness: SimNPC = manager.npcs[1]
|
||||||
|
var lasting_listener: SimNPC = manager.npcs[2]
|
||||||
|
var recent_listener: SimNPC = manager.npcs[3]
|
||||||
|
for npc in manager.npcs:
|
||||||
|
npc.position = Vector3(40.0 + npc.id * 2.0, 0.0, 0.0)
|
||||||
|
contributor.position = Vector3.ZERO
|
||||||
|
witness.position = Vector3(4.0, 0.0, 0.0)
|
||||||
|
lasting_listener.position = Vector3(20.0, 0.0, 0.0)
|
||||||
|
recent_listener.position = Vector3(22.0, 0.0, 0.0)
|
||||||
|
witness.hunger = 20.0
|
||||||
|
lasting_listener.hunger = 85.0
|
||||||
|
recent_listener.hunger = 20.0
|
||||||
|
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 2.0)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(
|
||||||
|
manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD), 2.0
|
||||||
|
),
|
||||||
|
"Retention should begin from a real completed food deposit"
|
||||||
|
)
|
||||||
|
var deposit_event := _find_deposit_event(manager, contributor.id)
|
||||||
|
_check(deposit_event != null, "The objective deposit fact should exist")
|
||||||
|
if deposit_event == null:
|
||||||
|
manager.free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
var event_id := int(deposit_event.data["event_id"])
|
||||||
|
|
||||||
|
_prepare_worker(witness, Vector3(20.0, 0.0, 0.0), true)
|
||||||
|
_prepare_worker(lasting_listener, Vector3(21.0, 0.0, 0.0), false)
|
||||||
|
manager.notify_npc_arrived(lasting_listener.id)
|
||||||
|
_prepare_worker(recent_listener, Vector3(22.0, 0.0, 0.0), false)
|
||||||
|
manager.notify_npc_arrived(recent_listener.id)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.npc_knows_event(lasting_listener.id, event_id)
|
||||||
|
and manager.npc_knows_event(recent_listener.id, event_id)
|
||||||
|
),
|
||||||
|
"The direct witness should communicate the same fact to both listeners"
|
||||||
|
)
|
||||||
|
var lasting_record: KnownEventStateRecord = manager.get_known_event_record(
|
||||||
|
lasting_listener.id, event_id
|
||||||
|
)
|
||||||
|
var recent_record: KnownEventStateRecord = manager.get_known_event_record(
|
||||||
|
recent_listener.id, event_id
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
lasting_record != null
|
||||||
|
and recent_record != null
|
||||||
|
and lasting_record.get_acquired_tick() == manager.tick_count
|
||||||
|
and recent_record.get_acquired_tick() == manager.tick_count
|
||||||
|
and (
|
||||||
|
lasting_record.get_source_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Communicated memories should retain acquisition time and direct-source proof"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
manager.is_known_event_lasting(lasting_listener.id, event_id),
|
||||||
|
"A fact that changed trust should become a lasting relationship cause"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not manager.is_known_event_lasting(recent_listener.id, event_id),
|
||||||
|
"A fact without a social consequence should remain recent"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.get_memory_summary(lasting_listener.id) == {"lasting": 1, "recent": 0}
|
||||||
|
and manager.get_memory_summary(recent_listener.id) == {"lasting": 0, "recent": 1}
|
||||||
|
),
|
||||||
|
"Memory summaries should distinguish lasting and recent knowledge"
|
||||||
|
)
|
||||||
|
|
||||||
|
var objective_event_count: int = manager.economic_events.size()
|
||||||
|
var objective_next_event_id: int = manager.next_event_id
|
||||||
|
for npc in manager.npcs:
|
||||||
|
npc.set_task(SimulationIds.ACTION_REST, 1000.0)
|
||||||
|
npc.target_id = &"retention_hold"
|
||||||
|
npc.start_working()
|
||||||
|
var review_interval: int = manager.get_knowledge_review_interval()
|
||||||
|
for _step in range(review_interval - 1):
|
||||||
|
manager.simulate_tick()
|
||||||
|
_check(
|
||||||
|
manager.npc_knows_event(recent_listener.id, event_id),
|
||||||
|
"Recent knowledge should survive until the exact daily review boundary"
|
||||||
|
)
|
||||||
|
|
||||||
|
var saved_json: String = manager.serialize_state()
|
||||||
|
var boundary_data = JSON.parse_string(saved_json)
|
||||||
|
boundary_data["simulation"]["tick_count"] = review_interval
|
||||||
|
var boundary_restored := _create_manager(998)
|
||||||
|
_check(
|
||||||
|
boundary_restored.restore_state_from_json(JSON.stringify(boundary_data)),
|
||||||
|
"A save loaded on a review boundary should restore successfully"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
boundary_restored.npc_knows_event(lasting_listener.id, event_id)
|
||||||
|
and not boundary_restored.npc_knows_event(recent_listener.id, event_id)
|
||||||
|
and not boundary_restored.npc_knows_event(witness.id, event_id)
|
||||||
|
and not boundary_restored.npc_knows_event(contributor.id, event_id)
|
||||||
|
),
|
||||||
|
"Loading exactly on a review boundary should immediately expire stale recent facts"
|
||||||
|
)
|
||||||
|
var restored := _create_manager(999)
|
||||||
|
_check(
|
||||||
|
restored.restore_state_from_json(saved_json),
|
||||||
|
"Retention metadata should restore immediately before review"
|
||||||
|
)
|
||||||
|
manager.event_knowledge_forgotten.connect(_on_event_knowledge_forgotten)
|
||||||
|
manager.simulate_tick()
|
||||||
|
restored.simulate_tick()
|
||||||
|
_check(
|
||||||
|
restored.get_state_checksum() == manager.get_state_checksum(),
|
||||||
|
"Original and restored simulations should forget the same facts at the same tick"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.npc_knows_event(lasting_listener.id, event_id)
|
||||||
|
and not manager.npc_knows_event(recent_listener.id, event_id)
|
||||||
|
and not manager.npc_knows_event(witness.id, event_id)
|
||||||
|
and not manager.npc_knows_event(contributor.id, event_id)
|
||||||
|
),
|
||||||
|
"Daily review should keep only the lasting causal memory"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
forgotten_pairs == ["0:%d" % event_id, "1:%d" % event_id, "3:%d" % event_id],
|
||||||
|
"Forgetting signals should be emitted once in stable knower/event order"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.economic_events.size() == objective_event_count
|
||||||
|
and manager.next_event_id == objective_next_event_id
|
||||||
|
),
|
||||||
|
"Forgetting knowledge must not prune or replay objective event history"
|
||||||
|
)
|
||||||
|
var lasting_relationship: RelationshipStateRecord = (
|
||||||
|
manager.relationship_system.get_relationship(lasting_listener.id, contributor.id)
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
lasting_relationship != null
|
||||||
|
and is_equal_approx(lasting_relationship.get_trust(), 0.65)
|
||||||
|
and lasting_relationship.get_last_trust_cause_event_id() == event_id
|
||||||
|
),
|
||||||
|
"Forgetting routine facts must preserve trust and its explainable current cause"
|
||||||
|
)
|
||||||
|
var reload_probe := _create_manager(1001)
|
||||||
|
_check(
|
||||||
|
reload_probe.restore_state_from_json(manager.serialize_state()),
|
||||||
|
"A communicated memory should remain valid after its direct speaker forgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
_prepare_worker(witness, Vector3(20.0, 0.0, 0.0), true)
|
||||||
|
_prepare_worker(recent_listener, Vector3(21.0, 0.0, 0.0), true)
|
||||||
|
var before_stale_share: String = manager.get_state_checksum()
|
||||||
|
_check(
|
||||||
|
not manager.try_communicate_at_shared_activity(witness.id, recent_listener.id),
|
||||||
|
"A direct witness who forgot a fact should no longer be able to share it"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
manager.get_state_checksum() == before_stale_share,
|
||||||
|
"Rejected stale communication should not mutate state"
|
||||||
|
)
|
||||||
|
|
||||||
|
_prepare_choice_divergence(manager, contributor, lasting_listener, recent_listener)
|
||||||
|
var lasting_choice: ActionSelectionResult = manager.action_selector.select_action(
|
||||||
|
lasting_listener, manager.village, 0.5, manager.npcs
|
||||||
|
)
|
||||||
|
var recent_choice: ActionSelectionResult = manager.action_selector.select_action(
|
||||||
|
recent_listener, manager.village, 0.5, manager.npcs
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
lasting_choice.action_id == SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
and contributor.npc_name in lasting_choice.reason
|
||||||
|
),
|
||||||
|
"The lasting causal memory should preserve the informed helping choice"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
recent_choice.action_id == SimulationIds.ACTION_PATROL,
|
||||||
|
"The villager whose routine fact faded should continue ordinary guard work"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.free()
|
||||||
|
boundary_restored.free()
|
||||||
|
restored.free()
|
||||||
|
reload_probe.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_worker(npc: SimNPC, position: Vector3, start_working: bool) -> void:
|
||||||
|
npc.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
npc.target_id = &"guard_post"
|
||||||
|
npc.position = position
|
||||||
|
if start_working:
|
||||||
|
npc.start_working()
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_choice_divergence(
|
||||||
|
manager: Node, contributor: SimNPC, informed: SimNPC, control: SimNPC
|
||||||
|
) -> void:
|
||||||
|
var pantry: StorageStateRecord = manager.get_pantry()
|
||||||
|
pantry.withdraw(
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
maxf(
|
||||||
|
(
|
||||||
|
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||||
|
- (ActionSelectionSystem.RELATIONSHIP_AID_PANTRY_THRESHOLD - 5.0)
|
||||||
|
),
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||||
|
manager.economy.deposit_resource(SimulationIds.RESOURCE_WOOD, 10.0)
|
||||||
|
manager.village.safety = 50.0
|
||||||
|
manager.village.knowledge = 20.0
|
||||||
|
manager.village.update_priorities()
|
||||||
|
contributor.hunger = 95.0
|
||||||
|
contributor.is_starving = true
|
||||||
|
contributor.is_dead = false
|
||||||
|
for observer in [informed, control]:
|
||||||
|
observer.profession = SimulationIds.PROFESSION_GUARD
|
||||||
|
observer.hunger = 20.0
|
||||||
|
observer.energy = 80.0
|
||||||
|
observer.is_starving = false
|
||||||
|
observer.starvation_ticks = 0
|
||||||
|
observer.inventory.clear()
|
||||||
|
observer.last_task = &""
|
||||||
|
|
||||||
|
|
||||||
|
func _find_deposit_event(manager: Node, actor_id: int) -> EconomicEventRecord:
|
||||||
|
for event in manager.get_npc_events(actor_id, 4):
|
||||||
|
if StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED:
|
||||||
|
return event
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _create_manager(seed_value: int = 831) -> Node:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.simulation_seed = seed_value
|
||||||
|
manager.debug_logs = false
|
||||||
|
var home_positions: Array[Vector3] = [
|
||||||
|
Vector3(0.0, 0.0, 0.0),
|
||||||
|
Vector3(1.0, 0.0, 0.0),
|
||||||
|
Vector3(2.0, 0.0, 0.0),
|
||||||
|
Vector3(3.0, 0.0, 0.0),
|
||||||
|
Vector3(30.0, 0.0, 30.0),
|
||||||
|
Vector3(40.0, 0.0, 40.0),
|
||||||
|
]
|
||||||
|
manager.home_positions = home_positions
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
func _on_event_knowledge_forgotten(knower_id: int, event: EconomicEventRecord) -> void:
|
||||||
|
forgotten_pairs.append("%d:%d" % [knower_id, int(event.data["event_id"])])
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Knowledge retention passed: recent fades, lasting cause remains")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://31vf2sh14nwe
|
||||||
@@ -17,6 +17,7 @@ func _run() -> void:
|
|||||||
_test_previous_world_relationship_migration()
|
_test_previous_world_relationship_migration()
|
||||||
_test_previous_world_knowledge_migration()
|
_test_previous_world_knowledge_migration()
|
||||||
_test_previous_world_provenance_migration()
|
_test_previous_world_provenance_migration()
|
||||||
|
_test_previous_world_retention_migration()
|
||||||
_test_relationship_schema_rejection()
|
_test_relationship_schema_rejection()
|
||||||
_test_schema_rejection()
|
_test_schema_rejection()
|
||||||
|
|
||||||
@@ -138,6 +139,15 @@ func _test_schema_rejection() -> void:
|
|||||||
)
|
)
|
||||||
_check(SimulationStateRecord.from_json("[]") == null, "Non-record JSON should be rejected")
|
_check(SimulationStateRecord.from_json("[]") == null, "Non-record JSON should be rejected")
|
||||||
|
|
||||||
|
var manager := _create_manager(41)
|
||||||
|
var invalid_clock_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
invalid_clock_data["simulation"]["tick_interval"] = 0.0
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(invalid_clock_data) == null,
|
||||||
|
"Saved simulation clocks with a zero tick interval should be rejected"
|
||||||
|
)
|
||||||
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
func _test_legacy_resource_migration() -> void:
|
func _test_legacy_resource_migration() -> void:
|
||||||
var migrated := ResourceStateRecord.from_dictionary(
|
var migrated := ResourceStateRecord.from_dictionary(
|
||||||
@@ -272,7 +282,7 @@ func _test_previous_world_knowledge_migration() -> void:
|
|||||||
var deposit_event := EconomicEventRecord.create(
|
var deposit_event := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
12,
|
0,
|
||||||
1,
|
1,
|
||||||
SimulationIds.npc_inventory_id(1),
|
SimulationIds.npc_inventory_id(1),
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
@@ -305,11 +315,11 @@ func _test_previous_world_knowledge_migration() -> void:
|
|||||||
func _test_previous_world_provenance_migration() -> void:
|
func _test_previous_world_provenance_migration() -> void:
|
||||||
var manager := _create_manager(60)
|
var manager := _create_manager(60)
|
||||||
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
previous_data["schema_version"] = SimulationStateRecord.PROVENANCE_LEGACY_SCHEMA_VERSION
|
||||||
var deposit_event := EconomicEventRecord.create(
|
var deposit_event := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
12,
|
0,
|
||||||
1,
|
1,
|
||||||
SimulationIds.npc_inventory_id(1),
|
SimulationIds.npc_inventory_id(1),
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
@@ -358,6 +368,77 @@ func _test_previous_world_provenance_migration() -> void:
|
|||||||
manager.free()
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_previous_world_retention_migration() -> void:
|
||||||
|
var manager := _create_manager(61)
|
||||||
|
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
||||||
|
previous_data["simulation"]["tick_count"] = 50
|
||||||
|
var deposit_event := EconomicEventRecord.create(
|
||||||
|
0,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SimulationIds.npc_inventory_id(0),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
2.0
|
||||||
|
)
|
||||||
|
previous_data["economic_events"] = [deposit_event.to_dictionary()]
|
||||||
|
previous_data["simulation"]["next_event_id"] = 1
|
||||||
|
var actor_knowledge := (
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(0, 0, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
|
var witness_knowledge := (
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
|
var communicated_knowledge := (
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
|
for old_record in [actor_knowledge, witness_knowledge, communicated_knowledge]:
|
||||||
|
old_record["schema_version"] = 2
|
||||||
|
old_record.erase("acquired_tick")
|
||||||
|
old_record.erase("source_acquisition_method")
|
||||||
|
previous_data["event_knowledge"] = [communicated_knowledge, actor_knowledge, witness_knowledge]
|
||||||
|
previous_data["relationships"] = []
|
||||||
|
var migrated := SimulationStateRecord.from_dictionary(previous_data)
|
||||||
|
_check(migrated != null, "World schema v6 should add deterministic retention metadata")
|
||||||
|
if migrated != null:
|
||||||
|
var records_by_knower := {}
|
||||||
|
for record in migrated.event_knowledge:
|
||||||
|
records_by_knower[record.get_knower_id()] = record
|
||||||
|
var migrated_actor := records_by_knower.get(0) as KnownEventStateRecord
|
||||||
|
var migrated_listener := records_by_knower.get(1) as KnownEventStateRecord
|
||||||
|
_check(
|
||||||
|
migrated_actor != null and migrated_actor.get_acquired_tick() == 0,
|
||||||
|
"Direct v6 knowledge should retain the objective event tick"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
migrated_listener != null
|
||||||
|
and migrated_listener.get_acquired_tick() == 50
|
||||||
|
and (
|
||||||
|
migrated_listener.get_source_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Communicated v6 knowledge should receive a fresh lease and source snapshot"
|
||||||
|
)
|
||||||
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
func _test_relationship_schema_rejection() -> void:
|
func _test_relationship_schema_rejection() -> void:
|
||||||
var manager := _create_manager(58)
|
var manager := _create_manager(58)
|
||||||
var missing_cause_data: Dictionary = manager.create_state_record().to_dictionary()
|
var missing_cause_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
@@ -389,7 +470,7 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
var event := EconomicEventRecord.create(
|
var event := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
1,
|
0,
|
||||||
0,
|
0,
|
||||||
SimulationIds.npc_inventory_id(0),
|
SimulationIds.npc_inventory_id(0),
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
@@ -411,7 +492,7 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
var unrelated_deposit := EconomicEventRecord.create(
|
var unrelated_deposit := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
1,
|
0,
|
||||||
2,
|
2,
|
||||||
SimulationIds.npc_inventory_id(2),
|
SimulationIds.npc_inventory_id(2),
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
@@ -435,7 +516,7 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
var nonpositive_deposit := EconomicEventRecord.create(
|
var nonpositive_deposit := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
1,
|
0,
|
||||||
1,
|
1,
|
||||||
SimulationIds.npc_inventory_id(1),
|
SimulationIds.npc_inventory_id(1),
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
@@ -456,26 +537,40 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
var provenance_event := EconomicEventRecord.create(
|
var provenance_event := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
1,
|
0,
|
||||||
0,
|
0,
|
||||||
SimulationIds.npc_inventory_id(0),
|
SimulationIds.npc_inventory_id(0),
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
SimulationIds.RESOURCE_FOOD,
|
SimulationIds.RESOURCE_FOOD,
|
||||||
1.0
|
1.0
|
||||||
)
|
)
|
||||||
var missing_source_data: Dictionary = manager.create_state_record().to_dictionary()
|
var historical_source_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
missing_source_data["economic_events"] = [provenance_event.to_dictionary()]
|
historical_source_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
missing_source_data["simulation"]["next_event_id"] = 1
|
historical_source_data["simulation"]["next_event_id"] = 1
|
||||||
missing_source_data["event_knowledge"] = [
|
historical_source_data["event_knowledge"] = [
|
||||||
(
|
(
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
. create(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
. to_dictionary()
|
. to_dictionary()
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
_check(
|
_check(
|
||||||
SimulationStateRecord.from_dictionary(missing_source_data) == null,
|
SimulationStateRecord.from_dictionary(historical_source_data) != null,
|
||||||
"Communicated knowledge must reference a source who knows the same event"
|
"A listener's provenance should remain valid after the direct source forgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
var unknown_source_data: Dictionary = historical_source_data.duplicate(true)
|
||||||
|
unknown_source_data["event_knowledge"][0]["source_npc_id"] = 999
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(unknown_source_data) == null,
|
||||||
|
"Communicated knowledge must reference an existing historical source"
|
||||||
)
|
)
|
||||||
|
|
||||||
var wrong_actor_data: Dictionary = manager.create_state_record().to_dictionary()
|
var wrong_actor_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
@@ -499,12 +594,26 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
relayed_source_data["event_knowledge"] = [
|
relayed_source_data["event_knowledge"] = [
|
||||||
(
|
(
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
. create(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
|
)
|
||||||
. to_dictionary()
|
. to_dictionary()
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 0)
|
. create(
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||||
|
)
|
||||||
. to_dictionary()
|
. to_dictionary()
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -517,6 +626,14 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
SimulationStateRecord.from_dictionary(relayed_source_data) == null,
|
SimulationStateRecord.from_dictionary(relayed_source_data) == null,
|
||||||
"Communicated facts must not form a second relay hop in this phase"
|
"Communicated facts must not form a second relay hop in this phase"
|
||||||
)
|
)
|
||||||
|
var reacquired_source_data: Dictionary = relayed_source_data.duplicate(true)
|
||||||
|
reacquired_source_data["event_knowledge"][0]["source_acquisition_method"] = String(
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(reacquired_source_data) != null,
|
||||||
|
"Historical direct provenance should survive a source reacquiring the fact differently"
|
||||||
|
)
|
||||||
|
|
||||||
var order_independent_data: Dictionary = manager.create_state_record().to_dictionary()
|
var order_independent_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
order_independent_data["economic_events"] = [provenance_event.to_dictionary()]
|
order_independent_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
@@ -524,7 +641,14 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
order_independent_data["event_knowledge"] = [
|
order_independent_data["event_knowledge"] = [
|
||||||
(
|
(
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
. create(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
. to_dictionary()
|
. to_dictionary()
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -542,6 +666,22 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
SimulationStateRecord.from_dictionary(order_independent_data) != null,
|
SimulationStateRecord.from_dictionary(order_independent_data) != null,
|
||||||
"Provenance validation should not depend on knowledge array ordering"
|
"Provenance validation should not depend on knowledge array ordering"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var invalid_direct_time_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
invalid_direct_time_data["simulation"]["tick_count"] = 2
|
||||||
|
invalid_direct_time_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
|
invalid_direct_time_data["simulation"]["next_event_id"] = 1
|
||||||
|
invalid_direct_time_data["event_knowledge"] = [
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 1)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(invalid_direct_time_data) == null,
|
||||||
|
"Direct witness acquisition must use the immutable objective event tick"
|
||||||
|
)
|
||||||
manager.free()
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -162,16 +162,20 @@ func test_event_communication_preserves_first_provenance_and_stops_after_one_hop
|
|||||||
var knowledge_system := EventKnowledgeSystemScript.new()
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
knowledge_system.observe_event(event, villagers)
|
knowledge_system.observe_event(event, villagers)
|
||||||
|
|
||||||
assert_null(knowledge_system.communicate_event(event, listener.id, relay_target.id))
|
assert_null(knowledge_system.communicate_event(event, listener.id, relay_target.id, 50))
|
||||||
var communicated := knowledge_system.communicate_event(event, witness.id, listener.id)
|
var communicated := knowledge_system.communicate_event(event, witness.id, listener.id, 50)
|
||||||
assert_not_null(communicated)
|
assert_not_null(communicated)
|
||||||
assert_eq(
|
assert_eq(
|
||||||
communicated.get_acquisition_method(), SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
communicated.get_acquisition_method(), SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
)
|
)
|
||||||
assert_eq(communicated.get_source_npc_id(), witness.id)
|
assert_eq(communicated.get_source_npc_id(), witness.id)
|
||||||
assert_null(knowledge_system.communicate_event(event, actor.id, listener.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_eq(knowledge_system.get_record(listener.id, event.data["event_id"]), communicated)
|
||||||
assert_null(knowledge_system.communicate_event(event, listener.id, relay_target.id))
|
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"])))
|
assert_false(knowledge_system.knows_event(relay_target.id, int(event.data["event_id"])))
|
||||||
var actor_gap_system := EventKnowledgeSystemScript.new()
|
var actor_gap_system := EventKnowledgeSystemScript.new()
|
||||||
var direct_witness_records: Array[KnownEventStateRecord] = [
|
var direct_witness_records: Array[KnownEventStateRecord] = [
|
||||||
@@ -180,4 +184,36 @@ func test_event_communication_preserves_first_provenance_and_stops_after_one_hop
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
actor_gap_system.restore(direct_witness_records)
|
actor_gap_system.restore(direct_witness_records)
|
||||||
assert_null(actor_gap_system.communicate_event(event, witness.id, actor.id))
|
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, 1, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 10),
|
||||||
|
KnownEventStateRecord.create(1, 2, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 20),
|
||||||
|
]
|
||||||
|
ranked_system.restore(ranked_records)
|
||||||
|
var lasting_event_ids: Array[int] = [1]
|
||||||
|
assert_eq(ranked_system.get_communicable_event_ids(1, lasting_event_ids), [1, 2])
|
||||||
|
|||||||
+31
-6
@@ -31,6 +31,8 @@ func _ready() -> void:
|
|||||||
simulation_manager.relationship_changed.connect(_on_relationship_changed)
|
simulation_manager.relationship_changed.connect(_on_relationship_changed)
|
||||||
if simulation_manager.has_signal("event_knowledge_changed"):
|
if simulation_manager.has_signal("event_knowledge_changed"):
|
||||||
simulation_manager.event_knowledge_changed.connect(_on_event_knowledge_changed)
|
simulation_manager.event_knowledge_changed.connect(_on_event_knowledge_changed)
|
||||||
|
if simulation_manager.has_signal("event_knowledge_forgotten"):
|
||||||
|
simulation_manager.event_knowledge_forgotten.connect(_on_event_knowledge_forgotten)
|
||||||
|
|
||||||
if "village" in simulation_manager:
|
if "village" in simulation_manager:
|
||||||
_on_village_changed(simulation_manager.village)
|
_on_village_changed(simulation_manager.village)
|
||||||
@@ -133,6 +135,10 @@ func _on_event_knowledge_changed(_knower_id: int, _event: EconomicEventRecord) -
|
|||||||
_refresh_npc_inspector()
|
_refresh_npc_inspector()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_event_knowledge_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||||
|
_refresh_npc_inspector()
|
||||||
|
|
||||||
|
|
||||||
func _refresh_npc_inspector() -> void:
|
func _refresh_npc_inspector() -> void:
|
||||||
if not debug_overlay_visible:
|
if not debug_overlay_visible:
|
||||||
return
|
return
|
||||||
@@ -182,6 +188,7 @@ func _refresh_npc_inspector() -> void:
|
|||||||
score_text += "\n\nUnavailable\n" + "\n".join(rejection_rows)
|
score_text += "\n\nUnavailable\n" + "\n".join(rejection_rows)
|
||||||
var event_text := _build_event_history(npc)
|
var event_text := _build_event_history(npc)
|
||||||
var relationship_text := _build_relationship_display(npc)
|
var relationship_text := _build_relationship_display(npc)
|
||||||
|
var memory_text := _build_memory_display(npc)
|
||||||
var known_fact_text := _build_known_fact_display(npc)
|
var known_fact_text := _build_known_fact_display(npc)
|
||||||
npc_inspector_label.text = (
|
npc_inspector_label.text = (
|
||||||
(
|
(
|
||||||
@@ -192,6 +199,7 @@ func _refresh_npc_inspector() -> void:
|
|||||||
+ "Carrying food: %.0f wood: %.0f\n"
|
+ "Carrying food: %.0f wood: %.0f\n"
|
||||||
+ "%s\n"
|
+ "%s\n"
|
||||||
+ "%s\n"
|
+ "%s\n"
|
||||||
|
+ "%s\n"
|
||||||
+ "Why\n%s%s\n\n"
|
+ "Why\n%s%s\n\n"
|
||||||
+ "Recent\n%s\n\n"
|
+ "Recent\n%s\n\n"
|
||||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||||
@@ -207,6 +215,7 @@ func _refresh_npc_inspector() -> void:
|
|||||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||||
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
||||||
relationship_text,
|
relationship_text,
|
||||||
|
memory_text,
|
||||||
known_fact_text,
|
known_fact_text,
|
||||||
reason_text,
|
reason_text,
|
||||||
score_text,
|
score_text,
|
||||||
@@ -288,7 +297,8 @@ func _build_known_fact_display(npc: SimNPC) -> String:
|
|||||||
for other_npc in simulation_manager.npcs:
|
for other_npc in simulation_manager.npcs:
|
||||||
name_map[other_npc.id] = other_npc.npc_name
|
name_map[other_npc.id] = other_npc.npc_name
|
||||||
var latest_event := known_events[known_events.size() - 1] as EconomicEventRecord
|
var latest_event := known_events[known_events.size() - 1] as EconomicEventRecord
|
||||||
var provenance := ""
|
var provenance := "source not recorded"
|
||||||
|
var retention := "recent"
|
||||||
if simulation_manager.has_method("get_known_event_record"):
|
if simulation_manager.has_method("get_known_event_record"):
|
||||||
var known_record: KnownEventStateRecord = simulation_manager.get_known_event_record(
|
var known_record: KnownEventStateRecord = simulation_manager.get_known_event_record(
|
||||||
npc.id, int(latest_event.data["event_id"])
|
npc.id, int(latest_event.data["event_id"])
|
||||||
@@ -296,14 +306,29 @@ func _build_known_fact_display(npc: SimNPC) -> String:
|
|||||||
if known_record != null:
|
if known_record != null:
|
||||||
match known_record.get_acquisition_method():
|
match known_record.get_acquisition_method():
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||||
provenance = " (performed personally)"
|
provenance = "performed personally"
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
||||||
provenance = " (witnessed)"
|
provenance = "witnessed"
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
var source_name: String = name_map.get(
|
var source_name: String = name_map.get(
|
||||||
known_record.get_source_npc_id(), "someone"
|
known_record.get_source_npc_id(), "someone"
|
||||||
)
|
)
|
||||||
provenance = " (heard from %s)" % source_name
|
provenance = "heard from %s" % source_name
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
||||||
provenance = " (source not recorded)"
|
provenance = "source not recorded"
|
||||||
return "Known fact: %s%s" % [latest_event.description(name_map), provenance]
|
if (
|
||||||
|
simulation_manager.has_method("is_known_event_lasting")
|
||||||
|
and (simulation_manager.is_known_event_lasting(npc.id, int(latest_event.data["event_id"])))
|
||||||
|
):
|
||||||
|
retention = "lasting"
|
||||||
|
return "Known fact: %s (%s · %s)" % [latest_event.description(name_map), provenance, retention]
|
||||||
|
|
||||||
|
|
||||||
|
func _build_memory_display(npc: SimNPC) -> String:
|
||||||
|
if not simulation_manager.has_method("get_memory_summary"):
|
||||||
|
return ""
|
||||||
|
var summary: Dictionary = simulation_manager.get_memory_summary(npc.id)
|
||||||
|
return (
|
||||||
|
"Memory: %d lasting · %d recent"
|
||||||
|
% [int(summary.get("lasting", 0)), int(summary.get("recent", 0))]
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user