跳到主要內容

Mini MCOS + CRM Prototype:從共存真實模型到 AI 世界建構

 這陣子利用 AI 所作的探索主要有兩條途徑。一是知識蒸餾 + MyTime 數位分身,一是共存真實世界。

看似兩條不同路徑,其實指向同一個核心,只不過出發點相反。

知識蒸餾 + MyTime 數位分身:

事件 -> 世界模型

共存真實世界:

世界模型 -> 事件

兩者互為鏡像。

這兩條研究路徑最後都指向同一個問題:如何利用 LLM 的世界模型,重新建構一個可運作、可推理、可互動的世界。

LLM 已經學到大量世界知識,因此非常適合作為世界模型(World Model)的核心推理引擎,也讓這類系統第一次具有較高的可行性。

底下是對得到的部份結果和一些嘗試所作的整理和記錄。

  • 建構虛擬世界 (MCOS): 一個宏大的願景,目標是建構一個像 Matrix 那樣的世界。其中部份內容較科幻,但是對於整個世界的格局和架構有個大概了解。
  • 共存真實模型 (CRM): 一種用於大型互動世界的事件導向一致性模型。這是從一個問題開始發展起來的架構:可否為不同玩家維持不同但不衝突的真實?
  • Mini MCOS + CRM Prototype: 結合 MCOS 與 CRM 的一個小型實驗及模擬程式。非常雛形的一個模擬,每個世界只跑 50 個 ticks,主要目的是驗證 CRM 的核心想法。

§建構虛擬世界 (MCOS)

MCOS(Micro Cosmos Operating System)目前仍屬於概念性架構,目的是探索大型 AI 原生虛擬世界可能需要的整體分層設計,而不是提出已完成的實作方案。

1. 夢境的藍圖:虛擬世界架構的宏觀設計

打造與現實無異的虛擬世界,核心是將其視為一個**「微型宇宙操作系統」 (MCOS)**。為了高效管理與無限擴充,我們將其分為三層:

  • 核心仿真層: 虛擬世界的心臟。負責維持時間、空間的絕對同步,並讓地心引力、能量守恆等基礎物理規則永不停歇地運作。
  • 互動與感知層: 大腦與大腦外的橋樑。透過腦機接口捕捉人的意圖,並將世界的幾何、光影數據,直接轉換成大腦看得懂的五感神經信號,完美欺騙大腦。
  • 內容與社會層: 宇宙的日常。運行非玩家角色(NPC)的 AI、維持社會經濟,並動態生成新聞與隨機事件,讓世界保持運轉。

系統防護機制: 為了不當機,世界被切成數百萬個小區域無縫切換。若某處出現物理 Bug,系統會在幾毫秒內將該區時間倒帶、修正,甚至微微覆蓋使用者的記憶(如產生一陣暈眩或既視感),抹除出錯痕跡。

2. 代碼與感知:構建底層物理引擎與環境模擬

要讓使用者無論用放大鏡還是望遠鏡都找不到破綻,底層的物理引擎(FPE)必須做到大小兼顧:

  • 看哪裡,算哪裡: 為了節省運算資源,引擎採用「薛丁格的貓」邏輯。沒人看的地方只用文字記錄狀態;一旦有人走近,系統才會在千分之一秒內將該區升級為全精度計算(包括分子與量子實驗變化)。
  • 絕對的真實感: 這裡沒有預先畫好的死背景。風雨、海浪都是根據流體力學當場計算的;東西被打碎時(如玻璃、牆壁),碎裂的痕跡與質量也是依據材料特性即時產生。
  • 視覺與光線的極致: 系統會追蹤每一道光線的數百次折射。但為了效率,會聰明地忽略人類潛意識分不出來的微小差異,確保運作流暢不頭暈。

3. 數位生命:複雜 NPC 的行為邏輯與情感模型

一個真實的世界,取決於裡面的居民活不活靈。Matrix 裡的幾十億個 NPC 必須擁有個性和自由意志的假象:

  • 獨立思考(感知-認知-行動, Perception-Cognition-Action, P-C-A): NPC 有自己的觀察視角、信念(Beliefs, 會記錯或誤會)、慾望 (Desires)與短中長期計畫 (Intentions),並依照這些擬定行為。
  • 會聊天、會進化的 AI 大腦: 每個 NPC 都內建專屬的語言模型與記憶庫。農夫只聊收成,不會聊高科技,且他們會跟路人學習新的流行語和社會規範,不斷進化。
  • 喜怒哀樂與記憶: NPC 的情緒由事件觸發,並會影響做事的衝勁。他們擁有自傳式的回憶,也會像人類一樣隨著時間選擇性遺忘不重要的事。
  • 智商調配(社會 LOD): 玩家走近時,NPC 才會瞬間開機、充滿靈魂;遠離玩家時則降為低耗能狀態,只跑背景數據。系統也會模擬 NPC 的生老病死,維持社會的歷史厚重感。

結論:編程宇宙的最終考驗

建構一個與物理現實無異的虛擬世界,這不僅是技術上的終極挑戰,更是哲學和倫理上的終極考驗。

§共存真實模型 (CRM)

相較於 MCOS 偏向長期願景,CRM (Coexistent Reality Model) 則是本文目前真正開始進行實作驗證的一部分。Mini Prototype 的主要目的,就是驗證 CRM 在事件提交(Event Commit)模型下是否具有可行性。

摘要

大型互動世界通常假設所有玩家共享一個全域一致的世界狀態。這種設計雖然易於理解,但在高延遲與高併發環境下,會導致大量同步成本與回滾行為。其實對玩家體驗而言,全域狀態一致性既非必要,也非最佳解。本文提出「共存真實模型(CRM)」,允許多個玩家各自維持暫時不同、但不衝突的真實世界視角,並僅在事件層級上進行承諾與收斂。此模型以事件提交取代狀態同步,避免回滾,並能在網路分割下平滑退化。

1. 問題背景

傳統線上遊戲與虛擬世界架構依賴權威伺服器維持單一世界狀態。當玩家行為產生衝突時,系統必須回溯或修正已顯示的結果,造成體驗中斷。 然而實務觀察顯示,只要因果關係合理、敘事連貫、最終結果一致,玩家通常不會察覺暫時的不一致。

2. 核心觀點

CRM 的核心假設是:

玩家不需要共享完全相同的狀態,只需要共享「最終會產生影響的事件結果」。

3. 共存真實模型

個人真實(Personal Reality, PR)

每位玩家都有自己的即時模擬世界,可自由推進,不需等待同步。

共享真實(Shared Reality, SR)

只有會影響他人的事件,才進入共享層級。

世界帳本(World Ledger, WL)

所有被承諾的事件都寫入帳本,確保最終一致的結果。

4. 為什麼不需要回滾

CRM 不回溯時間,而是重新詮釋事件意義:

  • 攻擊失敗 → 未命中
  • 撿取衝突 → 延遲取得
  • 動作被拒 → 視覺淡出

玩家看到的是「合理結果」,而不是「被修正的錯誤」。

5. 網路分割不是錯誤,而是模式

在 CRM 中,網路分割代表世界暫時分岔,而非系統故障。只要事件最終能被分類、承諾或降級,世界就不會崩潰。

6. 限制與適用範圍

CRM 不適合需要即時全域公平或嚴格決定性的系統,例如金融交易或競技裁判。但對多數互動世界而言,這是一個更符合人類感知的模型。

7. 結論

共存真實模型提供了一種重新思考一致性的方式:

不是讓世界永遠一致,而是讓體驗永遠合理。

我們希望此模型能成為大型互動系統設計的新討論起點。

§Mini MCOS + CRM Prototype

Mini MCOS + CRM Prototype 並不是 MCOS 的完整實現,而是一個極小型的概念驗證(Proof of Concept)。它驗證了 CRM 的事件提交、Personal Reality、World Ledger 與 Autonomous Agent 等核心想法,可以在一個小型模擬世界中運作。

概念

MCOS:分層式虛擬世界架構(核心仿真 / 互動感知 / 內容社會)

CRM:事件導向一致性模型,允許多個真實共存,衝突時重新詮釋而非回滾

系統架構

┌─────────────────────────────────────────────────┐
│  Agent A (PR)  │  Agent B (PR)  │  NPC Agents   │
├─────────────────────────────────────────────────┤
│            Commit Resolver (SR)                  │
│  因果檢查 → 衝突偵測 → COMMIT / DOWNGRADE / REJECT │
├─────────────────────────────────────────────────┤
│            World Ledger (WL)                     │
│            Append-only 事件日誌                   │
├─────────────────────────────────────────────────┤
│            World Engine (Core)                   │
│   10x10 網格 │ 時間步進 │ 資源再生                │
└─────────────────────────────────────────────────┘

核心機制

CRM 事件承諾流程

  1. Agent 自主決策產生 Event
  2. 提交到 Commit Resolver
  3. Resolver 檢查因果性 → 違規則 REJECT
  4. 偵測同 tick 衝突 → DOWNGRADE(重新詮釋)
  5. 無衝突 → COMMIT
  6. 結果寫入 World Ledger,廣播給所有 PR

重新詮義範例

衝突DOWNGRADE 結果
兩人同時採集一人取得,另一人「already taken」
交易目標已被佔用「shop just closed」
攻擊已倒下的目標「attack missed」

BDI 自主決策

每個 Agent 根據 Belief-Desire-Intention 模型運作:

  • 信念:感知範圍內的環境狀態
  • 慾望:生存(飢餓)> 繁榮(工作/採集)> 交易 > 互動 > 探索
  • 意圖:根據信念+慾望+人格特質選擇行動
  • 人格:boldness(攻擊傾向)、sociability(對話頻率)、greed(採集積極度)、patience(等待忍耐度)

角色行為

角色工作行為交易物品
Farmer採集食物食物
Baker採集食物食物
Trader採集木材木材
Craftsman採集礦石礦石
Mayor閒逛

世界事件(隨機觸發,12% 機率/每 tick)

事件效果
sunny晴天,無特殊效果
rainy雨天,移動受阻
windy大風,資源位置打亂
drought乾旱,食物再生率降至 30%
market_boom市場繁榮,所有價格上漲 30-80%
market_crash市場崩盤,所有價格下跌 30-60%
festival慶典,所有 Agent 快樂度 +0.3
fog霧,感知範圍縮小

檔案結構

mcos_crm_prototype/
├── main.py              # 模擬主迴圈
├── world_engine.py      # 空間網格、時間、資源
├── world_ledger.py      # Append-only 事件帳本
├── personal_reality.py  # 每 Agent 的本地快照
├── commit_resolver.py   # CRM 核心:事件評估
├── npc_agent.py         # BDI 自主決策 Agent
├── economy.py           # 經濟系統
└── models.py            # 資料結構

執行

cd mcos_crm_prototype
python main.py

每次執行結果不同,隨機元素包括:

  • Agent 起始位置與資金
  • 人格特質(boldness, sociability, greed, patience)
  • 每 tick 的行動順序
  • 世界事件(drought, market_boom, fog, festival 等)
  • 市場價格波動
  • Agent 決策與行為

結論

MCOS、CRM 與 Mini Prototype 並不是一個完成的系統,而是一次對「AI 原生虛擬世界」的探索。

MCOS 嘗試回答「世界應該如何被建構」;CRM 嘗試回答「多個真實如何共存」;Prototype 則驗證其中部分概念是否具有工程上的可行性。

這些工作距離真正的大型 AI 世界仍然很遙遠,但希望能作為後續研究的一個起點。

對我而言,這三個部分並不是彼此獨立的專案,而是同一條研究路徑上的不同層次:MCOS 描述世界的整體架構,CRM 描述世界的一致性機制,而 MyTime 的數位分身與知識蒸餾,則探索如何從真實世界建立世界模型。三者最終都希望朝向 AI 原生世界(AI-native World)的建構。

