Status: ✅ Complete / Final Release Tech Stack: Python 3.10+ (Core Logic), Gemini 2.5 Flash Lite (AI Intelligence), TOON (Data Serialization) Architecture Pattern: Hybrid-Arbitrated "Two-Path" System
The system is engineered as a "Stateless Machine" to ensure mechanical accuracy, strictly separating narrative creativity from mathematical logic.
- Stateless Execution: The Rules Engine is a pure function pipeline. It receives input, calculates a result, and returns output without retaining internal state.
- Single Source of Truth: The
GameState(Python Dataclass) is the absolute authority. AI is never allowed to "remember" game state independently. - Deterministic Logic: All numerical calculations (HP, Dice Rolls, AC checks) occur 100% in Python code. AI is strictly forbidden from performing arithmetic.
graph TD
UserInput[User Input] --> Router{Intent Router}
%% Path A: Fixed Rules
Router -- "Match: Rule (Attack/Move)" --> PathA[Path A: Fixed Action]
PathA --> Engine[Rules Engine (Python)]
%% Path B: Creative
Router -- "Match: Creative/Unknown" --> PathB[Path B: Creative Action]
PathB --> Arbiter[LLM Arbitrator]
Arbiter -- "Define Mechanics (DC/Stat)" --> Engine
%% Convergence
Engine -- "Result (Success/Fail/Dmg)" --> Narrator[LLM Narrator]
Narrator --> Client[Client UI/CLI]
%% Data Flow
State[(GameState)] -.-> Toon[TOON Converter]
Toon -.-> Narrator
Toon -.-> Arbiter
We prioritize Python dataclasses for internal logic speed, but utilize the TOON format for external AI communication to optimize context window usage.
Represents both Players and Enemies.
| Field | Type | Description |
|---|---|---|
id |
str | Unique identifier (e.g., p1, e1). |
name |
str | Display name. |
role |
str | Class archetype (Fighter, Mage, Rogue). |
stats |
Dict | PHYS (Str/Dex), MENT (Int/Wis), SOC (Cha). |
hp |
int | Current Health Points. |
max_hp |
int | Maximum Health Points. |
ac |
int | Armor Class (Target number to hit). |
zone |
Enum | Relative positioning: NEAR, MID, FAR. |
inventory |
List[str] | Items held (e.g., ['Potion', 'Rope']). |
The container for the entire simulation snapshot.
- players: List of Character objects.
- enemies: List of Character objects.
- turn_count: Integer tracking the number of rounds.
- logs: List of strings (Last actions). Serves as short-term memory for the LLM.
Why TOON? Reduces token usage by ~40% compared to JSON and improves LLM readability for numerical data.
# Example TOON Payload sent to Gemini
players[2]{id,name,role,hp,zone,phys,ment,soc,items}:
p1,Valen,Fighter,18/20,NEAR,+3,0,+1,[Potion]
p2,Elara,Mage,12/15,MID,-1,+3,+1,[Scroll]
enemies[2]{id,name,hp,zone,condition}:
e1,Spider A,0/15,NEAR,Dead
e2,Spider B,7/15,NEAR,InjuredThe Router is the "Frontal Lobe" of the system. It uses a hybrid routing approach:
- Pass 1 (Regex-First - Zero Tokens): Simple commands like
LOOK,REST,QUIT, andINVENTORYare resolved instantly in pure Python with zero cost. - Pass 2 (LLM-Smart - Gemini 2.5 Flash Lite): Fires only when Pass 1 regex returns
UNKNOWN. It classifies natural language sentences to direct player intent to either Path A or Path B.
-
Input: "I cast Firebolt at the spider!"
- Check: Matches standard Lite 5e keywords (Attack, Cast, Move, Use Item).
- Result:
FIXED-> Path A (Rules Engine).
-
Input: "I hit the goblin and then run away to the FAR zone."
- Check: Matches a compound action combining multiple fixed mechanics.
- Result:
FIXED_COMBO-> Path A (Rules Engine). Extracts anaction_orderarray (e.g.["ATTACK", "MOVE"]) to dynamically execute the mechanics in the user's requested order.
-
Input: "I want to tie the rope to trip the goblin."
- Check: No standard rule matches. Complex intent.
- Result:
CREATIVE-> Path B (LLM Arbitrator).
A collection of Pure Python Functions. This layer is the "Calculator."
roll(expression: str) -> dict: Parses "1d20", "2d6+3". Returns stateless dict.resolve_attack(attacker, target) -> dict:- Calculation: d20 + PHYS vs Target AC (or MENT for spells). Includes Advantage/Disadvantage based on Zone distance and Conditions.
- Crit Logic: If natural 20, double damage dice count.
- Output: Dictionary containing
{is_hit, roll, total, damage, is_crit, disadvantage, message}.
resolve_check(actor, stat, dc) -> dict:- Calculation: d20 + Stat >= DC.
- Output: Dictionary containing
{success, total, raw_rolls}.
Hardcoded logic for specific class feats to ensure balance.
Second Wind(Fighter): Heals HP1d10 + 1. [recharges on Short Rest]Smite(Paladin): Adds2d8radiant damage on hit (charge is only consumed on a successful hit). [recharges on Long Rest]Lay on Hands(Paladin): Heals1d8 + MENT. [recharges on Long Rest]Pray(Cleric): RollsMENTvs DC 13 to heal an ally (1d8 + MENT) or deal radiant damage (1d8 + MENT) to an enemy. [recharges on Short Rest]Rest/Sleep(All): Automatically and fully restores all HP, and recharges short-rest abilities.
Optimized for speed and precision.
- Player: "I swing my longsword at the Wolf Spider!"
- Router: Classifies as
FIXED(Attack). CallsRulesEngine.resolve_attack(p1, e1). - Engine (Python):
- Rolls d20 (result 15) + PHYS (+3) = 18.
- Checks vs AC (12) -> HIT.
- Rolls Damage -> 6.
- Updates State: e1.hp decreases to 9.
- System Log: Generates raw string:
[System]: Hit! Dealt 6 Damage. - LLM Narrator: Receives Log + State.
- Output: "Your blade strikes true, cutting deep into the spider's leg! It screeches in pain."
Optimized for flexibility.
- Player: "I want to decipher the ancient runes on the wall."
- Router: Classifies as
CREATIVE. Forwards to LLM Arbitrator. - LLM Arbitrator:
- Analysis: Task requires intellect. Difficulty is Moderate.
- Output (Function Call):
resolve_check(p1, MENT, 12)
- Engine (Python):
- Rolls d20 (10) + MENT (+2) = 12.
- Checks 12 >= 12 -> SUCCESS.
- LLM Narrator: Receives "Success" signal.
- Output: "You successfully translate the runes. They read: 'Beware of the shadows ahead...'"
To provide a more dynamic and fair combat experience, the system uses an Initiative Queue orchestrated by the Main Game Loop (src/engine/game_loop.py).
-
Initialization:
- At the start of combat, every active Character (Player and Enemy) rolls Initiative:
1d20 + PHYS modifier. - The
InitiativeQueuesorts all combatants from highest to lowest roll.
- At the start of combat, every active Character (Player and Enemy) rolls Initiative:
-
Turn Execution (Single Layer):
- The queue advances one character at a time in
game_loop.py. - If it is a Player's Turn, the Engine waits for user input.
- If it is an Enemy's Turn, the AI logic evaluates targets (preferring NEAR zone), executes a single attack/move, and automatically narrates the outcome.
- Dead or Unconscious characters are dynamically skipped in the queue.
- Once the queue finishes,
Round Countincrements, and it loops back to the top.
- The queue advances one character at a time in
Every prompt sent to the LLM (Narrator or Arbitrator) includes a strictly formatted Context Block to minimize hallucinations and save tokens.
- System Constraints: Explicit instructions restricting the LLM to output strictly formatted JSON for the Arbitrator, or brief 1-2 sentence descriptions for the Narrator.
- Current State (TOON): Real-time HP, Zone Position, and Condition logic dynamically dumped into a highly compressed TOON string.
- Active Entity Context: Defines exactly which Player or Enemy is currently acting.
- Raw Action Iteration: The verbatim parsed action or attempt proposed by the game loop.