feat: add provenance-aware npc communication
This commit is contained in:
@@ -72,8 +72,11 @@ does not recompute decisions. At completion it atomically pays any
|
|||||||
definition-backed stored-resource cost before applying the action effect. A
|
definition-backed stored-resource cost before applying the action effect. A
|
||||||
late shortfall suppresses the effect and records a `task_blocked` fact. The
|
late shortfall suppresses the effect and records a `task_blocked` fact. The
|
||||||
manager also captures actor/nearby knowledge before forwarding newly recorded
|
manager also captures actor/nearby knowledge before forwarding newly recorded
|
||||||
events into `RelationshipSystem`. It exposes knowledge, relationship, and cause
|
events into `RelationshipSystem`. When an NPC arrives beside a worker at the
|
||||||
queries without moving social authority into UI.
|
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
|
||||||
|
knowledge, provenance, relationship, and cause queries without moving social
|
||||||
|
authority into UI.
|
||||||
|
|
||||||
### VillageEconomy
|
### VillageEconomy
|
||||||
|
|
||||||
@@ -94,6 +97,10 @@ queries without moving social authority into UI.
|
|||||||
history;
|
history;
|
||||||
- initially observes successful food-deposit events for the actor and living
|
- initially observes successful food-deposit events for the actor and living
|
||||||
NPCs within a fixed proximity radius;
|
NPCs within a fixed proximity radius;
|
||||||
|
- preserves first acquisition as performed, witnessed, communicated, or
|
||||||
|
legacy, with a stable source NPC ID for direct communication;
|
||||||
|
- permits one performed/witnessed fact to cross one co-located social hop, but
|
||||||
|
does not relay communicated or ambiguous legacy facts;
|
||||||
- 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
|
-> EventKnowledgeSystem records actor/nearby knowledge and bounded transfers
|
||||||
-> 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
|
||||||
@@ -33,7 +33,8 @@ would otherwise obscure that lifecycle:
|
|||||||
- `simulation/events/SimulationEventLog.gd` owns ordered event identity,
|
- `simulation/events/SimulationEventLog.gd` owns ordered event identity,
|
||||||
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 and captures proximity witnesses at record time;
|
known objective events, immutable acquisition provenance, proximity witnesses
|
||||||
|
at record time, and one-hop direct fact transfer;
|
||||||
- `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;
|
||||||
|
|||||||
@@ -689,12 +689,17 @@ Completed:
|
|||||||
21. Witnessed event knowledge: objective food-deposit events create separate
|
21. Witnessed event knowledge: objective food-deposit events create separate
|
||||||
known-event references for their actor and nearby living NPCs. Knowledge
|
known-event references for their actor and nearby living NPCs. Knowledge
|
||||||
gates trust, produces a real informed-versus-uninformed choice divergence,
|
gates trust, produces a real informed-versus-uninformed choice divergence,
|
||||||
survives schema-v5 save/load, and appears in the NPC inspector.
|
survives versioned save/load, and appears in the NPC inspector.
|
||||||
|
22. Co-worker fact communication: known-event records retain performed,
|
||||||
|
witnessed, communicated, or legacy acquisition provenance. An arriving NPC
|
||||||
|
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
|
||||||
|
replayed, and heard facts do not cascade yet.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Add one provenance-aware NPC-to-NPC fact transfer, then bounded
|
1. Add bounded importance/retention rules for known facts, as sequenced in
|
||||||
importance/retention rules, as sequenced in `LEARNING_ROADMAP.md`.
|
`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
|
||||||
|
|||||||
+22
-10
@@ -37,12 +37,13 @@ transfer occurred.
|
|||||||
|
|
||||||
## Persistence and determinism
|
## Persistence and determinism
|
||||||
|
|
||||||
`SimulationStateRecord` schema v5 stores the ordered event stream,
|
`SimulationStateRecord` schema v6 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. Schema v1 and v2 saves migrate to an empty
|
per-NPC known-event references with first-acquisition provenance. Schema v1 and
|
||||||
stream beginning at ID zero. Parsing rejects duplicate event IDs, invalid or
|
v2 saves migrate to an empty stream beginning at ID zero. Parsing rejects
|
||||||
duplicate knowledge references, relationship causes the observer does not
|
duplicate event IDs, invalid or duplicate knowledge references, impossible
|
||||||
know, and a next ID that could collide with restored history.
|
communicator sources, relationship causes the observer does not know, and a
|
||||||
|
next ID that could collide with restored history.
|
||||||
|
|
||||||
A successful food deposit can currently raise a hungry familiar NPC's directed
|
A successful food deposit can currently raise a hungry familiar NPC's directed
|
||||||
trust in its contributor only when that NPC knows the event. The actor and
|
trust in its contributor only when that NPC knows the event. The actor and
|
||||||
@@ -53,6 +54,15 @@ rather than copied prose, so the inspector can resolve and display the real
|
|||||||
completed fact. This is a first evidence-gated causal consumer of the event
|
completed fact. This is a first evidence-gated causal consumer of the event
|
||||||
stream, not general event sourcing.
|
stream, not general event sourcing.
|
||||||
|
|
||||||
|
Knowledge now records whether an NPC performed, witnessed, heard, or inherited
|
||||||
|
a fact from a legacy save. When an NPC arrives beside an already-working NPC at
|
||||||
|
the same non-storage activity site, the worker may communicate one newest
|
||||||
|
direct fact within 2.5 metres. The listener references the same immutable event
|
||||||
|
and retains the speaker ID; no transaction or event is replayed. Only
|
||||||
|
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
|
||||||
|
is acquired, not a reconstruction of their needs when the old event occurred.
|
||||||
|
|
||||||
The food-loop regression verifies this chain:
|
The food-loop regression verifies this chain:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -73,8 +83,10 @@ NPC record, so unloading presentation does not lose the fact.
|
|||||||
|
|
||||||
The stream is currently kept in full for the small simulation garden. Before
|
The stream is currently kept in full for the small simulation garden. Before
|
||||||
large populations or long-running worlds, add measured retention,
|
large populations or long-running worlds, add measured retention,
|
||||||
archival/summary rules, and query indexes. Proximity is the only current witness
|
archival/summary rules, and query indexes. Proximity is the only current direct
|
||||||
rule; line of sight, hearing, acquisition provenance, communication, secrecy,
|
witness rule, and communication is intentionally one-hop and tied to shared
|
||||||
false beliefs, multi-event causal graphs, and memory retention belong in later
|
activity arrival. Line of sight, hearing, acquisition timestamps, multi-hop
|
||||||
event/history slices. They should extend this record family without making
|
rumours, secrecy, false beliefs, multi-event causal graphs, and memory
|
||||||
prose authoritative or recomputing old evidence from current positions.
|
retention belong in later event/history slices. They should extend this record
|
||||||
|
family without making prose authoritative or recomputing old evidence from
|
||||||
|
current positions.
|
||||||
|
|||||||
@@ -668,16 +668,26 @@ The first witnessed-knowledge slice is also complete:
|
|||||||
- two equally familiar guards with different evidence now gain different trust
|
- two equally familiar guards with different evidence now gain different trust
|
||||||
and choose helping versus ordinary patrol;
|
and choose helping versus ordinary patrol;
|
||||||
- the inspector resolves and displays the selected NPC's latest known fact;
|
- the inspector resolves and displays the selected NPC's latest known fact;
|
||||||
- schema v5 migration, invalid-reference rejection, deterministic save/load,
|
- schema v6 migration, invalid-reference rejection, deterministic save/load,
|
||||||
and proximity/idempotence tests cover the complete slice.
|
and proximity/idempotence tests cover the complete slice.
|
||||||
|
|
||||||
This proves the first Milestone 6 evidence-to-choice exit path without claiming
|
The first communicated-knowledge slice is complete:
|
||||||
full memory or belief simulation. The practical next sequence is:
|
|
||||||
|
|
||||||
1. Add acquisition provenance and one bounded NPC-to-NPC fact transfer so
|
- first acquisition is persisted as performed, witnessed, communicated, or
|
||||||
information can travel beyond direct witnesses without replaying events.
|
legacy, with the direct speaker retained for communicated facts;
|
||||||
2. Add importance/retention rules for known facts before expanding into
|
- a villager arriving beside an already-working villager at a shared
|
||||||
distortion, secrecy, rumours, or opportunity generation.
|
non-storage activity can receive one newest direct fact within 2.5 metres;
|
||||||
|
- communication references the original immutable event, changes only the
|
||||||
|
listener's knowledge/relationship consequence, and cannot relay a heard fact
|
||||||
|
again in this bounded phase;
|
||||||
|
- the inspector names who communicated the fact, and schema-v6 migration,
|
||||||
|
malformed-provenance rejection, event-history invariance, save/checksum, and
|
||||||
|
informed-choice regressions cover the complete slice.
|
||||||
|
|
||||||
|
This advances the first Milestone 6 evidence-to-choice path without claiming
|
||||||
|
full memory or belief simulation. The practical next slice is importance and
|
||||||
|
retention for known facts before expanding into distortion, secrecy, rumours,
|
||||||
|
or opportunity generation.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+20
-14
@@ -414,8 +414,9 @@ Focused `RefCounted` collaborators keep rule ownership visible:
|
|||||||
- `VillageEconomy` owns storage/inventory transactions and synchronized
|
- `VillageEconomy` owns storage/inventory transactions and synchronized
|
||||||
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 and captures
|
- `EventKnowledgeSystem` owns per-NPC known-event references, immutable
|
||||||
actor/nearby evidence when a witnessable event is recorded;
|
first-acquisition provenance, actor/nearby evidence capture, and bounded
|
||||||
|
one-hop communication;
|
||||||
- `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.
|
||||||
|
|
||||||
@@ -476,6 +477,7 @@ NpcVisual navigates through the active world
|
|||||||
| |
|
| |
|
||||||
| v
|
| v
|
||||||
| SimulationManager marks NPC as working
|
| SimulationManager marks NPC as working
|
||||||
|
| -> at a shared activity, one nearby worker may communicate one direct fact
|
||||||
| |
|
| |
|
||||||
| v
|
| v
|
||||||
| Later ticks complete work
|
| Later ticks complete work
|
||||||
@@ -521,12 +523,13 @@ 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 references to known objective events
|
│ ├── knowledge/ Per-NPC facts, provenance, and bounded transfer
|
||||||
│ ├── 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
|
||||||
├── tests/
|
├── tests/
|
||||||
│ ├── action_system_boundaries_test.gd
|
│ ├── action_system_boundaries_test.gd
|
||||||
|
│ ├── communicated_knowledge_consequence_test.gd
|
||||||
│ ├── 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
|
||||||
@@ -568,7 +571,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 v5. F5/F9 provide one
|
clock, and RNG state serialize through world schema v6. 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
|
||||||
@@ -582,12 +585,13 @@ These are expected prototype constraints, not necessarily isolated bugs:
|
|||||||
village storage. Other village metrics remain aggregate values rather than
|
village storage. Other village metrics remain aggregate values rather than
|
||||||
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 can retain direct proximity-witness knowledge of a
|
familiarity/trust. They retain direct or one-hop communicated knowledge of a
|
||||||
food deposit, but do not yet have wider social dimensions, goals, line of
|
food deposit with first-acquisition provenance, but do not yet have wider
|
||||||
sight/hearing evidence, memory decay, false beliefs, or communication.
|
social dimensions, goals, line-of-sight/hearing evidence, importance/decay,
|
||||||
- The reason inspector exposes current decisions, utility rejections, and one
|
false beliefs, or multi-hop rumours.
|
||||||
exact relationship cause plus the latest known fact, but deeper historical
|
- The reason inspector exposes current decisions, utility rejections, one exact
|
||||||
decision traces are not yet retained.
|
relationship cause, and the latest known fact with its speaker when
|
||||||
|
communicated, but deeper historical decision traces are not yet retained.
|
||||||
- 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.
|
||||||
@@ -827,10 +831,12 @@ simulation-garden runtime capture are complete.
|
|||||||
|
|
||||||
The first relationship and knowledge consequences are also complete:
|
The first relationship and knowledge consequences are also complete:
|
||||||
food-deposit events become separate known facts for their actor and nearby
|
food-deposit events become separate known facts for their actor and nearby
|
||||||
living NPCs; only an informed hungry familiar NPC gains directed trust. The
|
living NPCs; only an informed hungry familiar NPC gains directed trust. At a
|
||||||
exact event remains visible as both known fact and relationship cause, and that
|
shared non-storage activity, an already-working direct knower can tell one fact
|
||||||
trust can redirect ordinary work toward helping a starving acquaintance. One
|
to an arriving nearby villager. The same event remains visible as known fact
|
||||||
provenance-aware NPC-to-NPC fact transfer is the next systems slice.
|
and relationship cause, the inspector names the speaker, and the listener's
|
||||||
|
future work can change without duplicating event history. Importance and
|
||||||
|
retention for known facts is 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 v5 and captures:
|
simulation. The current world schema is v6 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,14 @@ simulation. The current world schema is v5 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.
|
copying it, including first-acquisition method and communicator provenance.
|
||||||
|
|
||||||
The top-level identity is:
|
The top-level identity is:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema": "the_steward.simulation",
|
"schema": "the_steward.simulation",
|
||||||
"schema_version": 5
|
"schema_version": 6
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ 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, and rejection of unsupported
|
round-tripping, divergent known-event state, communicated provenance, and
|
||||||
schemas.
|
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
|
||||||
@@ -106,9 +106,24 @@ a food deposit performed by the relationship subject.
|
|||||||
|
|
||||||
World schema v4 migrates relationship causes into known-event records. The old
|
World schema v4 migrates relationship causes into known-event records. The old
|
||||||
schema used village-wide awareness, so this preserves the implied known fact
|
schema used village-wide awareness, so this preserves the implied known fact
|
||||||
without falsely inventing spatial witness provenance. New v5 facts are created
|
without falsely inventing spatial witness provenance.
|
||||||
only at event-record time from the actor and nearby NPC positions; historical
|
|
||||||
events are never re-evaluated against current positions.
|
SimulationStateRecord v6 / KnownEventStateRecord v2 adds immutable
|
||||||
|
first-acquisition provenance: `performed`, `witnessed`, `communicated`, or
|
||||||
|
`legacy`. Communicated records also retain the stable NPC ID of the direct
|
||||||
|
speaker. New direct facts are created only at event-record time from the actor
|
||||||
|
and nearby NPC positions; historical events are never re-evaluated against
|
||||||
|
current positions. A co-located shared-activity encounter can add one
|
||||||
|
communicated reference to the original fact without appending or replaying the
|
||||||
|
event.
|
||||||
|
|
||||||
|
World schema v5 migrates actor-owned knowledge to `performed`; non-actor
|
||||||
|
knowledge becomes `legacy` because v5 may contain either a proximity witness or
|
||||||
|
an older implied relationship fact. Parsing never invents that missing
|
||||||
|
provenance. Current communicated records require a distinct existing source
|
||||||
|
who knows the same event through `performed` or `witnessed` acquisition. This
|
||||||
|
enforces the current one-hop boundary and makes validation independent of array
|
||||||
|
ordering.
|
||||||
|
|
||||||
## Resource authority
|
## Resource authority
|
||||||
|
|
||||||
@@ -146,10 +161,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–v4 layouts;
|
supported v1–v5 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 provenance, memory decay, or communication;
|
acquisition timestamps, importance/retention, memory decay, or 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
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ signal state_restored
|
|||||||
signal npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult)
|
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)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ var action_executor := ActionExecutionSystem.new()
|
|||||||
var target_resolver := ActionTargetResolver.new()
|
var target_resolver := ActionTargetResolver.new()
|
||||||
var speed_index := 2
|
var speed_index := 2
|
||||||
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
|
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
|
||||||
|
const KNOWLEDGE_COMMUNICATION_RADIUS := 2.5
|
||||||
signal speed_changed(multiplier: float)
|
signal speed_changed(multiplier: float)
|
||||||
|
|
||||||
const NPC_NAMES := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
const NPC_NAMES := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
||||||
@@ -384,18 +386,24 @@ func notify_npc_arrived(npc_id: int) -> void:
|
|||||||
npc.has_travel_target = false
|
npc.has_travel_target = false
|
||||||
npc.start_working()
|
npc.start_working()
|
||||||
|
|
||||||
|
var working_speakers: Array[SimNPC] = []
|
||||||
if npc.target_id != &"":
|
if npc.target_id != &"":
|
||||||
for other in npcs:
|
for other in npcs:
|
||||||
if other.id == npc.id or other.is_dead:
|
if other.id == npc.id or other.is_dead:
|
||||||
continue
|
continue
|
||||||
|
if other.target_id != npc.target_id:
|
||||||
|
continue
|
||||||
if (
|
if (
|
||||||
other.target_id == npc.target_id
|
|
||||||
and (
|
|
||||||
other.task_state
|
other.task_state
|
||||||
in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
||||||
)
|
|
||||||
):
|
):
|
||||||
relationship_system.increase_shared_work_familiarity(npc.id, other.id)
|
relationship_system.increase_shared_work_familiarity(npc.id, other.id)
|
||||||
|
if other.task_state == SimNPC.TASK_STATE_WORKING:
|
||||||
|
working_speakers.append(other)
|
||||||
|
working_speakers.sort_custom(_sort_npcs_by_id)
|
||||||
|
for speaker in working_speakers:
|
||||||
|
if try_communicate_at_shared_activity(speaker.id, npc.id):
|
||||||
|
break
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print(
|
print(
|
||||||
@@ -583,14 +591,81 @@ func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
|
|||||||
event, npcs
|
event, npcs
|
||||||
)
|
)
|
||||||
for learned_record in learned_records:
|
for learned_record in learned_records:
|
||||||
event_knowledge_changed.emit(learned_record.get_knower_id(), event)
|
_apply_new_event_knowledge(learned_record, event)
|
||||||
var knower_ids: Array[int] = event_knowledge_system.get_knowers(int(event.data["event_id"]))
|
economic_event_recorded.emit(event)
|
||||||
|
|
||||||
|
|
||||||
|
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
|
||||||
|
var speaker := _find_npc_by_id(speaker_id)
|
||||||
|
var listener := _find_npc_by_id(listener_id)
|
||||||
|
if not _can_communicate_at_shared_activity(speaker, listener):
|
||||||
|
return false
|
||||||
|
var known_event_ids := event_knowledge_system.get_known_event_ids(speaker.id, 0)
|
||||||
|
for index in range(known_event_ids.size() - 1, -1, -1):
|
||||||
|
var event := event_log.get_by_id(known_event_ids[index])
|
||||||
|
if event == null:
|
||||||
|
continue
|
||||||
|
var learned_record := event_knowledge_system.communicate_event(
|
||||||
|
event, speaker.id, listener.id
|
||||||
|
)
|
||||||
|
if learned_record == null:
|
||||||
|
continue
|
||||||
|
_apply_new_event_knowledge(learned_record, event)
|
||||||
|
event_knowledge_transferred.emit(speaker.id, listener.id, event)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _can_communicate_at_shared_activity(speaker: SimNPC, listener: SimNPC) -> bool:
|
||||||
|
if speaker == null or listener == null or speaker.id == listener.id:
|
||||||
|
return false
|
||||||
|
if speaker.is_dead or listener.is_dead:
|
||||||
|
return false
|
||||||
|
if (
|
||||||
|
speaker.task_state != SimNPC.TASK_STATE_WORKING
|
||||||
|
or listener.task_state != SimNPC.TASK_STATE_WORKING
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
if speaker.target_id.is_empty() or speaker.target_id != listener.target_id:
|
||||||
|
return false
|
||||||
|
if storage_states.has(speaker.target_id):
|
||||||
|
return false
|
||||||
|
var speaker_action := SimulationDefinitions.get_action(speaker.current_task)
|
||||||
|
var listener_action := SimulationDefinitions.get_action(listener.current_task)
|
||||||
|
if (
|
||||||
|
speaker_action == null
|
||||||
|
or listener_action == null
|
||||||
|
or speaker_action.target_type != SimulationIds.TARGET_ACTIVITY
|
||||||
|
or listener_action.target_type != SimulationIds.TARGET_ACTIVITY
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return (
|
||||||
|
speaker.position.distance_squared_to(listener.position)
|
||||||
|
<= KNOWLEDGE_COMMUNICATION_RADIUS * KNOWLEDGE_COMMUNICATION_RADIUS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_new_event_knowledge(
|
||||||
|
known_event: KnownEventStateRecord, event: EconomicEventRecord
|
||||||
|
) -> void:
|
||||||
|
event_knowledge_changed.emit(known_event.get_knower_id(), event)
|
||||||
|
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, knower_ids
|
event, npcs, newly_informed_ids
|
||||||
)
|
)
|
||||||
for relationship in changed_relationships:
|
for relationship in changed_relationships:
|
||||||
relationship_changed.emit(relationship, event)
|
relationship_changed.emit(relationship, event)
|
||||||
economic_event_recorded.emit(event)
|
|
||||||
|
|
||||||
|
func _find_npc_by_id(npc_id: int) -> SimNPC:
|
||||||
|
for npc in npcs:
|
||||||
|
if npc.id == npc_id:
|
||||||
|
return npc
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func _sort_npcs_by_id(first: SimNPC, second: SimNPC) -> bool:
|
||||||
|
return first.id < second.id
|
||||||
|
|
||||||
|
|
||||||
func get_primary_relationship(npc_id: int) -> RelationshipStateRecord:
|
func get_primary_relationship(npc_id: int) -> RelationshipStateRecord:
|
||||||
@@ -616,6 +691,10 @@ func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
|||||||
return event_knowledge_system.knows_event(npc_id, event_id)
|
return event_knowledge_system.knows_event(npc_id, event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func get_known_event_record(npc_id: int, event_id: int) -> KnownEventStateRecord:
|
||||||
|
return event_knowledge_system.get_record(npc_id, event_id)
|
||||||
|
|
||||||
|
|
||||||
func get_current_speed() -> String:
|
func get_current_speed() -> String:
|
||||||
return "%.2fx" % SPEED_LEVELS[speed_index]
|
return "%.2fx" % SPEED_LEVELS[speed_index]
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ const EVENT_TASK_STARTED := &"task_started"
|
|||||||
const EVENT_TASK_BLOCKED := &"task_blocked"
|
const EVENT_TASK_BLOCKED := &"task_blocked"
|
||||||
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
|
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
|
||||||
|
|
||||||
|
const KNOWLEDGE_ACQUISITION_PERFORMED := &"performed"
|
||||||
|
const KNOWLEDGE_ACQUISITION_WITNESSED := &"witnessed"
|
||||||
|
const KNOWLEDGE_ACQUISITION_COMMUNICATED := &"communicated"
|
||||||
|
const KNOWLEDGE_ACQUISITION_LEGACY := &"legacy"
|
||||||
|
|
||||||
|
|
||||||
static func npc_inventory_id(npc_id: int) -> StringName:
|
static func npc_inventory_id(npc_id: int) -> StringName:
|
||||||
return StringName("npc_%d_inventory" % npc_id)
|
return StringName("npc_%d_inventory" % npc_id)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
|||||||
var actor := _find_npc(int(event.data["actor_id"]), npcs)
|
var actor := _find_npc(int(event.data["actor_id"]), npcs)
|
||||||
if actor == null:
|
if actor == null:
|
||||||
return learned
|
return learned
|
||||||
var actor_record := _remember(actor.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)
|
||||||
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
|
||||||
@@ -23,17 +24,50 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
|||||||
continue
|
continue
|
||||||
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(npc.id, int(event.data["event_id"]))
|
var witness_record := _remember(
|
||||||
|
npc.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
if witness_record != null:
|
if witness_record != null:
|
||||||
learned.append(witness_record)
|
learned.append(witness_record)
|
||||||
learned.sort_custom(_sort_records)
|
learned.sort_custom(_sort_records)
|
||||||
return learned
|
return learned
|
||||||
|
|
||||||
|
|
||||||
|
func communicate_event(
|
||||||
|
event: EconomicEventRecord, speaker_id: int, listener_id: int
|
||||||
|
) -> KnownEventStateRecord:
|
||||||
|
if (
|
||||||
|
event == null
|
||||||
|
or speaker_id == listener_id
|
||||||
|
or listener_id == int(event.data["actor_id"])
|
||||||
|
or not _is_witnessable(event)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var event_id := int(event.data["event_id"])
|
||||||
|
var speaker_record := get_record(speaker_id, event_id)
|
||||||
|
if speaker_record == null:
|
||||||
|
return null
|
||||||
|
if (
|
||||||
|
speaker_record.get_acquisition_method()
|
||||||
|
not in [
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
return _remember(
|
||||||
|
listener_id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, speaker_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func knows_event(knower_id: int, event_id: int) -> bool:
|
func knows_event(knower_id: int, event_id: int) -> bool:
|
||||||
return known_events.has(_key(knower_id, event_id))
|
return known_events.has(_key(knower_id, event_id))
|
||||||
|
|
||||||
|
|
||||||
|
func get_record(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
||||||
|
return known_events.get(_key(knower_id, event_id)) as KnownEventStateRecord
|
||||||
|
|
||||||
|
|
||||||
func get_knowers(event_id: int) -> Array[int]:
|
func get_knowers(event_id: int) -> Array[int]:
|
||||||
var knower_ids: Array[int] = []
|
var knower_ids: Array[int] = []
|
||||||
for record in known_events.values():
|
for record in known_events.values():
|
||||||
@@ -71,11 +105,18 @@ func restore(records: Array[KnownEventStateRecord]) -> void:
|
|||||||
known_events[_key(record.get_knower_id(), record.get_event_id())] = record
|
known_events[_key(record.get_knower_id(), record.get_event_id())] = record
|
||||||
|
|
||||||
|
|
||||||
func _remember(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
func _remember(
|
||||||
|
knower_id: int,
|
||||||
|
event_id: int,
|
||||||
|
acquisition_method: StringName,
|
||||||
|
source_npc_id: int = KnownEventStateRecord.NO_SOURCE_NPC_ID
|
||||||
|
) -> 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(knower_id, event_id)
|
var record := KnownEventStateRecord.create(
|
||||||
|
knower_id, event_id, acquisition_method, source_npc_id
|
||||||
|
)
|
||||||
known_events[key] = record
|
known_events[key] = record
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
class_name KnownEventStateRecord
|
class_name KnownEventStateRecord
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_VERSION := 1
|
const SCHEMA_VERSION := 2
|
||||||
|
const NO_SOURCE_NPC_ID := -1
|
||||||
|
|
||||||
|
const VALID_ACQUISITION_METHODS := [
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||||
|
]
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
|
|
||||||
@@ -10,7 +18,12 @@ func _init(record_data: Dictionary = {}) -> void:
|
|||||||
data = record_data.duplicate(true)
|
data = record_data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
static func create(
|
||||||
|
knower_id: int,
|
||||||
|
event_id: int,
|
||||||
|
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||||
|
source_npc_id: int = NO_SOURCE_NPC_ID
|
||||||
|
) -> KnownEventStateRecord:
|
||||||
return (
|
return (
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
. new(
|
. new(
|
||||||
@@ -18,6 +31,8 @@ static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
|||||||
"schema_version": SCHEMA_VERSION,
|
"schema_version": SCHEMA_VERSION,
|
||||||
"knower_id": knower_id,
|
"knower_id": knower_id,
|
||||||
"event_id": event_id,
|
"event_id": event_id,
|
||||||
|
"acquisition_method": String(acquisition_method),
|
||||||
|
"source_npc_id": source_npc_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -26,13 +41,22 @@ static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
|||||||
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"]):
|
if not record_data.has_all(["knower_id", "event_id", "acquisition_method", "source_npc_id"]):
|
||||||
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 source_npc_id := int(record_data["source_npc_id"])
|
||||||
if knower_id < 0 or event_id < 0:
|
if knower_id < 0 or event_id < 0:
|
||||||
return null
|
return null
|
||||||
return create(knower_id, event_id)
|
if acquisition_method not in VALID_ACQUISITION_METHODS:
|
||||||
|
return null
|
||||||
|
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
|
if source_npc_id < 0 or source_npc_id == knower_id:
|
||||||
|
return null
|
||||||
|
elif source_npc_id != NO_SOURCE_NPC_ID:
|
||||||
|
return null
|
||||||
|
return create(knower_id, event_id, acquisition_method, source_npc_id)
|
||||||
|
|
||||||
|
|
||||||
func get_knower_id() -> int:
|
func get_knower_id() -> int:
|
||||||
@@ -43,5 +67,13 @@ func get_event_id() -> int:
|
|||||||
return int(data["event_id"])
|
return int(data["event_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_acquisition_method() -> StringName:
|
||||||
|
return StringName(data["acquisition_method"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_source_npc_id() -> int:
|
||||||
|
return int(data["source_npc_id"])
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
return data.duplicate(true)
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ class_name SimulationStateRecord
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_NAME := "the_steward.simulation"
|
const SCHEMA_NAME := "the_steward.simulation"
|
||||||
const SCHEMA_VERSION := 5
|
const SCHEMA_VERSION := 6
|
||||||
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 PREVIOUS_SCHEMA_VERSION := 4
|
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
|
||||||
|
const PREVIOUS_SCHEMA_VERSION := 5
|
||||||
|
|
||||||
var simulation: Dictionary
|
var simulation: Dictionary
|
||||||
var village: VillageStateRecord
|
var village: VillageStateRecord
|
||||||
@@ -74,6 +75,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
LEGACY_SCHEMA_VERSION,
|
LEGACY_SCHEMA_VERSION,
|
||||||
EVENT_LEGACY_SCHEMA_VERSION,
|
EVENT_LEGACY_SCHEMA_VERSION,
|
||||||
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||||
|
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
||||||
PREVIOUS_SCHEMA_VERSION,
|
PREVIOUS_SCHEMA_VERSION,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
@@ -207,6 +209,7 @@ 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
|
||||||
@@ -221,7 +224,13 @@ 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:
|
||||||
|
if not _is_valid_knowledge_provenance(
|
||||||
|
known_event_record, npc_ids, event_records_by_id, knowledge_records_by_key
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
|
||||||
var relationship_keys := {}
|
var relationship_keys := {}
|
||||||
for item in relationship_data:
|
for item in relationship_data:
|
||||||
@@ -266,6 +275,54 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
static func _is_valid_knowledge_provenance(
|
||||||
|
known_event: KnownEventStateRecord,
|
||||||
|
npc_ids: Dictionary,
|
||||||
|
event_records_by_id: Dictionary,
|
||||||
|
knowledge_records_by_key: Dictionary
|
||||||
|
) -> bool:
|
||||||
|
var event := event_records_by_id.get(known_event.get_event_id()) as EconomicEventRecord
|
||||||
|
if event == null:
|
||||||
|
return false
|
||||||
|
var method := known_event.get_acquisition_method()
|
||||||
|
var knower_id := known_event.get_knower_id()
|
||||||
|
var actor_id := int(event.data["actor_id"])
|
||||||
|
if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
||||||
|
return true
|
||||||
|
if (
|
||||||
|
StringName(event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||||
|
or StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
||||||
|
or float(event.data["amount"]) <= 0.0
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
match method:
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||||
|
return knower_id == actor_id
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
||||||
|
return knower_id != actor_id
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
|
if knower_id == actor_id:
|
||||||
|
return false
|
||||||
|
var source_npc_id := known_event.get_source_npc_id()
|
||||||
|
if not npc_ids.has(source_npc_id):
|
||||||
|
return false
|
||||||
|
var source_record := (
|
||||||
|
knowledge_records_by_key.get("%d:%d" % [source_npc_id, known_event.get_event_id()])
|
||||||
|
as KnownEventStateRecord
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
source_record != null
|
||||||
|
and (
|
||||||
|
source_record.get_acquisition_method()
|
||||||
|
in [
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
||||||
var migrated := legacy_data.duplicate(true)
|
var migrated := legacy_data.duplicate(true)
|
||||||
migrated["schema_version"] = SCHEMA_VERSION
|
migrated["schema_version"] = SCHEMA_VERSION
|
||||||
@@ -301,7 +358,21 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
|||||||
in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
|
in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
|
||||||
):
|
):
|
||||||
migrated["relationships"] = _migrate_npc_familiarity(legacy_data.get("npcs", []))
|
migrated["relationships"] = _migrate_npc_familiarity(legacy_data.get("npcs", []))
|
||||||
migrated["event_knowledge"] = _migrate_relationship_causes(migrated.get("relationships", []))
|
if (
|
||||||
|
version
|
||||||
|
in [
|
||||||
|
LEGACY_SCHEMA_VERSION,
|
||||||
|
EVENT_LEGACY_SCHEMA_VERSION,
|
||||||
|
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||||
|
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
migrated["event_knowledge"] = _migrate_relationship_causes(
|
||||||
|
migrated.get("relationships", [])
|
||||||
|
)
|
||||||
|
migrated["event_knowledge"] = _migrate_known_event_provenance(
|
||||||
|
migrated.get("event_knowledge", []), migrated.get("economic_events", [])
|
||||||
|
)
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
@@ -377,3 +448,41 @@ static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) ->
|
|||||||
if int(first["knower_id"]) != int(second["knower_id"]):
|
if int(first["knower_id"]) != int(second["knower_id"]):
|
||||||
return int(first["knower_id"]) < int(second["knower_id"])
|
return int(first["knower_id"]) < int(second["knower_id"])
|
||||||
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:
|
||||||
|
if not knowledge_data is Array:
|
||||||
|
return []
|
||||||
|
var actor_by_event_id := {}
|
||||||
|
if event_data is Array:
|
||||||
|
for event_item in event_data:
|
||||||
|
if not event_item is Dictionary:
|
||||||
|
continue
|
||||||
|
actor_by_event_id[int(event_item.get("event_id", -1))] = int(
|
||||||
|
event_item.get("actor_id", -1)
|
||||||
|
)
|
||||||
|
var migrated_knowledge: Array = []
|
||||||
|
var can_sort := true
|
||||||
|
for item in knowledge_data:
|
||||||
|
if not item is Dictionary:
|
||||||
|
migrated_knowledge.append(item)
|
||||||
|
can_sort = false
|
||||||
|
continue
|
||||||
|
if int(item.get("schema_version", -1)) == KnownEventStateRecord.SCHEMA_VERSION:
|
||||||
|
migrated_knowledge.append(item.duplicate(true))
|
||||||
|
continue
|
||||||
|
if int(item.get("schema_version", -1)) != 1 or not item.has_all(["knower_id", "event_id"]):
|
||||||
|
migrated_knowledge.append(item.duplicate(true))
|
||||||
|
can_sort = false
|
||||||
|
continue
|
||||||
|
var knower_id := int(item["knower_id"])
|
||||||
|
var event_id := int(item["event_id"])
|
||||||
|
var acquisition_method := SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
||||||
|
if int(actor_by_event_id.get(event_id, -2)) == knower_id:
|
||||||
|
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||||
|
migrated_knowledge.append(
|
||||||
|
KnownEventStateRecord.create(knower_id, event_id, acquisition_method).to_dictionary()
|
||||||
|
)
|
||||||
|
if can_sort:
|
||||||
|
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
||||||
|
return migrated_knowledge
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: 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 listener: SimNPC = manager.npcs[2]
|
||||||
|
var control: SimNPC = manager.npcs[3]
|
||||||
|
contributor.position = Vector3.ZERO
|
||||||
|
witness.position = Vector3(4.0, 0.0, 0.0)
|
||||||
|
listener.position = Vector3(20.0, 0.0, 0.0)
|
||||||
|
control.position = Vector3(21.0, 0.0, 0.0)
|
||||||
|
for observer in [witness, listener, control]:
|
||||||
|
observer.hunger = 85.0
|
||||||
|
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 2.0)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(
|
||||||
|
manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD), 2.0
|
||||||
|
),
|
||||||
|
"Communication should begin from a real completed food-deposit fact"
|
||||||
|
)
|
||||||
|
var deposit_event := _find_deposit_event(manager, contributor.id)
|
||||||
|
_check(deposit_event != null, "The original deposit must remain in objective event history")
|
||||||
|
if deposit_event == null:
|
||||||
|
manager.free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
var event_id := int(deposit_event.data["event_id"])
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.npc_knows_event(contributor.id, event_id)
|
||||||
|
and manager.npc_knows_event(witness.id, event_id)
|
||||||
|
and not manager.npc_knows_event(listener.id, event_id)
|
||||||
|
and not manager.npc_knows_event(control.id, event_id)
|
||||||
|
),
|
||||||
|
"Only the contributor and nearby witness should know the fact initially"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.get_known_event_record(witness.id, event_id).get_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
),
|
||||||
|
"The teller should retain direct witness provenance"
|
||||||
|
)
|
||||||
|
|
||||||
|
for npc in [witness, listener]:
|
||||||
|
npc.set_task(SimulationIds.ACTION_EAT)
|
||||||
|
npc.target_id = SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||||
|
npc.position = Vector3(20.0 + npc.id, 0.0, 0.0)
|
||||||
|
npc.start_working()
|
||||||
|
_check(
|
||||||
|
not manager.try_communicate_at_shared_activity(witness.id, listener.id),
|
||||||
|
"Passing at storage should not trigger the bounded shared-work conversation"
|
||||||
|
)
|
||||||
|
|
||||||
|
_prepare_shared_patrol(witness, listener, Vector3(20.0, 0.0, 0.0), Vector3(30.0, 0.0, 0.0))
|
||||||
|
var before_failed_transfer: String = manager.get_state_checksum()
|
||||||
|
_check(
|
||||||
|
not manager.try_communicate_at_shared_activity(witness.id, listener.id),
|
||||||
|
"Matching target IDs must not teleport knowledge between distant workers"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
manager.get_state_checksum() == before_failed_transfer,
|
||||||
|
"A rejected distant conversation must not mutate simulation state"
|
||||||
|
)
|
||||||
|
|
||||||
|
listener.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
listener.target_id = witness.target_id
|
||||||
|
listener.position = witness.position + Vector3(1.0, 0.0, 0.0)
|
||||||
|
var event_count_before: int = manager.economic_events.size()
|
||||||
|
var next_event_id_before: int = manager.next_event_id
|
||||||
|
manager.notify_npc_arrived(listener.id)
|
||||||
|
_check(
|
||||||
|
manager.npc_knows_event(listener.id, event_id),
|
||||||
|
"Arriving beside the witness at shared work should transfer one fact"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
manager.economic_events.size() == event_count_before
|
||||||
|
and manager.next_event_id == next_event_id_before
|
||||||
|
),
|
||||||
|
"Communication must reference the original event without replaying or duplicating it"
|
||||||
|
)
|
||||||
|
var communicated_record: KnownEventStateRecord = manager.get_known_event_record(
|
||||||
|
listener.id, event_id
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
communicated_record != null
|
||||||
|
and (
|
||||||
|
communicated_record.get_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
|
)
|
||||||
|
and communicated_record.get_source_npc_id() == witness.id
|
||||||
|
),
|
||||||
|
"The listener should preserve who communicated the objective fact"
|
||||||
|
)
|
||||||
|
var listener_relationship: RelationshipStateRecord = (
|
||||||
|
manager.relationship_system.get_relationship(listener.id, contributor.id)
|
||||||
|
)
|
||||||
|
var control_relationship: RelationshipStateRecord = (
|
||||||
|
manager.relationship_system.get_relationship(control.id, contributor.id)
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
listener_relationship != null
|
||||||
|
and is_equal_approx(listener_relationship.get_trust(), 0.65)
|
||||||
|
and listener_relationship.get_last_trust_cause_event_id() == event_id
|
||||||
|
),
|
||||||
|
"Learning the fact should apply its trust consequence only to the listener"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
control_relationship != null
|
||||||
|
and is_equal_approx(
|
||||||
|
control_relationship.get_trust(), RelationshipStateRecord.NEUTRAL_TRUST
|
||||||
|
)
|
||||||
|
and not manager.npc_knows_event(control.id, event_id)
|
||||||
|
),
|
||||||
|
"A nearby uninvolved villager should not receive a directed conversation"
|
||||||
|
)
|
||||||
|
|
||||||
|
var before_repeat: String = manager.get_state_checksum()
|
||||||
|
_check(
|
||||||
|
not manager.try_communicate_at_shared_activity(witness.id, listener.id),
|
||||||
|
"Repeating the same conversation should not replace first-acquisition provenance"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
manager.get_state_checksum() == before_repeat,
|
||||||
|
"Repeated communication must not add knowledge or apply trust twice"
|
||||||
|
)
|
||||||
|
control.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
control.target_id = listener.target_id
|
||||||
|
control.position = listener.position + Vector3(1.0, 0.0, 0.0)
|
||||||
|
control.start_working()
|
||||||
|
_check(
|
||||||
|
not manager.try_communicate_at_shared_activity(listener.id, control.id),
|
||||||
|
"Communicated facts should not relay again before the rumour phase"
|
||||||
|
)
|
||||||
|
|
||||||
|
_prepare_choice_divergence(manager, contributor, listener, control)
|
||||||
|
var informed_choice: ActionSelectionResult = manager.action_selector.select_action(
|
||||||
|
listener, manager.village, 0.5, manager.npcs
|
||||||
|
)
|
||||||
|
var control_choice: ActionSelectionResult = manager.action_selector.select_action(
|
||||||
|
control, manager.village, 0.5, manager.npcs
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
informed_choice.action_id == SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
and contributor.npc_name in informed_choice.reason
|
||||||
|
),
|
||||||
|
"The informed listener should act on communicated evidence"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
control_choice.action_id == SimulationIds.ACTION_PATROL,
|
||||||
|
"The uninformed control should continue ordinary guard work"
|
||||||
|
)
|
||||||
|
|
||||||
|
var saved_json: String = manager.serialize_state()
|
||||||
|
var restored := _create_manager(999)
|
||||||
|
_check(restored.restore_state_from_json(saved_json), "Communicated provenance should restore")
|
||||||
|
var restored_record: KnownEventStateRecord = restored.get_known_event_record(
|
||||||
|
listener.id, event_id
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
restored_record != null
|
||||||
|
and (
|
||||||
|
restored_record.get_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
|
)
|
||||||
|
and restored_record.get_source_npc_id() == witness.id
|
||||||
|
),
|
||||||
|
"Save/load should preserve the exact speaker provenance"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
restored.get_state_checksum() == manager.get_state_checksum(),
|
||||||
|
"Restored communication state should retain the deterministic checksum"
|
||||||
|
)
|
||||||
|
|
||||||
|
var restored_listener: SimNPC = restored.npcs[listener.id]
|
||||||
|
var restored_control: SimNPC = restored.npcs[control.id]
|
||||||
|
var current_next_informed_choice: ActionSelectionResult = manager.action_selector.select_action(
|
||||||
|
listener, manager.village, 0.5, manager.npcs
|
||||||
|
)
|
||||||
|
var current_next_control_choice: ActionSelectionResult = manager.action_selector.select_action(
|
||||||
|
control, manager.village, 0.5, manager.npcs
|
||||||
|
)
|
||||||
|
var restored_informed_choice: ActionSelectionResult = restored.action_selector.select_action(
|
||||||
|
restored_listener, restored.village, 0.5, restored.npcs
|
||||||
|
)
|
||||||
|
var restored_control_choice: ActionSelectionResult = restored.action_selector.select_action(
|
||||||
|
restored_control, restored.village, 0.5, restored.npcs
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
restored_informed_choice.action_id == current_next_informed_choice.action_id
|
||||||
|
and restored_control_choice.action_id == current_next_control_choice.action_id
|
||||||
|
),
|
||||||
|
"Restored knowledge should preserve the next informed and control choices"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
restored.get_state_checksum() == manager.get_state_checksum(),
|
||||||
|
"Equivalent future choices should advance restored RNG streams identically"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.free()
|
||||||
|
restored.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_shared_patrol(
|
||||||
|
witness: SimNPC, listener: SimNPC, witness_position: Vector3, listener_position: Vector3
|
||||||
|
) -> void:
|
||||||
|
for npc in [witness, listener]:
|
||||||
|
npc.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
npc.target_id = &"guard_post"
|
||||||
|
witness.position = witness_position
|
||||||
|
listener.position = listener_position
|
||||||
|
witness.start_working()
|
||||||
|
listener.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
|
||||||
|
for observer in [informed, control]:
|
||||||
|
observer.profession = SimulationIds.PROFESSION_GUARD
|
||||||
|
observer.hunger = 20.0
|
||||||
|
observer.energy = 80.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 = 821) -> 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 _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Communicated knowledge passed: witness -> coworker -> informed choice")
|
||||||
|
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://x8jugu64gj2o
|
||||||
@@ -44,6 +44,9 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
var contributor: SimNPC = simulation_manager.npcs[0]
|
var contributor: SimNPC = simulation_manager.npcs[0]
|
||||||
var witness: SimNPC = simulation_manager.npcs[1]
|
var witness: SimNPC = simulation_manager.npcs[1]
|
||||||
|
var listener: SimNPC = simulation_manager.npcs[2]
|
||||||
|
for npc in simulation_manager.npcs:
|
||||||
|
npc.position = Vector3(20.0 + npc.id * 2.0, 0.0, 0.0)
|
||||||
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
|
||||||
@@ -61,11 +64,32 @@ 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 "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
|
||||||
),
|
),
|
||||||
"NPC inspector should show the witnessed fact and its relationship consequence"
|
"NPC inspector should show the witnessed fact and its relationship consequence"
|
||||||
)
|
)
|
||||||
|
var guard_site := (
|
||||||
|
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
|
||||||
|
)
|
||||||
|
witness.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
witness.target_id = guard_site.site_id
|
||||||
|
witness.position = guard_site.get_interaction_position()
|
||||||
|
witness.start_working()
|
||||||
|
listener.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
listener.target_id = guard_site.site_id
|
||||||
|
listener.position = witness.position + Vector3(1.0, 0.0, 0.0)
|
||||||
|
simulation_manager.notify_npc_arrived(listener.id)
|
||||||
|
village_ui.selected_npc_index = listener.id
|
||||||
|
village_ui.call("_refresh_npc_inspector")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
"Known fact: %s deposited" % contributor.npc_name in inspector_label.text
|
||||||
|
and "(heard from %s)" % witness.npc_name in inspector_label.text
|
||||||
|
),
|
||||||
|
"NPC inspector should identify who communicated a known fact"
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
||||||
"Playable runtime should instance the Jajce Terrain3D world"
|
"Playable runtime should instance the Jajce Terrain3D world"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ func _run() -> void:
|
|||||||
_test_previous_world_event_migration()
|
_test_previous_world_event_migration()
|
||||||
_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_relationship_schema_rejection()
|
_test_relationship_schema_rejection()
|
||||||
_test_schema_rejection()
|
_test_schema_rejection()
|
||||||
|
|
||||||
@@ -266,7 +267,7 @@ func _test_previous_world_relationship_migration() -> void:
|
|||||||
func _test_previous_world_knowledge_migration() -> void:
|
func _test_previous_world_knowledge_migration() -> void:
|
||||||
var manager := _create_manager(59)
|
var manager := _create_manager(59)
|
||||||
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.KNOWLEDGE_LEGACY_SCHEMA_VERSION
|
||||||
previous_data.erase("event_knowledge")
|
previous_data.erase("event_knowledge")
|
||||||
var deposit_event := EconomicEventRecord.create(
|
var deposit_event := EconomicEventRecord.create(
|
||||||
0,
|
0,
|
||||||
@@ -291,12 +292,72 @@ func _test_previous_world_knowledge_migration() -> void:
|
|||||||
migrated.event_knowledge.size() == 1
|
migrated.event_knowledge.size() == 1
|
||||||
and migrated.event_knowledge[0].get_knower_id() == 0
|
and migrated.event_knowledge[0].get_knower_id() == 0
|
||||||
and migrated.event_knowledge[0].get_event_id() == 0
|
and migrated.event_knowledge[0].get_event_id() == 0
|
||||||
|
and (
|
||||||
|
migrated.event_knowledge[0].get_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"World v4 migration should preserve the fact implied by a relationship cause"
|
"World v4 migration should preserve the fact implied by a relationship cause"
|
||||||
)
|
)
|
||||||
manager.free()
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_previous_world_provenance_migration() -> void:
|
||||||
|
var manager := _create_manager(60)
|
||||||
|
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
||||||
|
var deposit_event := EconomicEventRecord.create(
|
||||||
|
0,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
12,
|
||||||
|
1,
|
||||||
|
SimulationIds.npc_inventory_id(1),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
2.0
|
||||||
|
)
|
||||||
|
previous_data["economic_events"] = [deposit_event.to_dictionary()]
|
||||||
|
previous_data["simulation"]["next_event_id"] = 1
|
||||||
|
previous_data["event_knowledge"] = [
|
||||||
|
{"schema_version": 1, "knower_id": 0, "event_id": 0},
|
||||||
|
{"schema_version": 1, "knower_id": 1, "event_id": 0},
|
||||||
|
]
|
||||||
|
previous_data["relationships"] = [
|
||||||
|
RelationshipStateRecord.create(0, 1, 0.5, 0.65, 0).to_dictionary()
|
||||||
|
]
|
||||||
|
var migrated := SimulationStateRecord.from_dictionary(previous_data)
|
||||||
|
_check(migrated != null, "World schema v5 should add honest acquisition provenance")
|
||||||
|
if migrated != null:
|
||||||
|
var observer_record: KnownEventStateRecord
|
||||||
|
var actor_record: KnownEventStateRecord
|
||||||
|
for known_event in migrated.event_knowledge:
|
||||||
|
if known_event.get_knower_id() == 0:
|
||||||
|
observer_record = known_event
|
||||||
|
elif known_event.get_knower_id() == 1:
|
||||||
|
actor_record = known_event
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
actor_record != null
|
||||||
|
and (
|
||||||
|
actor_record.get_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"World v5 migration can safely identify an actor's own completed fact"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
observer_record != null
|
||||||
|
and (
|
||||||
|
observer_record.get_acquisition_method()
|
||||||
|
== SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"World v5 migration must not invent witness or speaker provenance"
|
||||||
|
)
|
||||||
|
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()
|
||||||
@@ -391,6 +452,96 @@ func _test_relationship_schema_rejection() -> void:
|
|||||||
SimulationStateRecord.from_dictionary(nonpositive_cause_data) == null,
|
SimulationStateRecord.from_dictionary(nonpositive_cause_data) == null,
|
||||||
"A trust cause must describe a successful positive food deposit"
|
"A trust cause must describe a successful positive food deposit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var provenance_event := EconomicEventRecord.create(
|
||||||
|
0,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
SimulationIds.npc_inventory_id(0),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
var missing_source_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
missing_source_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
|
missing_source_data["simulation"]["next_event_id"] = 1
|
||||||
|
missing_source_data["event_knowledge"] = [
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(missing_source_data) == null,
|
||||||
|
"Communicated knowledge must reference a source who knows the same event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wrong_actor_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
wrong_actor_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
|
wrong_actor_data["simulation"]["next_event_id"] = 1
|
||||||
|
wrong_actor_data["event_knowledge"] = [
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||||
|
. to_dictionary()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(wrong_actor_data) == null,
|
||||||
|
"Performed provenance must belong to the event actor"
|
||||||
|
)
|
||||||
|
|
||||||
|
var relayed_source_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
relayed_source_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
|
relayed_source_data["simulation"]["next_event_id"] = 1
|
||||||
|
relayed_source_data["event_knowledge"] = [
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
||||||
|
. to_dictionary()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 0)
|
||||||
|
. to_dictionary()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(0, 0, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||||
|
. to_dictionary()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(relayed_source_data) == null,
|
||||||
|
"Communicated facts must not form a second relay hop in this phase"
|
||||||
|
)
|
||||||
|
|
||||||
|
var order_independent_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
order_independent_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||||
|
order_independent_data["simulation"]["next_event_id"] = 1
|
||||||
|
order_independent_data["event_knowledge"] = [
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
||||||
|
. to_dictionary()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(0, 0, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||||
|
. to_dictionary()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
KnownEventStateRecord
|
||||||
|
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED)
|
||||||
|
. to_dictionary()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
_check(
|
||||||
|
SimulationStateRecord.from_dictionary(order_independent_data) != null,
|
||||||
|
"Provenance validation should not depend on knowledge array ordering"
|
||||||
|
)
|
||||||
manager.free()
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,3 +128,56 @@ func test_event_knowledge_is_spatial_directed_and_idempotent() -> void:
|
|||||||
assert_true(knowledge_system.knows_event(nearby.id, 12))
|
assert_true(knowledge_system.knows_event(nearby.id, 12))
|
||||||
assert_false(knowledge_system.knows_event(distant.id, 12))
|
assert_false(knowledge_system.knows_event(distant.id, 12))
|
||||||
assert_eq(knowledge_system.get_knowers(12), [actor.id, nearby.id])
|
assert_eq(knowledge_system.get_knowers(12), [actor.id, nearby.id])
|
||||||
|
assert_eq(
|
||||||
|
knowledge_system.get_record(actor.id, 12).get_acquisition_method(),
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||||
|
)
|
||||||
|
assert_eq(
|
||||||
|
knowledge_system.get_record(nearby.id, 12).get_acquisition_method(),
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func test_event_communication_preserves_first_provenance_and_stops_after_one_hop() -> void:
|
||||||
|
var actor := SimNPC.new(0, "Amina", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
||||||
|
var witness := SimNPC.new(1, "Tarik", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
||||||
|
var listener := SimNPC.new(2, "Jasmin", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
||||||
|
var relay_target := SimNPC.new(3, "Elma", SimulationIds.PROFESSION_GUARD, 5.0, 5.0)
|
||||||
|
actor.position = Vector3.ZERO
|
||||||
|
witness.position = Vector3(2.0, 0.0, 0.0)
|
||||||
|
listener.position = Vector3(20.0, 0.0, 0.0)
|
||||||
|
relay_target.position = Vector3(20.0, 0.0, 1.0)
|
||||||
|
var villagers: Array[SimNPC] = [actor, witness, listener, relay_target]
|
||||||
|
var event := EconomicEventRecord.create(
|
||||||
|
21,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
40,
|
||||||
|
actor.id,
|
||||||
|
SimulationIds.npc_inventory_id(actor.id),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
2.0,
|
||||||
|
Vector3.ZERO
|
||||||
|
)
|
||||||
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
|
knowledge_system.observe_event(event, villagers)
|
||||||
|
|
||||||
|
assert_null(knowledge_system.communicate_event(event, listener.id, relay_target.id))
|
||||||
|
var communicated := knowledge_system.communicate_event(event, witness.id, listener.id)
|
||||||
|
assert_not_null(communicated)
|
||||||
|
assert_eq(
|
||||||
|
communicated.get_acquisition_method(), SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
|
)
|
||||||
|
assert_eq(communicated.get_source_npc_id(), witness.id)
|
||||||
|
assert_null(knowledge_system.communicate_event(event, actor.id, listener.id))
|
||||||
|
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_false(knowledge_system.knows_event(relay_target.id, int(event.data["event_id"])))
|
||||||
|
var actor_gap_system := EventKnowledgeSystemScript.new()
|
||||||
|
var direct_witness_records: Array[KnownEventStateRecord] = [
|
||||||
|
KnownEventStateRecord.create(
|
||||||
|
witness.id, int(event.data["event_id"]), SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||||
|
)
|
||||||
|
]
|
||||||
|
actor_gap_system.restore(direct_witness_records)
|
||||||
|
assert_null(actor_gap_system.communicate_event(event, witness.id, actor.id))
|
||||||
|
|||||||
+19
-1
@@ -288,4 +288,22 @@ 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
|
||||||
return "Known fact: %s" % latest_event.description(name_map)
|
var provenance := ""
|
||||||
|
if simulation_manager.has_method("get_known_event_record"):
|
||||||
|
var known_record: KnownEventStateRecord = simulation_manager.get_known_event_record(
|
||||||
|
npc.id, int(latest_event.data["event_id"])
|
||||||
|
)
|
||||||
|
if known_record != null:
|
||||||
|
match known_record.get_acquisition_method():
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||||
|
provenance = " (performed personally)"
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
||||||
|
provenance = " (witnessed)"
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
|
var source_name: String = name_map.get(
|
||||||
|
known_record.get_source_npc_id(), "someone"
|
||||||
|
)
|
||||||
|
provenance = " (heard from %s)" % source_name
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
||||||
|
provenance = " (source not recorded)"
|
||||||
|
return "Known fact: %s%s" % [latest_event.description(name_map), provenance]
|
||||||
|
|||||||
Reference in New Issue
Block a user