A. 模擬結果

  • OK = COMMIT,事件成功執行
  • DOWN = DOWNGRADE,衝突後重新詮釋
  • REJ = REJECT,因果性違規
============================================================
  Mini MCOS + CRM Prototype
  Coexistent Reality Model - Autonomous Simulation
  Agents: 3 players + 2 NPCs
  World: 10x10 grid  |  Ticks: 50
  Seed: 1863589732
============================================================

══════════ Tick   1 ══════════
  Bob     (Farmer   )  OK  MOVE   (direction=east)
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Alice   (Baker    )  OK  MOVE   (direction=east)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Carol   (Trader   )  OK  TALK   (target=bob,topic=life)
  ------------------------------------------------------------
  Economy: supply=[food:53 wood:20 ore:10]  prices=[food:4.8 wood:5.7 ore:7.9]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(4, 1)  credits=128.2  hunger=[..........] 0.08  inv=[empty]  mood=happy(0.5)
  Bob      pos=(8, 0)  credits=124.5  hunger=[..........] 0.07  inv=[empty]  mood=happy(0.5)
  Carol    pos=(9, 2)  credits= 96.8  hunger=[..........] 0.07  inv=[empty]  mood=happy(0.5)
  Smith    pos=(8, 6)  credits=123.2  hunger=[..........] 0.07  inv=[empty]  mood=happy(0.5)
  Mayor    pos=(2, 6)  credits= 98.0  hunger=[..........] 0.07  inv=[empty]  mood=happy(0.5)

══════════ Tick   2 ══════════
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=east)
  Mayor   (Mayor    )  OK  IDLE   ()
  Alice   (Baker    )  OK  MOVE   (direction=east)
  Carol   (Trader   )  OK  MOVE   (direction=north)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:10]  prices=[food:4.3 wood:5.5 ore:7.7]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 1)  credits=128.2  hunger=[#.........] 0.16  inv=[empty]  mood=happy(0.4)
  Bob      pos=(9, 0)  credits=124.5  hunger=[#.........] 0.16  inv=[empty]  mood=happy(0.4)
  Carol    pos=(9, 1)  credits= 96.8  hunger=[#.........] 0.16  inv=[empty]  mood=happy(0.4)
  Smith    pos=(7, 6)  credits=123.2  hunger=[#.........] 0.14  inv=[empty]  mood=happy(0.4)
  Mayor    pos=(2, 6)  credits= 98.0  hunger=[#.........] 0.15  inv=[empty]  mood=happy(0.4)

══════════ Tick   3 ══════════
  ** WORLD EVENT: rainy - rain slows movement **
  Carol   (Trader   )  OK  ATTACK (target=bob,reason=robbery)
  Bob     (Farmer   )  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  MOVE   (direction=south)
  Mayor   (Mayor    )  OK  MOVE   (direction=south)
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:53 wood:20 ore:10]  prices=[food:4.0 wood:5.3 ore:7.5]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 2)  credits=128.2  hunger=[##........] 0.23  inv=[empty]  mood=happy(0.4)
  Bob      pos=(8, 0)  credits=124.5  hunger=[##........] 0.24  inv=[empty]  mood=happy(0.4)
  Carol    pos=(9, 1)  credits= 96.8  hunger=[##........] 0.23  inv=[empty]  mood=happy(0.4)
  Smith    pos=(7, 7)  credits=123.2  hunger=[##........] 0.22  inv=[empty]  mood=happy(0.4)
  Mayor    pos=(2, 7)  credits= 98.0  hunger=[##........] 0.23  inv=[empty]  mood=happy(0.4)

══════════ Tick   4 ══════════
  Carol   (Trader   )  OK  MOVE   (direction=north)
  Smith   (Craftsman)  OK  MOVE   (direction=east)
  Alice   (Baker    )  OK  MOVE   (direction=south)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Bob     (Farmer   )  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:53 wood:20 ore:10]  prices=[food:4.0 wood:5.2 ore:7.3]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 3)  credits=128.2  hunger=[###.......] 0.31  inv=[empty]  mood=happy(0.4)
  Bob      pos=(8, 1)  credits=124.5  hunger=[###.......] 0.32  inv=[empty]  mood=happy(0.4)
  Carol    pos=(9, 0)  credits= 96.8  hunger=[##........] 0.30  inv=[empty]  mood=happy(0.4)
  Smith    pos=(8, 7)  credits=123.2  hunger=[##........] 0.29  inv=[empty]  mood=happy(0.4)
  Mayor    pos=(3, 7)  credits= 98.0  hunger=[###.......] 0.32  inv=[empty]  mood=happy(0.4)

══════════ Tick   5 ══════════
  Bob     (Farmer   )  OK  TALK   (target=carol,topic=life)
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Alice   (Baker    )  OK  MOVE   (direction=east)
  Carol   (Trader   )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:53 wood:23 ore:10]  prices=[food:3.9 wood:5.1 ore:7.3]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(6, 3)  credits=128.2  hunger=[###.......] 0.38  inv=[empty]  mood=happy(0.3)
  Bob      pos=(8, 1)  credits=124.5  hunger=[####......] 0.40  inv=[empty]  mood=happy(0.4)
  Carol    pos=(9, 0)  credits= 96.8  hunger=[###.......] 0.39  inv=[empty]  mood=happy(0.4)
  Smith    pos=(8, 6)  credits=123.2  hunger=[###.......] 0.37  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(4, 7)  credits= 98.0  hunger=[###.......] 0.40  inv=[empty]  mood=happy(0.3)

══════════ Tick   6 ══════════
  Carol   (Trader   )  OK  ATTACK (target=bob,reason=robbery)
  Alice   (Baker    )  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  IDLE   ()
  Bob     (Farmer   )  OK  MOVE   (direction=east)
  Smith   (Craftsman)  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:52 wood:20 ore:10]  prices=[food:3.7 wood:4.6 ore:7.2]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 2)  credits=128.2  hunger=[####......] 0.46  inv=[empty]  mood=happy(0.3)
  Bob      pos=(9, 1)  credits=124.5  hunger=[####......] 0.49  inv=[empty]  mood=angry(0.3)
  Carol    pos=(9, 0)  credits= 96.8  hunger=[####......] 0.47  inv=[empty]  mood=happy(0.3)
  Smith    pos=(9, 6)  credits=123.2  hunger=[####......] 0.46  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(4, 7)  credits= 98.0  hunger=[####......] 0.49  inv=[empty]  mood=happy(0.3)

══════════ Tick   7 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=south)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=east)
  Carol   (Trader   )  OK  MOVE   (direction=east)
  Alice   (Baker    )  OK  MOVE   (direction=north)
  ------------------------------------------------------------
  Economy: supply=[food:52 wood:22 ore:10]  prices=[food:3.7 wood:4.5 ore:7.2]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 1)  credits=128.2  hunger=[#####.....] 0.53  inv=[empty]  mood=happy(0.3)
  Bob      pos=(9, 1)  credits=124.5  hunger=[#####.....] 0.56  inv=[empty]  mood=happy(0.3)
  Carol    pos=(9, 0)  credits= 96.8  hunger=[#####.....] 0.54  inv=[empty]  mood=happy(0.3)
  Smith    pos=(8, 6)  credits=123.2  hunger=[#####.....] 0.56  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(4, 8)  credits= 98.0  hunger=[#####.....] 0.55  inv=[empty]  mood=happy(0.3)

══════════ Tick   8 ══════════
  Alice   (Baker    )  OK  MOVE   (direction=north)
  Bob     (Farmer   )  OK  MOVE   (direction=south)
  Carol   (Trader   )  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:10]  prices=[food:4.0 wood:4.6 ore:7.4]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 0)  credits=128.2  hunger=[######....] 0.62  inv=[empty]  mood=happy(0.3)
  Bob      pos=(9, 2)  credits=124.5  hunger=[######....] 0.63  inv=[empty]  mood=happy(0.3)
  Carol    pos=(9, 0)  credits= 96.8  hunger=[######....] 0.64  inv=[empty]  mood=happy(0.3)
  Smith    pos=(7, 7)  credits=123.2  hunger=[######....] 0.65  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(4, 8)  credits= 98.0  hunger=[######....] 0.65  inv=[empty]  mood=happy(0.3)

══════════ Tick   9 ══════════
  Bob     (Farmer   )  OK  MOVE   (direction=south)
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  MOVE   (direction=south)
  Carol   (Trader   )  OK  MOVE   (direction=west)
  ------------------------------------------------------------
  Economy: supply=[food:52 wood:20 ore:10]  prices=[food:3.9 wood:4.6 ore:7.2]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(5, 1)  credits=128.2  hunger=[######....] 0.68  inv=[empty]  mood=happy(0.2)
  Bob      pos=(9, 3)  credits=124.5  hunger=[#######...] 0.73  inv=[empty]  mood=happy(0.3)
  Carol    pos=(8, 0)  credits= 96.8  hunger=[#######...] 0.71  inv=[empty]  mood=happy(0.3)
  Smith    pos=(5, 7)  credits=123.2  hunger=[#######...] 0.74  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(2, 8)  credits= 98.0  hunger=[#######...] 0.73  inv=[empty]  mood=happy(0.2)

══════════ Tick  10 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=west)
  Carol   (Trader   )  OK  MOVE   (direction=south)
  Alice   (Baker    )  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:13]  prices=[food:4.0 wood:4.4 ore:7.0]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(4, 2)  credits=128.2  hunger=[#######...] 0.75  inv=[empty]  mood=happy(0.2)
  Bob      pos=(8, 3)  credits=124.5  hunger=[########..] 0.80  inv=[empty]  mood=happy(0.2)
  Carol    pos=(8, 1)  credits= 96.8  hunger=[#######...] 0.77  inv=[empty]  mood=happy(0.2)
  Smith    pos=(3, 7)  credits=123.2  hunger=[########..] 0.82  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(0, 8)  credits= 98.0  hunger=[########..] 0.81  inv=[empty]  mood=happy(0.2)

══════════ Tick  11 ══════════
  Alice   (Baker    )  OK  MOVE   (direction=north)
  Carol   (Trader   )  OK  MOVE   (direction=east)
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  Smith   (Craftsman)  OK  MOVE   (direction=east)
  Bob     (Farmer   )  OK  ATTACK (target=carol,reason=robbery)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:21 ore:13]  prices=[food:3.9 wood:4.4 ore:7.0]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(4, 1)  credits=128.2  hunger=[########..] 0.83  inv=[empty]  mood=happy(0.2)
  Bob      pos=(8, 3)  credits=124.5  hunger=[########..] 0.89  inv=[empty]  mood=angry(0.2)
  Carol    pos=(8, 1)  credits= 96.8  hunger=[########..] 0.83  inv=[empty]  mood=angry(0.2)
  Smith    pos=(4, 7)  credits=123.2  hunger=[########..] 0.89  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(0, 8)  credits= 98.0  hunger=[########..] 0.89  inv=[empty]  mood=happy(0.2)

══════════ Tick  12 ══════════
  Carol   (Trader   )  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  ATTACK (target=carol,reason=dispute)
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:10]  prices=[food:4.2 wood:4.3 ore:6.9]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(3, 1)  credits=128.2  hunger=[########..] 0.89  inv=[empty]  mood=happy(0.2)
  Bob      pos=(7, 3)  credits=124.5  hunger=[#########.] 0.98  inv=[empty]  mood=angry(0.3)
  Carol    pos=(8, 0)  credits= 96.8  hunger=[########..] 0.90  inv=[empty]  mood=angry(0.4)
  Smith    pos=(3, 6)  credits=123.2  hunger=[#########.] 0.99  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(0, 8)  credits= 98.0  hunger=[#########.] 0.98  inv=[empty]  mood=happy(0.2)

══════════ Tick  13 ══════════
  Carol   (Trader   )  OK  MOVE   (direction=south)
  Alice   (Baker    )  OK  MOVE   (direction=south)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=south)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:53 wood:21 ore:10]  prices=[food:4.0 wood:4.1 ore:7.0]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[#########.] 0.96  inv=[empty]  mood=happy(0.2)
  Bob      pos=(6, 4)  credits=124.5  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Carol    pos=(7, 1)  credits= 96.8  hunger=[#########.] 0.99  inv=[empty]  mood=angry(0.4)
  Smith    pos=(2, 5)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(2, 8)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.2)

══════════ Tick  14 ══════════
  Carol   (Trader   )  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  ATTACK (target=alice,reason=provoked)
  Bob     (Farmer   )  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:21 ore:10]  prices=[food:4.0 wood:3.8 ore:6.7]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[##########] 1.00  inv=[food:1]  mood=angry(0.2)
  Bob      pos=(5, 3)  credits=124.5  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Carol    pos=(5, 1)  credits= 96.8  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.3)
  Smith    pos=(3, 5)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(2, 8)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.2)

══════════ Tick  15 ══════════
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=south)
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  MOVE   (direction=east)
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  ------------------------------------------------------------
  Economy: supply=[food:54 wood:21 ore:10]  prices=[food:3.9 wood:3.6 ore:7.0]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.2)
  Bob      pos=(4, 4)  credits=124.5  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Carol    pos=(5, 1)  credits= 96.8  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.3)
  Smith    pos=(2, 4)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.1)
  Mayor    pos=(0, 8)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.1)

══════════ Tick  16 ══════════
  Carol   (Trader   )  OK  ATTACK (target=alice,reason=provoked)
  Bob     (Farmer   )  OK  MOVE   (direction=south)
  Alice   (Baker    )  OK  GATHER (resource_type=food,position=[3, 3])
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:10]  prices=[food:3.9 wood:3.4 ore:6.9]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[######....] 0.68  inv=[food:1]  mood=angry(0.4)
  Bob      pos=(3, 5)  credits=124.5  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Carol    pos=(4, 1)  credits= 96.8  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.3)
  Smith    pos=(3, 5)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.1)
  Mayor    pos=(0, 8)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.1)

══════════ Tick  17 ══════════
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Bob     (Farmer   )  OK  ATTACK (target=npc_smith,reason=robbery)
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  Carol   (Trader   )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:21 ore:10]  prices=[food:3.9 wood:3.2 ore:6.8]  M-Credits=570.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[###.......] 0.37  inv=[empty]  mood=angry(0.3)
  Bob      pos=(3, 4)  credits=124.5  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Carol    pos=(4, 1)  credits= 96.8  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.3)
  Smith    pos=(3, 3)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Mayor    pos=(1, 7)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.1)

══════════ Tick  18 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  WORK   (payment=7,resource_type=food,position=[3, 3])
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman) DOWN GATHER (resource_type=food,position=[3, 3])  [resource depleted just before arrival]
  Carol   (Trader   )  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:48 wood:20 ore:10]  prices=[food:3.8 wood:3.1 ore:6.7]  M-Credits=577.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=135.2  hunger=[#####.....] 0.51  inv=[food:1]  mood=angry(0.3)
  Bob      pos=(3, 3)  credits=124.5  hunger=[##########] 1.00  inv=[food:1]  mood=angry(0.2)
  Carol    pos=(3, 2)  credits= 96.8  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Smith    pos=(3, 3)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=angry(0.2)
  Mayor    pos=(0, 8)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.1)

══════════ Tick  19 ══════════
  ** WORLD EVENT: fog - perception radius reduced **
  Smith   (Craftsman)  OK  GATHER (resource_type=food,position=[3, 3])
  Alice   (Baker    ) DOWN GATHER (resource_type=food,position=[3, 3])  [found only scraps remains]
  Bob     (Farmer   )  OK  EAT    (reason=hungry)
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  Carol   (Trader   ) DOWN GATHER (resource_type=food,position=[3, 3])  [found only scraps remains]
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:3.8 wood:3.1 ore:6.7]  M-Credits=577.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=135.2  hunger=[######....] 0.60  inv=[food:1]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=124.5  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.2)
  Carol    pos=(3, 3)  credits= 96.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.2)
  Smith    pos=(3, 3)  credits=123.2  hunger=[##########] 1.00  inv=[food:1]  mood=sad(0.2)
  Mayor    pos=(1, 7)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.1)

══════════ Tick  20 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  EAT    (reason=hungry)
  Carol   (Trader   ) DOWN GATHER (resource_type=food,position=[3, 3])  [found only scraps remains]
  Alice   (Baker    )  OK  TRADE  (target=carol,item=food,cost=7)
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:3.9 wood:2.9 ore:6.6]  M-Credits=577.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[######....] 0.65  inv=[food:2]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=124.5  hunger=[######....] 0.67  inv=[food:1]  mood=happy(0.2)
  Carol    pos=(3, 3)  credits=103.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.3)
  Smith    pos=(3, 3)  credits=123.2  hunger=[######....] 0.60  inv=[empty]  mood=sad(0.2)
  Mayor    pos=(2, 8)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.1)

══════════ Tick  21 ══════════
  Bob     (Farmer   )  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Mayor   (Mayor    )  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:46 wood:20 ore:10]  prices=[food:4.0 wood:2.8 ore:6.6]  M-Credits=577.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=128.2  hunger=[###.......] 0.34  inv=[food:1]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=124.5  hunger=[###.......] 0.35  inv=[empty]  mood=happy(0.2)
  Carol    pos=(3, 3)  credits=103.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.3)
  Smith    pos=(2, 3)  credits=123.2  hunger=[######....] 0.69  inv=[empty]  mood=sad(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.1)

══════════ Tick  22 ══════════
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=7)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Mayor   (Mayor    )  OK  GATHER (resource_type=food,position=[1, 9])
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Bob     (Farmer   )  OK  WORK   (payment=3,resource_type=food,position=[3, 3])
  ------------------------------------------------------------
  Economy: supply=[food:44 wood:20 ore:10]  prices=[food:3.7 wood:2.7 ore:6.6]  M-Credits=580.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=121.2  hunger=[####......] 0.43  inv=[food:2]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=134.5  hunger=[####......] 0.48  inv=[empty]  mood=happy(0.3)
  Carol    pos=(3, 3)  credits=103.8  hunger=[##########] 1.00  inv=[food:1]  mood=sad(0.3)
  Smith    pos=(1, 3)  credits=123.2  hunger=[#######...] 0.76  inv=[empty]  mood=sad(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[##########] 1.00  inv=[food:1]  mood=sad(0.1)

══════════ Tick  23 ══════════
  Smith   (Craftsman)  OK  GATHER (resource_type=food,position=[1, 2])
  Alice   (Baker    )  OK  TRADE  (target=carol,item=food,cost=5)
  Mayor   (Mayor    )  OK  EAT    (reason=hungry)
  Bob     (Farmer   )  OK  ATTACK (target=npc_smith,reason=provoked)
  Carol   (Trader   ) REJ  EAT    (reason=hungry)  [violates causality]
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:3.6 wood:2.3 ore:6.2]  M-Credits=580.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=116.2  hunger=[#####.....] 0.50  inv=[food:3]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=134.5  hunger=[#####.....] 0.54  inv=[empty]  mood=happy(0.3)
  Carol    pos=(3, 3)  credits=108.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.3)
  Smith    pos=(1, 2)  credits=123.2  hunger=[########..] 0.82  inv=[food:1]  mood=angry(0.3)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.2)

══════════ Tick  24 ══════════
  Mayor   (Mayor    )  OK  GATHER (resource_type=food,position=[1, 9])
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=5)
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  EAT    (reason=hungry)
  Bob     (Farmer   ) DOWN GATHER (resource_type=food,position=[3, 3])  [found only scraps remains]
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:10]  prices=[food:3.5 wood:2.4 ore:6.3]  M-Credits=580.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=111.2  hunger=[#####.....] 0.56  inv=[food:4]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=139.5  hunger=[######....] 0.61  inv=[empty]  mood=happy(0.3)
  Carol    pos=(3, 3)  credits=108.8  hunger=[##########] 1.00  inv=[food:1]  mood=sad(0.3)
  Smith    pos=(1, 2)  credits=123.2  hunger=[####......] 0.49  inv=[empty]  mood=angry(0.3)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[######....] 0.70  inv=[food:1]  mood=happy(0.2)

══════════ Tick  25 ══════════
  Carol   (Trader   )  OK  EAT    (reason=hungry)
  Alice   (Baker    )  OK  TRADE  (target=carol,item=food,cost=4)
  Bob     (Farmer   )  OK  WORK   (payment=5,resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  EAT    (reason=hungry)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:21 ore:10]  prices=[food:3.5 wood:2.6 ore:6.1]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=107.2  hunger=[######....] 0.65  inv=[food:5]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=144.5  hunger=[#######...] 0.73  inv=[empty]  mood=happy(0.3)
  Carol    pos=(3, 3)  credits=112.8  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.3)
  Smith    pos=(1, 1)  credits=123.2  hunger=[#####.....] 0.56  inv=[empty]  mood=angry(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[###.......] 0.39  inv=[empty]  mood=happy(0.2)

══════════ Tick  26 ══════════
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Carol   (Trader   )  OK  MOVE   (direction=north)
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:11]  prices=[food:3.4 wood:2.3 ore:6.1]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=107.2  hunger=[###.......] 0.33  inv=[food:4]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=144.5  hunger=[########..] 0.81  inv=[food:1]  mood=happy(0.3)
  Carol    pos=(3, 2)  credits=112.8  hunger=[######....] 0.68  inv=[empty]  mood=happy(0.3)
  Smith    pos=(0, 1)  credits=123.2  hunger=[######....] 0.63  inv=[empty]  mood=angry(0.2)
  Mayor    pos=(1, 8)  credits= 98.0  hunger=[####......] 0.48  inv=[empty]  mood=happy(0.2)

══════════ Tick  27 ══════════
  Bob     (Farmer   )  OK  EAT    (reason=hungry)
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=5)
  Mayor   (Mayor    )  OK  IDLE   ()
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Carol   (Trader   )  OK  TALK   (target=bob,topic=work)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:11]  prices=[food:3.3 wood:2.3 ore:5.9]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits=102.2  hunger=[####......] 0.41  inv=[food:5]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=149.5  hunger=[####......] 0.49  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 2)  credits=112.8  hunger=[#######...] 0.75  inv=[empty]  mood=happy(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[#######...] 0.71  inv=[empty]  mood=angry(0.2)
  Mayor    pos=(1, 8)  credits= 98.0  hunger=[#####.....] 0.57  inv=[empty]  mood=happy(0.2)

══════════ Tick  28 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  Smith   (Craftsman)  OK  GATHER (resource_type=food,position=[0, 2])
  Bob     (Farmer   )  OK  TALK   (target=carol,topic=life)
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=7)
  Carol   (Trader   )  OK  MOVE   (direction=west)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:26 ore:12]  prices=[food:3.2 wood:2.0 ore:5.9]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 95.2  hunger=[####......] 0.48  inv=[food:6]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=156.5  hunger=[#####.....] 0.58  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 2)  credits=112.8  hunger=[########..] 0.84  inv=[empty]  mood=happy(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[#######...] 0.78  inv=[food:1]  mood=happy(0.2)
  Mayor    pos=(1, 7)  credits= 98.0  hunger=[######....] 0.65  inv=[empty]  mood=happy(0.2)

══════════ Tick  29 ══════════
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=5)
  Bob     (Farmer   )  OK  ATTACK (target=alice,reason=dispute)
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  EAT    (reason=hungry)
  Mayor   (Mayor    )  OK  IDLE   ()
  ------------------------------------------------------------
  Economy: supply=[food:48 wood:26 ore:12]  prices=[food:2.9 wood:1.9 ore:5.7]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 90.2  hunger=[#####.....] 0.58  inv=[food:7]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=161.5  hunger=[######....] 0.64  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=112.8  hunger=[#########.] 0.91  inv=[empty]  mood=happy(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[####......] 0.46  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(1, 7)  credits= 98.0  hunger=[#######...] 0.74  inv=[empty]  mood=happy(0.1)

══════════ Tick  30 ══════════
  Alice   (Baker    )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=west)
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  Carol   (Trader   ) DOWN GATHER (resource_type=food,position=[3, 3])  [resource depleted just before arrival]
  ------------------------------------------------------------
  Economy: supply=[food:51 wood:20 ore:10]  prices=[food:2.9 wood:1.6 ore:5.9]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 90.2  hunger=[######....] 0.65  inv=[food:8]  mood=happy(0.3)
  Bob      pos=(2, 3)  credits=161.5  hunger=[#######...] 0.71  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=112.8  hunger=[#########.] 0.99  inv=[empty]  mood=sad(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[#####.....] 0.54  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(1, 6)  credits= 98.0  hunger=[########..] 0.82  inv=[empty]  mood=happy(0.1)

══════════ Tick  31 ══════════
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Smith   (Craftsman)  OK  GATHER (resource_type=food,position=[0, 2])
  Bob     (Farmer   ) DOWN GATHER (resource_type=food,position=[3, 3])  [found only scraps remains]
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:10]  prices=[food:2.7 wood:1.5 ore:5.9]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 90.2  hunger=[###.......] 0.33  inv=[food:7]  mood=happy(0.4)
  Bob      pos=(3, 3)  credits=161.5  hunger=[########..] 0.80  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=112.8  hunger=[##########] 1.00  inv=[food:1]  mood=sad(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[######....] 0.62  inv=[food:1]  mood=happy(0.2)
  Mayor    pos=(2, 7)  credits= 98.0  hunger=[#########.] 0.90  inv=[empty]  mood=happy(0.1)

══════════ Tick  32 ══════════
  Smith   (Craftsman)  OK  GATHER (resource_type=food,position=[0, 2])
  Alice   (Baker    )  OK  TRADE  (target=carol,item=food,cost=6)
  Carol   (Trader   ) REJ  EAT    (reason=hungry)  [violates causality]
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Mayor   (Mayor    )  OK  MOVE   (direction=south)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:10]  prices=[food:2.2 wood:1.2 ore:5.9]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 84.2  hunger=[####......] 0.42  inv=[food:8]  mood=happy(0.4)
  Bob      pos=(3, 3)  credits=161.5  hunger=[########..] 0.87  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=118.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[######....] 0.69  inv=[food:2]  mood=happy(0.2)
  Mayor    pos=(1, 8)  credits= 98.0  hunger=[#########.] 0.98  inv=[empty]  mood=sad(0.1)

══════════ Tick  33 ══════════
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=5)
  Smith   (Craftsman)  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Mayor   (Mayor    )  OK  GATHER (resource_type=food,position=[1, 9])
  Bob     (Farmer   ) DOWN GATHER (resource_type=food,position=[3, 3])  [resource depleted just before arrival]
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:2.1 wood:1.3 ore:5.8]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 79.2  hunger=[####......] 0.50  inv=[food:9]  mood=happy(0.4)
  Bob      pos=(3, 3)  credits=166.5  hunger=[#########.] 0.94  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=118.8  hunger=[##########] 1.00  inv=[food:1]  mood=happy(0.3)
  Smith    pos=(0, 2)  credits=123.2  hunger=[###.......] 0.36  inv=[food:1]  mood=happy(0.3)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[##########] 1.00  inv=[food:1]  mood=happy(0.1)

══════════ Tick  34 ══════════
  Alice   (Baker    )  OK  TRADE  (target=carol,item=food,cost=6)
  Carol   (Trader   ) REJ  EAT    (reason=hungry)  [violates causality]
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Mayor   (Mayor    )  OK  EAT    (reason=hungry)
  Smith   (Craftsman)  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:10]  prices=[food:1.9 wood:1.1 ore:5.6]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 73.2  hunger=[#####.....] 0.57  inv=[food:10]  mood=happy(0.4)
  Bob      pos=(3, 3)  credits=166.5  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=124.8  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.3)
  Smith    pos=(1, 2)  credits=123.2  hunger=[####......] 0.44  inv=[food:1]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.2)

══════════ Tick  35 ══════════
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Mayor   (Mayor    )  OK  IDLE   ()
  Smith   (Craftsman)  OK  TALK   (target=carol,topic=life)
  Alice   (Baker    ) REJ  TRADE  (target=npc_smith,item=food,cost=4)  [violates causality]
  Carol   (Trader   ) DOWN GATHER (resource_type=food,position=[3, 3])  [already taken]
  ------------------------------------------------------------
  Economy: supply=[food:48 wood:20 ore:10]  prices=[food:1.8 wood:1.0 ore:5.4]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 73.2  hunger=[######....] 0.65  inv=[food:10]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=166.5  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=124.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.4)
  Smith    pos=(1, 2)  credits=123.2  hunger=[####......] 0.50  inv=[food:1]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[######....] 0.68  inv=[empty]  mood=happy(0.2)

══════════ Tick  36 ══════════
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Carol   (Trader   ) DOWN GATHER (resource_type=food,position=[3, 3])  [already taken]
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:48 wood:20 ore:10]  prices=[food:1.9 wood:1.0 ore:5.4]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 73.2  hunger=[###.......] 0.35  inv=[food:9]  mood=happy(0.4)
  Bob      pos=(3, 3)  credits=166.5  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=124.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.4)
  Smith    pos=(1, 3)  credits=123.2  hunger=[#####.....] 0.57  inv=[food:1]  mood=happy(0.2)
  Mayor    pos=(2, 9)  credits= 98.0  hunger=[#######...] 0.77  inv=[empty]  mood=happy(0.1)

══════════ Tick  37 ══════════
  ** WORLD EVENT: sunny - clear skies, productivity up **
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Mayor   (Mayor    )  OK  GATHER (resource_type=food,position=[1, 9])
  Carol   (Trader   ) DOWN GATHER (resource_type=food,position=[3, 3])  [already taken]
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=4)
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:1.8 wood:1.0 ore:5.1]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 69.2  hunger=[####......] 0.41  inv=[food:10]  mood=happy(0.4)
  Bob      pos=(3, 3)  credits=170.5  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=124.8  hunger=[##########] 1.00  inv=[empty]  mood=sad(0.5)
  Smith    pos=(1, 4)  credits=123.2  hunger=[######....] 0.66  inv=[food:1]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[########..] 0.87  inv=[food:1]  mood=happy(0.2)

══════════ Tick  38 ══════════
  Mayor   (Mayor    )  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[3, 3])
  Alice   (Baker    ) REJ  TRADE  (target=npc_smith,item=food,cost=7)  [violates causality]
  Bob     (Farmer   ) DOWN GATHER (resource_type=food,position=[3, 3])  [resource depleted just before arrival]
  Smith   (Craftsman)  OK  EAT    (reason=hungry)
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:1.7 wood:1.0 ore:5.3]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 69.2  hunger=[####......] 0.50  inv=[food:10]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=170.5  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=124.8  hunger=[##########] 1.00  inv=[food:1]  mood=sad(0.4)
  Smith    pos=(1, 4)  credits=123.2  hunger=[###.......] 0.34  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[#####.....] 0.56  inv=[empty]  mood=happy(0.2)

══════════ Tick  39 ══════════
  Mayor   (Mayor    )  OK  IDLE   ()
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Alice   (Baker    ) REJ  TRADE  (target=npc_smith,item=food,cost=4)  [violates causality]
  Carol   (Trader   )  OK  EAT    (reason=hungry)
  ------------------------------------------------------------
  Economy: supply=[food:46 wood:21 ore:10]  prices=[food:1.7 wood:1.0 ore:5.0]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 69.2  hunger=[#####.....] 0.57  inv=[food:10]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=170.5  hunger=[##########] 1.00  inv=[food:1]  mood=happy(0.4)
  Carol    pos=(3, 3)  credits=124.8  hunger=[######....] 0.60  inv=[empty]  mood=sad(0.4)
  Smith    pos=(1, 5)  credits=123.2  hunger=[####......] 0.42  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[######....] 0.64  inv=[empty]  mood=happy(0.2)

══════════ Tick  40 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Alice   (Baker    ) REJ  TRADE  (target=npc_smith,item=food,cost=6)  [violates causality]
  Bob     (Farmer   )  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  MOVE   (direction=north)
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:1.5 wood:1.0 ore:5.1]  M-Credits=585.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 69.2  hunger=[######....] 0.64  inv=[food:10]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=170.5  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.4)
  Carol    pos=(4, 2)  credits=124.8  hunger=[######....] 0.69  inv=[empty]  mood=sad(0.3)
  Smith    pos=(0, 5)  credits=123.2  hunger=[####......] 0.49  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(2, 9)  credits= 98.0  hunger=[#######...] 0.73  inv=[empty]  mood=happy(0.2)

══════════ Tick  41 ══════════
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  GATHER (resource_type=food,position=[1, 9])
  Bob     (Farmer   )  OK  WORK   (payment=6,resource_type=food,position=[3, 3])
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  MOVE   (direction=north)
  ------------------------------------------------------------
  Economy: supply=[food:45 wood:20 ore:10]  prices=[food:1.6 wood:1.0 ore:5.2]  M-Credits=591.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 69.2  hunger=[###.......] 0.32  inv=[food:9]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=176.5  hunger=[#######...] 0.72  inv=[food:1]  mood=happy(0.4)
  Carol    pos=(4, 1)  credits=124.8  hunger=[#######...] 0.77  inv=[empty]  mood=sad(0.3)
  Smith    pos=(0, 4)  credits=123.2  hunger=[#####.....] 0.58  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[########..] 0.82  inv=[food:1]  mood=happy(0.2)

══════════ Tick  42 ══════════
  Mayor   (Mayor    )  OK  EAT    (reason=hungry)
  Bob     (Farmer   )  OK  EAT    (reason=hungry)
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Carol   (Trader   )  OK  MOVE   (direction=west)
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=7)
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:1.3 wood:1.1 ore:5.1]  M-Credits=591.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 62.2  hunger=[####......] 0.41  inv=[food:10]  mood=happy(0.3)
  Bob      pos=(3, 3)  credits=183.5  hunger=[###.......] 0.40  inv=[empty]  mood=happy(0.5)
  Carol    pos=(2, 1)  credits=124.8  hunger=[########..] 0.85  inv=[empty]  mood=sad(0.3)
  Smith    pos=(0, 4)  credits=123.2  hunger=[######....] 0.66  inv=[empty]  mood=happy(0.2)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[#####.....] 0.52  inv=[empty]  mood=happy(0.2)

══════════ Tick  43 ══════════
  ** WORLD EVENT: festival - NPC happiness boosted **
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[1, 1])
  Smith   (Craftsman)  OK  MOVE   (direction=west)
  Bob     (Farmer   )  OK  MOVE   (direction=north)
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=5)
  ------------------------------------------------------------
  Economy: supply=[food:46 wood:20 ore:13]  prices=[food:1.0 wood:1.1 ore:4.8]  M-Credits=591.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 57.2  hunger=[####......] 0.48  inv=[food:11]  mood=happy(0.6)
  Bob      pos=(3, 2)  credits=188.5  hunger=[####......] 0.47  inv=[empty]  mood=happy(0.7)
  Carol    pos=(1, 1)  credits=124.8  hunger=[#########.] 0.93  inv=[food:1]  mood=happy(0.5)
  Smith    pos=(0, 3)  credits=123.2  hunger=[#######...] 0.75  inv=[empty]  mood=happy(0.4)
  Mayor    pos=(1, 8)  credits= 98.0  hunger=[#####.....] 0.59  inv=[empty]  mood=happy(0.5)

══════════ Tick  44 ══════════
  Bob     (Farmer   )  OK  WORK   (payment=3,resource_type=food,position=[3, 3])
  Alice   (Baker    ) REJ  TRADE  (target=npc_smith,item=food,cost=4)  [violates causality]
  Carol   (Trader   )  OK  EAT    (reason=hungry)
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Mayor   (Mayor    )  OK  GATHER (resource_type=food,position=[1, 9])
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:20 ore:10]  prices=[food:1.0 wood:1.0 ore:4.6]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 57.2  hunger=[#####.....] 0.56  inv=[food:11]  mood=happy(0.5)
  Bob      pos=(3, 3)  credits=191.5  hunger=[#####.....] 0.58  inv=[empty]  mood=happy(0.7)
  Carol    pos=(1, 1)  credits=124.8  hunger=[#####.....] 0.59  inv=[empty]  mood=happy(0.5)
  Smith    pos=(0, 4)  credits=123.2  hunger=[########..] 0.84  inv=[empty]  mood=happy(0.4)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[######....] 0.68  inv=[food:1]  mood=happy(0.5)

══════════ Tick  45 ══════════
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Alice   (Baker    ) REJ  TRADE  (target=carol,item=food,cost=4)  [violates causality]
  Carol   (Trader   )  OK  TALK   (target=alice,topic=life)
  Bob     (Farmer   )  OK  MOVE   (direction=west)
  Mayor   (Mayor    )  OK  EAT    (reason=hungry)
  ------------------------------------------------------------
  Economy: supply=[food:49 wood:21 ore:10]  prices=[food:1.1 wood:1.0 ore:4.5]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 57.2  hunger=[######....] 0.63  inv=[food:11]  mood=happy(0.5)
  Bob      pos=(2, 3)  credits=191.5  hunger=[######....] 0.65  inv=[empty]  mood=happy(0.6)
  Carol    pos=(1, 1)  credits=124.8  hunger=[######....] 0.67  inv=[empty]  mood=happy(0.5)
  Smith    pos=(0, 4)  credits=123.2  hunger=[#########.] 0.90  inv=[empty]  mood=happy(0.4)
  Mayor    pos=(1, 9)  credits= 98.0  hunger=[###.......] 0.34  inv=[empty]  mood=happy(0.5)

══════════ Tick  46 ══════════
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  Smith   (Craftsman)  OK  MOVE   (direction=south)
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[1, 1])
  Alice   (Baker    ) DOWN GATHER (resource_type=food,position=[3, 3])  [already taken]
  ------------------------------------------------------------
  Economy: supply=[food:48 wood:20 ore:10]  prices=[food:1.0 wood:1.0 ore:4.4]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 57.2  hunger=[######....] 0.69  inv=[food:11]  mood=happy(0.5)
  Bob      pos=(3, 3)  credits=191.5  hunger=[#######...] 0.74  inv=[empty]  mood=happy(0.6)
  Carol    pos=(1, 1)  credits=124.8  hunger=[#######...] 0.76  inv=[food:1]  mood=happy(0.5)
  Smith    pos=(0, 4)  credits=123.2  hunger=[#########.] 0.98  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(2, 9)  credits= 98.0  hunger=[####......] 0.41  inv=[empty]  mood=happy(0.4)

══════════ Tick  47 ══════════
  Alice   (Baker    )  OK  EAT    (reason=hungry)
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Mayor   (Mayor    )  OK  MOVE   (direction=north)
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Carol   (Trader   )  OK  EAT    (reason=hungry)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:10]  prices=[food:1.0 wood:1.0 ore:4.5]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 57.2  hunger=[###.......] 0.36  inv=[food:10]  mood=happy(0.5)
  Bob      pos=(3, 3)  credits=191.5  hunger=[########..] 0.82  inv=[food:1]  mood=happy(0.6)
  Carol    pos=(1, 1)  credits=124.8  hunger=[####......] 0.45  inv=[empty]  mood=happy(0.5)
  Smith    pos=(0, 2)  credits=123.2  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(2, 8)  credits= 98.0  hunger=[####......] 0.50  inv=[empty]  mood=happy(0.4)

══════════ Tick  48 ══════════
  ** WORLD EVENT: rainy - rain slows movement **
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=4)
  Carol   (Trader   )  OK  MOVE   (direction=south)
  Smith   (Craftsman)  OK  GATHER (resource_type=food,position=[0, 2])
  Bob     (Farmer   ) REJ  EAT    (reason=hungry)  [violates causality]
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:10]  prices=[food:1.0 wood:1.0 ore:4.8]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 53.2  hunger=[####......] 0.43  inv=[food:11]  mood=happy(0.5)
  Bob      pos=(3, 3)  credits=195.5  hunger=[#########.] 0.90  inv=[empty]  mood=happy(0.6)
  Carol    pos=(1, 2)  credits=124.8  hunger=[#####.....] 0.54  inv=[empty]  mood=happy(0.5)
  Smith    pos=(0, 2)  credits=123.2  hunger=[##########] 1.00  inv=[food:1]  mood=happy(0.3)
  Mayor    pos=(3, 8)  credits= 98.0  hunger=[#####.....] 0.60  inv=[empty]  mood=happy(0.4)

══════════ Tick  49 ══════════
  Smith   (Craftsman)  OK  EAT    (reason=hungry)
  Carol   (Trader   )  OK  TALK   (target=bob,topic=work)
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=4)
  Mayor   (Mayor    )  OK  MOVE   (direction=south)
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  ------------------------------------------------------------
  Economy: supply=[food:50 wood:20 ore:10]  prices=[food:1.2 wood:1.0 ore:4.6]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 49.2  hunger=[#####.....] 0.52  inv=[food:12]  mood=happy(0.5)
  Bob      pos=(3, 3)  credits=199.5  hunger=[#########.] 0.99  inv=[empty]  mood=happy(0.6)
  Carol    pos=(1, 2)  credits=124.8  hunger=[######....] 0.62  inv=[empty]  mood=happy(0.4)
  Smith    pos=(0, 2)  credits=123.2  hunger=[######....] 0.60  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(2, 9)  credits= 98.0  hunger=[######....] 0.68  inv=[empty]  mood=happy(0.3)

══════════ Tick  50 ══════════
  Smith   (Craftsman)  OK  MOVE   (direction=north)
  Bob     (Farmer   )  OK  GATHER (resource_type=food,position=[3, 3])
  Carol   (Trader   )  OK  GATHER (resource_type=food,position=[1, 2])
  Alice   (Baker    )  OK  TRADE  (target=bob,item=food,cost=5)
  Mayor   (Mayor    )  OK  MOVE   (direction=east)
  ------------------------------------------------------------
  Economy: supply=[food:47 wood:20 ore:10]  prices=[food:1.3 wood:1.0 ore:4.6]  M-Credits=594.7
  ------------------------------------------------------------
  Alice    pos=(3, 3)  credits= 44.2  hunger=[#####.....] 0.60  inv=[food:13]  mood=happy(0.5)
  Bob      pos=(3, 3)  credits=204.5  hunger=[##########] 1.00  inv=[empty]  mood=happy(0.7)
  Carol    pos=(1, 2)  credits=124.8  hunger=[#######...] 0.71  inv=[food:1]  mood=happy(0.4)
  Smith    pos=(0, 1)  credits=123.2  hunger=[######....] 0.69  inv=[empty]  mood=happy(0.3)
  Mayor    pos=(3, 9)  credits= 98.0  hunger=[#######...] 0.76  inv=[empty]  mood=happy(0.3)

============================================================
  SIMULATION COMPLETE
============================================================
  Total events : 250
  COMMIT       : 227
  DOWNGRADE    : 13
  REJECT       : 10
------------------------------------------------------------
  Final Agent States:
  Alice    credits= 44.2  inv=[food:13]  mood=happy(0.5)  events=50
  Bob      credits=204.5  inv=[empty]  mood=happy(0.7)  events=50
  Carol    credits=124.8  inv=[food:1]  mood=happy(0.4)  events=50
  Smith    credits=123.2  inv=[empty]  mood=happy(0.3)  events=50
  Mayor    credits= 98.0  inv=[empty]  mood=happy(0.3)  events=50
============================================================

B. Source Code Greatest Notes

>>> commit_resolver.py

from __future__ import annotations
import random
from models import Event, CommitResult, CommitStatus, Intent
from world_engine import WorldEngine
from world_ledger import WorldLedger


DOWNGRADE_MESSAGES = {
    Intent.GATHER: [
        "already taken",
        "resource depleted just before arrival",
        "found only scraps remains",
    ],
    Intent.TRADE: [
        "shop just closed",
        "trader already sold out",
        "deal fell through at last moment",
    ],
    Intent.ATTACK: [
        "attack missed",
        "target dodged at the last second",
        "blow glanced off harmlessly",
    ],
    Intent.TALK: [
        "target walked away mid-sentence",
        "too noisy to hear",
        "conversation drifted to nothing",
    ],
    Intent.MOVE: [
        "path blocked, retracing steps",
        "lost footing briefly",
    ],
    Intent.EAT: [
        "food spoiled unexpectedly",
        "ate but still slightly hungry",
    ],
    Intent.WORK: [
        "work partially completed",
        "tool broke mid-task",
    ],
    Intent.IDLE: [],
}


class CommitResolver:
    def __init__(self, world: WorldEngine, ledger: WorldLedger) -> None:
        self.world = world
        self.ledger = ledger
        self._pending: list[Event] = []
        self._committed_this_tick: dict[str, list[str]] = {}

    def submit(self, event: Event) -> None:
        self._pending.append(event)

    def resolve_all(self) -> list[CommitResult]:
        results: list[CommitResult] = []
        self._committed_this_tick.clear()
        for event in self._pending:
            result = self._evaluate(event)
            self.ledger.append(result)
            results.append(result)
            if result.status == CommitStatus.COMMIT:
                key = f"{event.intent.value}:{self._event_key(event)}"
                self._committed_this_tick.setdefault(key, []).append(event.actor)
            elif result.status == CommitStatus.DOWNGRADE:
                agent = self.world.get_agent(event.actor)
                if agent:
                    agent.emotion.boost("sadness", 0.08)
            elif result.status == CommitStatus.REJECT:
                agent = self.world.get_agent(event.actor)
                if agent:
                    agent.emotion.boost("anger", 0.05)
                    agent.emotion.boost("sadness", 0.03)
        self._pending.clear()
        return results

    def _event_key(self, event: Event) -> str:
        if event.intent == Intent.GATHER:
            return str(event.context.get("position", ""))
        if event.intent == Intent.TRADE:
            return str(event.context.get("target", ""))
        if event.intent in (Intent.ATTACK, Intent.TALK):
            return str(event.context.get("target", ""))
        if event.intent == Intent.MOVE:
            return event.context.get("direction", "")
        return ""

    def _evaluate(self, event: Event) -> CommitResult:
        agent = self.world.get_agent(event.actor)
        if agent is None:
            return CommitResult(CommitStatus.REJECT, event, "actor not found")

        if not self._check_causality(event, agent):
            return CommitResult(CommitStatus.REJECT, event, "violates causality")

        if self._detect_conflict(event):
            return self._downgrade(event)

        self._execute(event, agent)
        return CommitResult(CommitStatus.COMMIT, event, "no conflict")

    def _check_causality(self, event: Event, agent) -> bool:
        if event.intent == Intent.GATHER:
            pos = event.context.get("position")
            if pos and tuple(pos) != agent.position:
                return False
            rtype = event.context.get("resource_type")
            res = self.world.find_resource_at(agent.position, rtype)
            if res is None or res.depleted:
                return False

        if event.intent == Intent.MOVE:
            direction = event.context.get("direction", "")
            if direction not in ("north", "south", "east", "west"):
                return False

        if event.intent == Intent.TRADE:
            target_id = event.context.get("target", "")
            target = self.world.get_agent(target_id)
            if target is None:
                return False
            dist = abs(target.position[0] - agent.position[0]) + abs(
                target.position[1] - agent.position[1]
            )
            if dist > 1:
                return False

        if event.intent == Intent.EAT:
            if agent.inventory.get("food", 0) <= 0:
                return False

        return True

    def _detect_conflict(self, event: Event) -> bool:
        key = f"{event.intent.value}:{self._event_key(event)}"
        existing = self._committed_this_tick.get(key, [])
        if not existing:
            return False

        if event.intent == Intent.GATHER:
            return True

        if event.intent == Intent.TRADE:
            return True

        if event.intent in (Intent.ATTACK, Intent.TALK):
            return True

        return False

    def _downgrade(self, event: Event) -> CommitResult:
        messages = DOWNGRADE_MESSAGES.get(event.intent, ["unforeseen complication"])
        msg = random.choice(messages) if messages else "reinterpretation applied"

        reinterpreted = Event(
            actor=event.actor,
            intent=event.intent,
            context={**event.context, "downgrade_reason": msg},
            timestamp=event.timestamp,
            id=event.id,
        )
        return CommitResult(CommitStatus.DOWNGRADE, reinterpreted, msg)

    def _execute(self, event: Event, agent) -> None:
        if event.intent == Intent.MOVE:
            direction = event.context.get("direction", "")
            self.world.move_agent(agent, direction)

        elif event.intent == Intent.GATHER:
            rtype = event.context.get("resource_type", "food")
            res = self.world.find_resource_at(agent.position, rtype)
            if res:
                amount = res.harvest(1)
                if amount > 0:
                    agent.inventory[rtype] = agent.inventory.get(rtype, 0) + amount
                    agent.emotion.boost("happiness", 0.03)

        elif event.intent == Intent.EAT:
            if agent.inventory.get("food", 0) > 0:
                agent.inventory["food"] -= 1
                agent.hunger = max(0.0, agent.hunger - 0.4)
                agent.emotion.boost("happiness", 0.05)

        elif event.intent == Intent.WORK:
            agent.credits += event.context.get("payment", 5)
            rtype = event.context.get("resource_type")
            if rtype:
                pos = tuple(event.context.get("position", agent.position))
                res = self.world.find_resource_at(pos, rtype)
                if res:
                    amount = res.harvest(1)
                    if amount > 0:
                        agent.inventory[rtype] = agent.inventory.get(rtype, 0) + amount
                        agent.emotion.boost("happiness", 0.03)
            agent.hunger = min(1.0, agent.hunger + 0.05)
            agent.emotion.boost("sadness", 0.02)

        elif event.intent == Intent.TRADE:
            cost = event.context.get("cost", 5)
            item = event.context.get("item", "food")
            target_id = event.context.get("target", "")
            if agent.credits >= cost:
                agent.credits -= cost
                agent.inventory[item] = agent.inventory.get(item, 0) + 1
                target = self.world.get_agent(target_id)
                if target:
                    target.credits += cost
                    target.inventory[item] = target.inventory.get(item, 0) - 1
                    target.emotion.boost("happiness", 0.04)
                agent.emotion.boost("happiness", 0.03)

        elif event.intent == Intent.TALK:
            target_id = event.context.get("target", "")
            target = self.world.get_agent(target_id)
            if target:
                agent.emotion.boost("happiness", 0.02)
                target.emotion.boost("happiness", 0.02)

        elif event.intent == Intent.ATTACK:
            target_id = event.context.get("target", "")
            target = self.world.get_agent(target_id)
            if target:
                target.emotion.boost("anger", 0.2)
                target.emotion.boost("fear", 0.1)
                agent.emotion.boost("anger", 0.05)

>>> economy.py

from __future__ import annotations
import random
from world_engine import WorldEngine


class Economy:
    def __init__(self, world: WorldEngine) -> None:
        self.world = world
        self.prices: dict[str, float] = {
            "food": 5.0,
            "wood": 6.0,
            "ore": 8.0,
        }
        self.tax_rate: float = 0.1
        self._base_demand: dict[str, float] = {
            "food": 15.0,
            "wood": 10.0,
            "ore": 8.0,
        }

    def supply(self) -> dict[str, int]:
        return self.world.resource_summary()

    def demand(self) -> dict[str, float]:
        demand = dict(self._base_demand)
        for agent in self.world.agents.values():
            hunger_factor = agent.hunger * 3
            demand["food"] += hunger_factor
            if agent.inventory.get("food", 0) == 0:
                demand["food"] += 2
        return demand

    def update_prices(self) -> dict[str, float]:
        supply = self.supply()
        demand = self.demand()
        for rtype in self.prices:
            s = max(1, supply.get(rtype, 1))
            d = max(0.1, demand.get(rtype, 1))
            ratio = d / s
            adjustment = (ratio - 1.0) * 0.3
            noise = random.gauss(0, 0.15)
            self.prices[rtype] = max(1.0, min(20.0, self.prices[rtype] + adjustment + noise))
        return dict(self.prices)

    def tax_revenue(self, amount: float) -> float:
        return amount * self.tax_rate

    def summary(self) -> dict:
        supply = self.supply()
        demand = self.demand()
        total_credits = sum(a.credits for a in self.world.agents.values())
        return {
            "prices": dict(self.prices),
            "supply": supply,
            "demand": {k: round(v, 1) for k, v in demand.items()},
            "total_credits": round(total_credits, 1),
        }

>>> main.py

from __future__ import annotations
import random
from models import Agent, CommitStatus
from world_engine import WorldEngine
from world_ledger import WorldLedger
from commit_resolver import CommitResolver
from npc_agent import BDIAgent
from economy import Economy


TOTAL_TICKS = 50

AGENTS_SPEC = [
    {"id": "alice", "name": "Alice", "role": "Baker"},
    {"id": "bob", "name": "Bob", "role": "Farmer"},
    {"id": "carol", "name": "Carol", "role": "Trader"},
]

NPCS_SPEC = [
    {"id": "npc_smith", "name": "Smith", "role": "Craftsman"},
    {"id": "npc_mayor", "name": "Mayor", "role": "Mayor"},
]

DIVIDER = "=" * 60
THIN_DIVIDER = "-" * 60

WORLD_EVENTS = [
    ("sunny", "clear skies, productivity up"),
    ("rainy", "rain slows movement"),
    ("windy", "gusts scatter resources"),
    ("drought", "food regen halved"),
    ("market_boom", "all prices surge"),
    ("market_crash", "all prices drop"),
    ("festival", "NPC happiness boosted"),
    ("fog", "perception radius reduced"),
]


def build_world() -> tuple[WorldEngine, WorldLedger, CommitResolver, Economy, list[BDIAgent]]:
    world = WorldEngine(width=10, height=10)
    ledger = WorldLedger()
    resolver = CommitResolver(world, ledger)
    economy = Economy(world)
    agents: list[BDIAgent] = []

    start_positions = [
        (random.randint(0, world.width - 1), random.randint(0, world.height - 1))
        for _ in AGENTS_SPEC + NPCS_SPEC
    ]

    for i, spec in enumerate(AGENTS_SPEC + NPCS_SPEC):
        agent = Agent(
            id=spec["id"],
            name=spec["name"],
            role=spec["role"],
            position=start_positions[i],
            credits=round(random.uniform(60, 140), 1),
        )
        world.add_agent(agent)
        agents.append(BDIAgent(agent, world))

    return world, ledger, resolver, economy, agents


def maybe_world_event(economy: Economy) -> str | None:
    if random.random() > 0.12:
        return None
    event_name, desc = random.choice(WORLD_EVENTS)
    if event_name == "drought":
        for r in economy.world.resources:
            if r.resource_type == "food":
                r.regeneration_rate *= 0.3
    elif event_name == "market_boom":
        for k in economy.prices:
            economy.prices[k] *= random.uniform(1.3, 1.8)
    elif event_name == "market_crash":
        for k in economy.prices:
            economy.prices[k] *= random.uniform(0.4, 0.7)
    elif event_name == "festival":
        for a in economy.world.agents.values():
            a.emotion.boost("happiness", 0.3)
    elif event_name == "windy":
        random.shuffle(economy.world.resources)
    return f"** WORLD EVENT: {event_name} - {desc} **"


def print_agent_line(name: str, role: str, result, event) -> str:
    status_sym = {
        CommitStatus.COMMIT: " OK ",
        CommitStatus.DOWNGRADE: "DOWN",
        CommitStatus.REJECT: "REJ ",
    }
    sym = status_sym.get(result.status, " ???")
    ctx_parts = []
    for k, v in event.context.items():
        if k == "downgrade_reason":
            continue
        ctx_parts.append(f"{k}={v}")
    ctx_str = ",".join(ctx_parts)
    reason = ""
    if result.status == CommitStatus.DOWNGRADE:
        reason = f"  [{result.reason}]"
    elif result.status == CommitStatus.REJECT:
        reason = f"  [{result.reason}]"
    return f"  {name:8s}({role:9s}) {sym} {event.intent.value:7s}({ctx_str}){reason}"


def print_tick_header(tick: int) -> None:
    print(f"\n{'═' * 10} Tick {tick:3d} {'═' * 10}")


def print_economy(economy: Economy) -> None:
    s = economy.summary()
    supply_str = " ".join(f"{k}:{v}" for k, v in s["supply"].items())
    price_str = " ".join(f"{k}:{v:.1f}" for k, v in s["prices"].items())
    print(f"  {THIN_DIVIDER}")
    print(f"  Economy: supply=[{supply_str}]  prices=[{price_str}]  M-Credits={s['total_credits']}")


def print_agent_states(agents: list[BDIAgent]) -> None:
    print(f"  {THIN_DIVIDER}")
    for ba in agents:
        a = ba.agent
        inv_str = ",".join(f"{k}:{v}" for k, v in a.inventory.items() if v > 0) or "empty"
        hunger_bar = "#" * int(a.hunger * 10) + "." * (10 - int(a.hunger * 10))
        emotion_str = str(a.emotion)
        print(
            f"  {a.name:8s} pos={a.position}  credits={a.credits:5.1f}  "
            f"hunger=[{hunger_bar}] {a.hunger:.2f}  inv=[{inv_str}]  mood={emotion_str}"
        )


def print_summary(ledger: WorldLedger, agents: list[BDIAgent]) -> None:
    print(f"\n{DIVIDER}")
    print("  SIMULATION COMPLETE")
    print(f"{DIVIDER}")
    print(f"  Total events : {len(ledger)}")
    print(f"  COMMIT       : {ledger.committed_count()}")
    print(f"  DOWNGRADE    : {ledger.downgraded_count()}")
    print(f"  REJECT       : {ledger.rejected_count()}")
    print(f"{THIN_DIVIDER}")
    print("  Final Agent States:")
    for ba in agents:
        a = ba.agent
        inv_str = ",".join(f"{k}:{v}" for k, v in a.inventory.items() if v > 0) or "empty"
        print(
            f"  {a.name:8s} credits={a.credits:5.1f}  inv=[{inv_str}]  "
            f"mood={a.emotion}  events={len(a.last_events)}"
        )
    print(f"{DIVIDER}")


def main() -> None:
    import time as _time
    seed = int(_time.time() * 1000) % (2**31)
    random.seed(seed)
    world, ledger, resolver, economy, agents = build_world()

    print(f"{DIVIDER}")
    print("  Mini MCOS + CRM Prototype")
    print("  Coexistent Reality Model - Autonomous Simulation")
    print(f"  Agents: {len(AGENTS_SPEC)} players + {len(NPCS_SPEC)} NPCs")
    print(f"  World: {world.width}x{world.height} grid  |  Ticks: {TOTAL_TICKS}")
    print(f"  Seed: {seed}")
    print(f"{DIVIDER}")

    for tick in range(1, TOTAL_TICKS + 1):
        world.advance()
        print_tick_header(tick)

        world_event = maybe_world_event(economy)
        if world_event:
            print(f"  {world_event}")

        order = list(agents)
        random.shuffle(order)
        for bdi in order:
            ev = bdi.tick()
            if ev:
                resolver.submit(ev)

        results = resolver.resolve_all()

        for result in results:
            for bdi in agents:
                bdi.pr.integrate_commit(result)

        result_order = sorted(
            results, key=lambda r: next(
                (i for i, b in enumerate(order) if b.agent.id == r.resolved_event.actor),
                len(order),
            )
        )
        for result in result_order:
            ev = result.resolved_event
            agent = world.get_agent(ev.actor)
            if agent:
                name = agent.name
                role = agent.role
            else:
                name = ev.actor
                role = "?"
            print(print_agent_line(name, role, result, ev))

        if not results:
            print("  (no events this tick)")

        economy.update_prices()
        print_economy(economy)
        print_agent_states(agents)

    print_summary(ledger, agents)


if __name__ == "__main__":
    main()

>>> models.py

from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
import uuid


class CommitStatus(Enum):
    COMMIT = "COMMIT"
    DOWNGRADE = "DOWNGRADE"
    REJECT = "REJECT"


class Intent(Enum):
    MOVE = "MOVE"
    GATHER = "GATHER"
    WORK = "WORK"
    TRADE = "TRADE"
    TALK = "TALK"
    EAT = "EAT"
    IDLE = "IDLE"
    ATTACK = "ATTACK"


DIRECTION_VECTORS = {
    "north": (0, -1),
    "south": (0, 1),
    "east": (1, 0),
    "west": (-1, 0),
}


@dataclass
class Event:
    actor: str
    intent: Intent
    context: dict[str, Any] = field(default_factory=dict)
    timestamp: int = 0
    id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])

    def __str__(self) -> str:
        ctx = ",".join(f"{k}={v}" for k, v in self.context.items())
        return f"[{self.id}] {self.actor} {self.intent.value}({ctx})"


@dataclass
class CommitResult:
    status: CommitStatus
    resolved_event: Event
    reason: str = ""


@dataclass
class EmotionState:
    happiness: float = 0.5
    anger: float = 0.0
    fear: float = 0.0
    sadness: float = 0.0

    def decay(self, rate: float = 0.05) -> None:
        self.happiness = max(0.0, self.happiness * 0.92)
        self.anger = max(0.0, self.anger * 0.88)
        self.fear = max(0.0, self.fear * 0.88)
        self.sadness = max(0.0, self.sadness * 0.88)

    def boost(self, emotion: str, amount: float) -> None:
        current = getattr(self, emotion, 0.0)
        setattr(self, emotion, max(0.0, min(1.0, current + amount)))

    def dominant(self) -> tuple[str, float]:
        m = {
            "happy": self.happiness,
            "angry": self.anger,
            "afraid": self.fear,
            "sad": self.sadness,
        }
        best = max(m, key=m.get)
        return best, m[best]

    def __str__(self) -> str:
        name, val = self.dominant()
        return f"{name}({val:.1f})"


@dataclass
class Agent:
    id: str
    name: str
    role: str
    position: tuple[int, int] = (0, 0)
    credits: float = 100.0
    inventory: dict[str, int] = field(default_factory=dict)
    hunger: float = 0.0
    beliefs: dict[str, Any] = field(default_factory=dict)
    desires: list[str] = field(default_factory=lambda: ["survive", "prosper"])
    emotion: EmotionState = field(default_factory=EmotionState)
    lod_level: int = 2
    last_events: list[str] = field(default_factory=list)

    def perceive_radius(self) -> int:
        return 3 if self.lod_level >= 2 else 1

    def hunger_rate(self) -> float:
        return 0.08

    def is_npc(self) -> bool:
        return self.id.startswith("npc_")

    def __str__(self) -> str:
        return f"{self.name}({self.role})@{self.position}"


@dataclass
class ResourceNode:
    resource_type: str
    position: tuple[int, int]
    quantity: int = 10
    regeneration_rate: float = 0.5
    depleted: bool = False
    _regen_buf: float = 0.0

    def regen(self) -> None:
        if self.depleted:
            self.quantity = min(10, self.quantity + 1)
            if self.quantity > 2:
                self.depleted = False
        else:
            self._regen_buf += self.regeneration_rate
            if self._regen_buf >= 1.0:
                self.quantity = min(10, self.quantity + int(self._regen_buf))
                self._regen_buf -= int(self._regen_buf)

    def harvest(self, amount: int = 1) -> int:
        actual = min(amount, self.quantity)
        self.quantity -= actual
        if self.quantity <= 0:
            self.depleted = True
        return actual

>>> npc_agent.py

from __future__ import annotations
import random
from models import Agent, Event, Intent, DIRECTION_VECTORS
from world_engine import WorldEngine
from personal_reality import PersonalReality


ROLE_BEHAVIOR = {
    "Farmer": {
        "work_intent": Intent.GATHER,
        "work_context": {"resource_type": "food"},
        "trade_item": "food",
        "trade_price_range": (3, 6),
    },
    "Baker": {
        "work_intent": Intent.GATHER,
        "work_context": {"resource_type": "food"},
        "trade_item": "food",
        "trade_price_range": (4, 7),
    },
    "Trader": {
        "work_intent": Intent.GATHER,
        "work_context": {"resource_type": "wood"},
        "trade_item": "wood",
        "trade_price_range": (5, 8),
    },
    "Craftsman": {
        "work_intent": Intent.GATHER,
        "work_context": {"resource_type": "ore"},
        "trade_item": "ore",
        "trade_price_range": (6, 10),
    },
    "Mayor": {
        "work_intent": Intent.IDLE,
        "work_context": {},
        "trade_item": None,
        "trade_price_range": (0, 0),
    },
}


class BDIAgent:
    def __init__(self, agent: Agent, world: WorldEngine) -> None:
        self.agent = agent
        self.world = world
        self.pr = PersonalReality(agent)
        self._work_counter = 0
        self._personality = {
            "boldness": random.uniform(0.2, 0.8),
            "sociability": random.uniform(0.2, 0.8),
            "greed": random.uniform(0.2, 0.8),
            "patience": random.uniform(0.2, 0.8),
        }

    def tick(self) -> Event | None:
        self._sync_pr()
        event = self._decide()
        self.agent.emotion.decay(0.03)
        if self.agent.hunger > 0.7:
            self.agent.emotion.boost("sadness", 0.02)
        if self.agent.hunger > 0.9:
            self.agent.emotion.boost("fear", 0.01)
        return event

    def _sync_pr(self) -> None:
        nearby_a = self.world.nearby_agents(self.agent)
        nearby_r = self.world.nearby_resources(self.agent)
        self.pr.sync_from_world(nearby_a, nearby_r)

    def _decide(self) -> Event:
        ts = self.world.tick

        if self.agent.hunger > 0.7 and self.agent.inventory.get("food", 0) > 0:
            return Event(
                actor=self.agent.id,
                intent=Intent.EAT,
                context={"reason": "hungry"},
                timestamp=ts,
            )

        if self.agent.hunger > 0.9:
            return self._find_food(ts)

        if self.agent.hunger > 0.6 and random.random() < 0.4:
            return self._find_food(ts)

        role_cfg = ROLE_BEHAVIOR.get(self.agent.role, ROLE_BEHAVIOR["Mayor"])

        if self._should_trade():
            return self._do_trade(ts, role_cfg)

        if self._should_work():
            return self._do_work(ts, role_cfg)

        if random.random() < self._personality["sociability"] * 0.5:
            return self._do_talk(ts)

        if random.random() < 0.15:
            return self._do_attack(ts)

        return self._do_explore(ts)

    def _should_work(self) -> bool:
        self._work_counter += 1
        work_frequency = random.randint(2, 5)
        return self._work_counter % work_frequency == 0

    def _should_trade(self) -> bool:
        return self.agent.inventory.get(self._role_trade_item(), 0) > 0

    def _role_trade_item(self) -> str:
        cfg = ROLE_BEHAVIOR.get(self.agent.role, {})
        return cfg.get("trade_item", "") or ""

    def _find_food(self, ts: int) -> Event:
        resources = self.world.nearby_resources(self.agent, radius=5)
        food = [r for r in resources if r.resource_type == "food"]
        if food:
            nearest = min(
                food,
                key=lambda r: abs(r.position[0] - self.agent.position[0])
                + abs(r.position[1] - self.agent.position[1]),
            )
            self._move_toward(nearest.position)
            if nearest.position == self.agent.position:
                return Event(
                    actor=self.agent.id,
                    intent=Intent.GATHER,
                    context={"resource_type": "food", "position": list(nearest.position)},
                    timestamp=ts,
                )
        return self._do_explore(ts)

    def _do_work(self, ts: int, cfg: dict) -> Event:
        intent = cfg["work_intent"]
        ctx = dict(cfg["work_context"])
        if intent == Intent.GATHER:
            resources = self.world.nearby_resources(self.agent)
            typed = [r for r in resources if r.resource_type == ctx.get("resource_type")]
            if typed:
                target = min(
                    typed,
                    key=lambda r: abs(r.position[0] - self.agent.position[0])
                    + abs(r.position[1] - self.agent.position[1]),
                )
                self._move_toward(target.position)
                if target.position == self.agent.position:
                    return Event(
                        actor=self.agent.id,
                        intent=Intent.WORK,
                        context={
                            "payment": random.randint(3, 7),
                            "resource_type": ctx.get("resource_type"),
                            "position": list(target.position),
                        },
                        timestamp=ts,
                    )
            return self._do_explore(ts)
        if intent == Intent.TRADE:
            targets = self.world.nearby_agents(self.agent)
            trade_targets = [t for t in targets if t.id != self.agent.id]
            if trade_targets:
                target = random.choice(trade_targets)
                ctx["target"] = target.id
                return Event(
                    actor=self.agent.id, intent=intent, context=ctx, timestamp=ts
                )
            return self._do_explore(ts)
        return Event(actor=self.agent.id, intent=intent, context=ctx, timestamp=ts)

    def _do_trade(self, ts: int, cfg: dict) -> Event:
        targets = self.world.nearby_agents(self.agent)
        trade_targets = [t for t in targets if t.id != self.agent.id]
        if not trade_targets:
            return self._do_explore(ts)
        target = random.choice(trade_targets)
        item = cfg.get("trade_item", "food")
        lo, hi = cfg.get("trade_price_range", (3, 6))
        cost = random.randint(lo, hi)
        return Event(
            actor=self.agent.id,
            intent=Intent.TRADE,
            context={"target": target.id, "item": item, "cost": cost},
            timestamp=ts,
        )

    def _do_talk(self, ts: int) -> Event:
        targets = self.world.nearby_agents(self.agent)
        if not targets:
            return self._do_explore(ts)
        target = random.choice(targets)
        return Event(
            actor=self.agent.id,
            intent=Intent.TALK,
            context={"target": target.id, "topic": random.choice(["weather", "work", "life"])},
            timestamp=ts,
        )

    def _do_explore(self, ts: int) -> Event:
        if random.random() < self._personality["boldness"] * 0.3:
            targets = self.world.nearby_agents(self.agent, radius=2)
            others = [t for t in targets if t.id != self.agent.id]
            if others:
                return self._do_attack(ts)
        direction = random.choice(list(DIRECTION_VECTORS.keys()))
        return Event(
            actor=self.agent.id,
            intent=Intent.MOVE,
            context={"direction": direction},
            timestamp=ts,
        )

    def _do_attack(self, ts: int) -> Event:
        targets = self.world.nearby_agents(self.agent, radius=2)
        others = [t for t in targets if t.id != self.agent.id]
        if not others:
            return self._do_explore(ts)
        target = random.choice(others)
        return Event(
            actor=self.agent.id,
            intent=Intent.ATTACK,
            context={"target": target.id, "reason": random.choice(["provoked", "robbery", "dispute"])},
            timestamp=ts,
        )

    def _move_toward(self, target: tuple[int, int]) -> None:
        ax, ay = self.agent.position
        tx, ty = target
        if ax < tx:
            self.world.move_agent(self.agent, "east")
        elif ax > tx:
            self.world.move_agent(self.agent, "west")
        elif ay < ty:
            self.world.move_agent(self.agent, "south")
        elif ay > ty:
            self.world.move_agent(self.agent, "north")

>>> personal_reality.py

from __future__ import annotations
from models import Agent, Event, ResourceNode


class PersonalReality:
    def __init__(self, agent: Agent) -> None:
        self.agent = agent
        self.known_agents: dict[str, Agent] = {}
        self.known_resources: dict[tuple[int, int], ResourceNode] = {}
        self.pending_events: list[Event] = []
        self.local_time: int = 0

    def sync_from_world(self, nearby_agents: list[Agent], nearby_resources: list[ResourceNode]) -> None:
        self.known_agents.clear()
        for a in nearby_agents:
            self.known_agents[a.id] = a
        self.known_resources.clear()
        for r in nearby_resources:
            self.known_resources[r.position] = r

    def integrate_commit(self, result) -> None:
        ev = result.resolved_event
        if ev.actor == self.agent.id:
            self.agent.last_events.append(
                f"T{ev.timestamp}:{result.status.value}:{ev.intent.value}"
            )

    def perceive(self) -> dict:
        return {
            "position": self.agent.position,
            "hunger": self.agent.hunger,
            "credits": self.agent.credits,
            "inventory": dict(self.agent.inventory),
            "nearby_agents": list(self.known_agents.keys()),
            "nearby_resources": [
                (r.position, r.resource_type, r.quantity)
                for r in self.known_resources.values()
            ],
        }

>>> world_engine.py

from __future__ import annotations
import random
from models import Agent, ResourceNode, DIRECTION_VECTORS


class WorldEngine:
    def __init__(self, width: int = 10, height: int = 10):
        self.width = width
        self.height = height
        self.tick = 0
        self.agents: dict[str, Agent] = {}
        self.resources: list[ResourceNode] = []
        self._build_resource_nodes()

    def _build_resource_nodes(self) -> None:
        spots = set()
        for _ in range(8):
            while True:
                pos = (random.randint(0, self.width - 1), random.randint(0, self.height - 1))
                if pos not in spots:
                    spots.add(pos)
                    break
            rtype = random.choice(["food", "wood", "ore"])
            self.resources.append(ResourceNode(resource_type=rtype, position=pos))

    def add_agent(self, agent: Agent) -> None:
        self.agents[agent.id] = agent

    def get_agent(self, agent_id: str) -> Agent | None:
        return self.agents.get(agent_id)

    def move_agent(self, agent: Agent, direction: str) -> bool:
        dx, dy = DIRECTION_VECTORS.get(direction, (0, 0))
        nx, ny = agent.position[0] + dx, agent.position[1] + dy
        if 0 <= nx < self.width and 0 <= ny < self.height:
            agent.position = (nx, ny)
            return True
        return False

    def find_resource_at(self, pos: tuple[int, int], rtype: str | None = None) -> ResourceNode | None:
        for r in self.resources:
            if r.position == pos and not r.depleted:
                if rtype is None or r.resource_type == rtype:
                    return r
        return None

    def agents_at(self, pos: tuple[int, int], exclude: str = "") -> list[Agent]:
        return [a for a in self.agents.values() if a.position == pos and a.id != exclude]

    def nearby_agents(self, agent: Agent, radius: int | None = None) -> list[Agent]:
        if radius is None:
            radius = agent.perceive_radius()
        ax, ay = agent.position
        result = []
        for a in self.agents.values():
            if a.id == agent.id:
                continue
            dx = abs(a.position[0] - ax)
            dy = abs(a.position[1] - ay)
            if dx <= radius and dy <= radius:
                result.append(a)
        return result

    def nearby_resources(self, agent: Agent, radius: int | None = None) -> list[ResourceNode]:
        if radius is None:
            radius = agent.perceive_radius()
        ax, ay = agent.position
        return [
            r for r in self.resources
            if not r.depleted
            and abs(r.position[0] - ax) <= radius
            and abs(r.position[1] - ay) <= radius
        ]

    def update_lod(self) -> None:
        for agent in self.agents.values():
            nearby = self.nearby_agents(agent, radius=2)
            if len(nearby) > 0:
                agent.lod_level = 2
            else:
                agent.lod_level = 1

    def tick_resources(self) -> None:
        for r in self.resources:
            r.regen()
            if random.random() < 0.08:
                r.quantity = min(15, r.quantity + random.randint(1, 3))

    def tick_hunger(self) -> None:
        for agent in self.agents.values():
            hunger_var = random.gauss(0, 0.01)
            agent.hunger = min(1.0, max(0.0, agent.hunger + agent.hunger_rate() + hunger_var))

    def advance(self) -> int:
        self.tick += 1
        self.tick_resources()
        self.tick_hunger()
        self.update_lod()
        return self.tick

    def resource_summary(self) -> dict[str, int]:
        summary: dict[str, int] = {}
        for r in self.resources:
            if not r.depleted:
                summary[r.resource_type] = summary.get(r.resource_type, 0) + r.quantity
        return summary

>>> world_ledger.py

from __future__ import annotations
from models import Event, CommitResult, CommitStatus


class WorldLedger:
    def __init__(self) -> None:
        self.entries: list[CommitResult] = []
        self._event_index: dict[str, CommitResult] = {}

    def append(self, result: CommitResult) -> None:
        self.entries.append(result)
        self._event_index[result.resolved_event.id] = result

    def get(self, event_id: str) -> CommitResult | None:
        return self._event_index.get(event_id)

    def recent(self, n: int = 5) -> list[CommitResult]:
        return self.entries[-n:]

    def events_by(self, actor: str) -> list[CommitResult]:
        return [e for e in self.entries if e.resolved_event.actor == actor]

    def committed_count(self) -> int:
        return sum(1 for e in self.entries if e.status == CommitStatus.COMMIT)

    def downgraded_count(self) -> int:
        return sum(1 for e in self.entries if e.status == CommitStatus.DOWNGRADE)

    def rejected_count(self) -> int:
        return sum(1 for e in self.entries if e.status == CommitStatus.REJECT)

    def __len__(self) -> int:
        return len(self.entries)

留言

這個網誌中的熱門文章

WTL簡介

good Game Editor的視窗編輯器是以WTL作為框架實作的,因為WTL的資料非常稀少,本文就針對WTL作個簡單的入門介紹。 歷史背景 ATL(Active Template Library,或者ActiveX Template Library ) ,它本來的目的是為了要讓COM元件以及ActiveX元件的撰寫變得更容易。因為ATL是拿來寫COM元件的,所以它只有幾個非常基本的GUI類別,相當於MFC的CWnd和CDialog。 很幸運的是,這幾個 GUI類別有很足夠的彈性可以讓像WTL這樣的東西架構在它之上。 WTL實際上是ATL的一組擴充,它也和ATL一樣都是以C++ Template寫成。它擁有許多像MFC的強大GUI類別所提供的能力,同時還能編譯出更小的執行檔。如果你學習過 MFC的程式設計,那麼你會很習慣於像MFC那樣的元件包裝還有很彈性訊息處理機制,也就是說你會比較容易進入WTL的世界。 WTL有二個主要的修訂版本,版本3及7。版本的號碼對應到ATL的版本號碼,這也是為什麼不叫作版本1及2。一直到版本7.1之後微軟將WTL變成一個開源碼的專案,寄駐在Sourceforge上,而目前最新的版本已到8.1。 安裝 你可以在底下的位址下載到最新版本( http://sourceforge.net/projects/wtl/ ),目前最新的版本是8.1版,而good是使用8.0版本製作。在網站上提供了exe版的自解壓縮檔和zip格式的檔案。下載下來後,解壓縮到電腦上任意的位置。 解壓縮後,AppWiz、AppWizCE 和 AppWizMobile 三個資料夾,提供了以 Java Script撰寫成的針對不同版本Visual Studio的安裝程式。根據自己電腦上安裝的 VS.NET 版本,選擇對應的安裝程式來自動安裝 WTL 精靈到你的VS.NET資料夾。Samples資料夾裡有一些有趣的教學範例程式,有興趣的話可以自行研究看看。 因為WTL完全是以C++ Template實作,所以只有C++ Header檔,不需要任何Lib檔,所以我們只需要把include資料夾的位置加到自己專案的搜尋路徑裡就完成了安裝。因為 WTL可以是個通用的模組,所以一般來說我會選擇把它加入到全域的搜尋路徑裡,這樣就不必每個專案都新增一次搜尋路徑。 以 Visual Stud...

單人撲克牌遊戲 - 蒙地卡羅

更多可在網頁玩的 單人撲克牌遊戲 ; 新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

KillSudoku 4顆星精彩數獨詳解 - 鍊技巧

這題數獨(sudoku)題目估計為4+顆星,有點難度。解題需要應用多種技巧,過程非常精彩有趣,是個好題。 底下使用 KillSudoku 作詳細圖解。 1,使用基本排除法則,可以簡單填入6個數字。到此為止,開始使用 候選數法 來解題。如下所示,為填入6個數字後的狀態圖。 2,如下圖,使用進階排除法,在第9列和第4行可以先排除幾個候選數。 3,如圖,在第2行有一個 Naked Subset (3,4),可以對3,4候選數作排除。附帶提一下,反過來看在同一行裡面也可以說有另一個Hidden Subset(2,5,8)存在。Naked Subset和Hidden Subset常是一體二面同時存在,只不過對我們來說,Naked Subset是相對比較容易看的出來。 排除第2行的3,4後,又可以對第2列以外的3作排除,如下圖。 4,接著,在第5行又發現了一個 Naked Subset (3,7,8)。 對第5行三個Subset以外的候選數3,7,8作排除後,又接著產生可以對第5行以外的3作排除。 5,這一題解到此為止,開始進入高潮。大部份能解到3顆星題目的人,猜想應該就此卡住。以下開始需要應用更高級的鍊技巧,才能夠繼續進行。 應用X-Chains鍊技巧,可以找到一條由4條強連結組成的鍊,可以排除候選數2。這裡的鍊指的是由2條以上的強連結組成,而所謂的強連結是指在同一行、或同一列或同一個Box裡,由唯二的候選數構成的連結。如上圖中的第9行中,只有二個2,這二個2構成一條強連結。為什麼說這是一條強連結?因為在這條連結的AB二個端點中,肯定會有一個2存在,要麼是A點要麼是B點。鍊技巧就是將多條強連結串連起來作候選數排除的技巧,而X-Chains是高級的鍊技巧裡面的基本技巧。 接上圖,這樣一來就又可以應用基本排除方法,填入3個數字,如下圖所示。 6,接下來就是本題最精彩的部份,以下需要連續找到3條鍊,才能繼續往下解。 7,找出3條鍊後,剩下來的部份就沒什麼特別的了,只需要應用基本法就能把所有剩餘數字填完。