2179 lines
61 KiB
Markdown
2179 lines
61 KiB
Markdown
# VibeEngine - Core Design Document
|
|
|
|
## Table of Contents
|
|
|
|
- [1. Core Components](#1-core-components)
|
|
- [2. Character AI System](#2-character-ai-system)
|
|
- [3. Game Definition Format](#3-game-definition-format)
|
|
- [4. AI Model Deployment](#4-ai-model-deployment)
|
|
- [5. Preventing LLM Hallucination](#5-preventing-llm-hallucination)
|
|
- [6. Managing Context Windows](#6-managing-context-windows)
|
|
- [7. Map System](#7-map-system)
|
|
- [8. Rust Implementation](#8-rust-implementation)
|
|
- [9. Advanced Systems](#9-advanced-systems)
|
|
- [10. Example Implementations](#10-example-implementations)
|
|
|
|
## 1. Core Components
|
|
|
|
VibeEngine is built on several key components that work together to create dynamic, AI-driven gameplay experiences.
|
|
|
|
- **World State Manager**: Tracks game world, objects, characters, and their properties
|
|
- **Event System**: Handles player actions, AI decisions, and world changes
|
|
- **AI Integration Layer**: Manages communication with your AI model(s)
|
|
- **Game Logic Engine**: Processes rules, validates actions, manages turn flow
|
|
- **Narrative System**: Handles text generation, story progression, and dialogue
|
|
|
|
## 2. Character AI System
|
|
|
|
The Character AI System powers dynamic NPC interactions with:
|
|
|
|
- Personality profiles and goals for each NPC
|
|
- Memory of past interactions and relationships
|
|
- Context-aware AI queries including:
|
|
- Character context
|
|
- Current situation
|
|
- Available actions
|
|
- Advanced response parsing for natural dialogue and decisions
|
|
|
|
## 3. Game Definition Format
|
|
|
|
A flexible JSON or YAML-based format is used for game definitions.
|
|
|
|
### 3.1 World Definition
|
|
|
|
- Locations with descriptions, connections, and properties
|
|
- Objects with interactive capabilities
|
|
- Character templates with personality traits, goals, and behavioral parameters
|
|
|
|
### 3.2 Rule Systems
|
|
|
|
- Action definitions (what players/NPCs can do)
|
|
- Condition checks (requirements for actions)
|
|
- Consequence chains (what happens after actions)
|
|
|
|
### 3.3 Dynamic Elements
|
|
|
|
- Template-based text generation
|
|
- Conditional story branches
|
|
- Procedural event triggers
|
|
|
|
This allows game creators to define the framework while letting AI fill in character behaviors and dynamic responses.
|
|
|
|
## 4. AI Model Deployment
|
|
|
|
### 4.1 Self-Hosted Options
|
|
|
|
- **Ollama**: Easiest setup, runs models like Llama locally with simple API
|
|
- **LM Studio**: User-friendly GUI for running various open-source models
|
|
- **Text Generation WebUI**: More advanced interface with extensive model support
|
|
- **vLLM**: Higher performance option for production use
|
|
|
|
### 4.2 Model Recommendations
|
|
|
|
- **For character dialogue**: Llama 3.1 8B or Mistral 7B (good balance of capability and resource usage)
|
|
- **For game logic**: Smaller, faster models might suffice
|
|
- **Fine-tuning**: Consider fine-tuning on game-specific data for better character consistency
|
|
|
|
### 4.3 Integration Approach
|
|
|
|
- REST API calls to your local model server
|
|
- Structured prompts that include character context and game state
|
|
- Response parsing to extract actionable decisions from AI output
|
|
|
|
The key is designing prompts that give the AI enough context about the character's personality, current situation, and available actions to make believable decisions while maintaining game flow and narrative coherence.
|
|
|
|
## 5. Preventing LLM Hallucination
|
|
|
|
### 5.1 World Knowledge Grounding
|
|
|
|
#### 5.1.1 Provide Explicit Context
|
|
Instead of letting the AI invent details, feed it comprehensive world information with each request.
|
|
|
|
**Example Context for an RPG town:**
|
|
|
|
```yaml
|
|
# world/millbrook.yaml
|
|
name: "Millbrook"
|
|
description: "A small farming community known for its grain production"
|
|
population: 500
|
|
|
|
locations:
|
|
mill:
|
|
name: "The Old Mill"
|
|
owner: "Elder Thorne"
|
|
description: "A large water mill by the river, currently having wheel issues"
|
|
|
|
tavern:
|
|
name: "The Grain & Grapes"
|
|
owner: "Rosa"
|
|
description: "A cozy tavern serving local ale and simple meals"
|
|
|
|
market_square:
|
|
name: "Market Square"
|
|
description: "The bustling center of town where merchants gather"
|
|
|
|
events:
|
|
upcoming:
|
|
- name: "Harvest Festival"
|
|
date: "next week"
|
|
description: "Annual celebration of the harvest"
|
|
|
|
recent:
|
|
- description: "Bandits spotted near north road"
|
|
impact: 3 # Scale of 1-5
|
|
affects: ["trade", "safety"]
|
|
|
|
current_issues:
|
|
- issue: "Mill wheel broken"
|
|
severity: "high"
|
|
affects: ["economy", "food supply"]
|
|
|
|
- issue: "Merchant caravan late"
|
|
severity: "medium"
|
|
affects: ["supplies", "news from other towns"]
|
|
|
|
npcs:
|
|
- id: "miller_gareth"
|
|
name: "Gareth"
|
|
role: "Miller"
|
|
location: "mill"
|
|
|
|
- id: "tavern_rosa"
|
|
name: "Rosa"
|
|
role: "Tavern Owner"
|
|
location: "tavern"
|
|
|
|
- id: "guard_marcus"
|
|
name: "Marcus"
|
|
role: "Guard Captain"
|
|
location: "market_square"
|
|
```
|
|
|
|
#### 5.1.2 Character-Specific Information
|
|
|
|
```yaml
|
|
# characters/miller_gareth.yaml
|
|
id: "miller_gareth"
|
|
name: "Gareth"
|
|
role: "Miller"
|
|
location: "mill"
|
|
|
|
descriptors:
|
|
- "middle-aged"
|
|
- "flour-dusted clothes"
|
|
- "calloused hands"
|
|
- "perpetually worried expression"
|
|
|
|
personality:
|
|
traits:
|
|
- "hardworking"
|
|
- "worrier"
|
|
- "talkative"
|
|
- "knowledgeable about grain"
|
|
- "community-minded"
|
|
|
|
speech_patterns:
|
|
- "tends to ramble about grain quality"
|
|
- "frequently mentions the weather's impact on crops"
|
|
- "uses farming metaphors"
|
|
|
|
current_concerns:
|
|
- "mill_wheel_broken":
|
|
description: "The mill wheel is broken, affecting production"
|
|
priority: "high"
|
|
related_to: ["economy", "livelihood"]
|
|
|
|
- "upcoming_harvest":
|
|
description: "The annual harvest is approaching"
|
|
priority: "medium"
|
|
related_to: ["workload", "income"]
|
|
|
|
knowledge_base:
|
|
known_topics:
|
|
- "grain_types"
|
|
- "mill_operations"
|
|
- "local_farming_techniques"
|
|
- "town_gossip"
|
|
- "weather_patterns"
|
|
|
|
unknown_topics:
|
|
- "military_strategy"
|
|
- "arcane_magic"
|
|
- "distant_kingdoms"
|
|
- "advanced_mathematics"
|
|
|
|
relationships:
|
|
player:
|
|
current: 50 # 0-100 scale
|
|
history: []
|
|
|
|
elder_thorne:
|
|
type: "employer"
|
|
current: 80
|
|
|
|
tavern_rosa:
|
|
type: "friend"
|
|
current: 70
|
|
|
|
guard_marcus:
|
|
type: "acquaintance"
|
|
current: 40
|
|
|
|
inventory:
|
|
- "flour_sack"
|
|
- "mill_key"
|
|
- "wheat_samples"
|
|
|
|
schedule:
|
|
"06:00-18:00": "working at the mill"
|
|
"18:00-20:00": "at the tavern"
|
|
"20:00-06:00": "sleeping at home"
|
|
|
|
dialogue_hooks:
|
|
- "mill_repair": "The mill wheel won't last much longer..."
|
|
- "harvest_time": "The harvest is coming early this year."
|
|
- "bandit_trouble": "Heard about them bandits up north?"
|
|
```
|
|
|
|
### 5.2 Structured Prompting
|
|
|
|
#### 5.2.1 Use Explicit Constraints
|
|
|
|
```text
|
|
- "You can only reference information provided in the world context"
|
|
- "If asked about something not in your knowledge, say the character doesn't know"
|
|
- "Stick to the character's personality and knowledge limitations"
|
|
```
|
|
|
|
#### 5.2.2 Knowledge Boundaries
|
|
|
|
- Define what each character type would realistically know
|
|
- Merchants know trade routes and prices, not military secrets
|
|
- Guards know local security, not magical theory
|
|
- Farmers know crops and weather, not court politics
|
|
|
|
### 5.3 Response Format Control
|
|
|
|
#### 5.3.1 Structured Output Requirements
|
|
|
|
```json
|
|
{
|
|
"dialogue": "Oh, well the harvest festival is coming up next week! Though I am a bit worried about these bandits they spotted up north...",
|
|
"action": "scratches head and looks concerned",
|
|
"internal_state": "concerned about bandits, excited for festival",
|
|
"world_query": ""
|
|
}
|
|
```
|
|
|
|
#### 5.3.2 Response Validation
|
|
|
|
- Parse the structured output before displaying
|
|
- Check if dialogue references world elements not in the provided context
|
|
- Reject responses that invent new locations, characters, or events
|
|
|
|
### 5.4 Example Implementation Strategy
|
|
|
|
**Bad Approach (Prone to Hallucination):**
|
|
"Play as a miller in a fantasy town. The player asks about recent news."
|
|
|
|
**Good Approach:**
|
|
|
|
```yaml
|
|
WORLD_CONTEXT:
|
|
- Detailed town information
|
|
- Current events
|
|
- Character knowledge base
|
|
|
|
CHARACTER:
|
|
name: "Miller Gareth"
|
|
personality: "Worried, hardworking, talkative about grain"
|
|
knowledge: ["local farming", "mill operations", "town gossip"]
|
|
relationships:
|
|
player: "neutral"
|
|
|
|
CONVERSATION_HISTORY: []
|
|
CURRENT_SITUATION: "Player just entered the mill"
|
|
|
|
INSTRUCTIONS: |
|
|
- Respond naturally as Miller Gareth
|
|
- Only reference known information
|
|
- If unsure, say you don't know
|
|
- Format response as JSON with:
|
|
- dialogue: string
|
|
- action: string (optional)
|
|
- internal_state: string (optional)
|
|
|
|
Player: "What's the news around town?"
|
|
```
|
|
|
|
# Additional Techniques
|
|
|
|
## Fact Checking Layer
|
|
|
|
- Cross-reference AI responses against your world database
|
|
- Flag or reject responses that introduce new "facts"
|
|
- Maintain a consistency log of what's been established
|
|
|
|
## Layered AI Approach
|
|
|
|
- One AI for dialogue generation (with strict constraints)
|
|
- Another AI for world consistency checking
|
|
- Game engine validates both outputs against world state
|
|
|
|
## Dynamic Context Updates
|
|
|
|
- Update world context as events occur
|
|
- Remove outdated information to prevent confusion
|
|
- Add new confirmed facts from validated interactions
|
|
|
|
## Fallback Responses
|
|
|
|
- Pre-written responses for when AI output fails validation
|
|
- Character-appropriate ways to deflect unknown topics
|
|
- "I'm not sure about that" responses that fit each character's personality
|
|
|
|
This approach treats the AI as a dialogue generator within strict boundaries rather than a world creator, significantly reducing hallucination while maintaining engaging character interactions.
|
|
|
|
## 6. Managing Context Windows
|
|
|
|
Smaller models (7B-13B parameters) struggle significantly with large context windows, leading to:
|
|
|
|
- Information bleeding between different parts of the context
|
|
- Attention degradation where the model loses focus on relevant details
|
|
- Increased hallucination as the model gets overwhelmed
|
|
- Inconsistent responses based on context position effects
|
|
|
|
# Common Approaches for Managing Large Context
|
|
|
|
### 6.1 Hierarchical Context Management
|
|
|
|
#### 6.1.1 Recent + Summary Approach
|
|
|
|
- Keep last 10-20 messages in full detail
|
|
- Summarize older interactions into key facts
|
|
- Maintain separate "character memory" summaries
|
|
|
|
**Example:**
|
|
|
|
```text
|
|
RECENT CONVERSATION: [last 5 exchanges]
|
|
|
|
CHARACTER MEMORY:
|
|
- Player helped with mill repair
|
|
- Friendly relationship established
|
|
- Player mentioned being from the capital
|
|
|
|
WORLD EVENTS SUMMARY:
|
|
- Harvest festival completed
|
|
- Bandits captured
|
|
- New merchant arrived
|
|
```
|
|
|
|
### 6.2 Retrieval-Augmented Generation (RAG)
|
|
|
|
### Context Retrieval
|
|
|
|
- Store all game events/dialogue in a searchable database
|
|
- Use semantic search to find relevant context for current situation
|
|
- Only inject the most relevant 3-5 pieces of information
|
|
|
|
**Example Query:**
|
|
- Player asks about the festival
|
|
- Retrieved Context: Only festival-related events, not unrelated conversations
|
|
|
|
### 6.3 State-Based Context Reduction
|
|
|
|
### Categorical Organization
|
|
|
|
- Separate character relationships, world events, inventory, locations
|
|
- Only include relevant categories for current interaction
|
|
- Use different context "views" for different interaction types
|
|
|
|
### 6.4 Rolling Context Windows
|
|
|
|
Sliding Window Approach:
|
|
|
|
Maintain fixed-size context (e.g., 2000 tokens)
|
|
Compress older information into structured summaries
|
|
Keep full detail only for recent interactions
|
|
|
|
# 5. Multi-Pass Processing
|
|
|
|
Staged Approach:
|
|
|
|
Context Selection Pass: Determine what information is relevant
|
|
Response Generation Pass: Generate dialogue with selected context
|
|
Consistency Check Pass: Verify response against world state
|
|
|
|
# 6. Structured Memory Systems
|
|
|
|
Explicit Memory Management:
|
|
|
|
CHARACTER_FACTS: {
|
|
"player_relationship": "friendly",
|
|
"last_interaction": "helped with mill",
|
|
"known_about_player": ["from capital", "skilled fighter"]
|
|
}
|
|
WORLD_STATE: {
|
|
"mill_status": "repaired",
|
|
"current_events": ["festival over", "new merchant"]
|
|
}
|
|
|
|
# 7. Context Compression Techniques
|
|
|
|
Information Density:
|
|
|
|
Use abbreviated formats for historical events
|
|
Compress repeated information into single statements
|
|
Focus on actionable/relevant details only
|
|
|
|
Example Compression: Instead of: "The player said hello. The miller responded with a greeting. They discussed the weather..." Use: "Previous interaction: Friendly greeting, discussed weather concerns affecting crops"
|
|
Practical Implementation Strategy
|
|
|
|
For Your Game Engine:
|
|
|
|
Dynamic Context Building:
|
|
Analyze current player action/location
|
|
Retrieve only relevant world state and character memories
|
|
Include recent conversation history (last 3-5 exchanges)
|
|
Memory Persistence:
|
|
Store character impressions and key facts separately
|
|
Update character memory after each significant interaction
|
|
Maintain world event timeline with importance ratings
|
|
Context Prioritization:
|
|
Recent events > Historical events
|
|
Character-specific knowledge > General world knowledge
|
|
Player-relevant information > Background details
|
|
Fallback Mechanisms:
|
|
If context becomes too large, prioritize recent + character-specific information
|
|
Use "the character doesn't recall" for information that had to be excluded
|
|
Implement context overflow handling gracefully
|
|
|
|
This approach lets you maintain rich, consistent game worlds while keeping the AI model's context manageable and focused, significantly reducing hallucination risks even with smaller models.
|
|
|
|
## 7. Map System
|
|
|
|
### 7.1 Core Map Structure
|
|
|
|
Areas with Connections: Each area defines its connections to other areas, along with conditions that must be met to traverse them.
|
|
|
|
Condition Types:
|
|
|
|
Item requirements (need torch)
|
|
Character stats (strength, magic level, health)
|
|
World state (events completed, time of day, weather)
|
|
Character knowledge (learned location, met someone)
|
|
Story progression (quest completed, dialogue triggered)
|
|
|
|
Example Game File
|
|
|
|
json
|
|
|
|
```json
|
|
{
|
|
"game_info": {
|
|
"name": "Village of Shadows",
|
|
"version": "1.0",
|
|
"description": "A mysterious village with dark secrets"
|
|
},
|
|
|
|
"world_state": {
|
|
"time_of_day": "afternoon",
|
|
"weather": "cloudy",
|
|
"events": {
|
|
"mill_repaired": false,
|
|
"bandits_defeated": false,
|
|
"mayor_met": false,
|
|
"secret_passage_discovered": false
|
|
}
|
|
},
|
|
|
|
"areas": {
|
|
"village_center": {
|
|
"name": "Village Center",
|
|
"description": "A bustling town square with a stone fountain. Villagers go about their daily business.",
|
|
"connections": {
|
|
"millers_house": {
|
|
"direction": "north",
|
|
"description": "A path leads north to the miller's house",
|
|
"conditions": []
|
|
},
|
|
"tavern": {
|
|
"direction": "east",
|
|
"description": "The warm glow of the tavern beckons from the east",
|
|
"conditions": []
|
|
},
|
|
"mayors_hall": {
|
|
"direction": "west",
|
|
"description": "The imposing mayor's hall stands to the west",
|
|
"conditions": [
|
|
{
|
|
"type": "world_event",
|
|
"event": "mayor_met",
|
|
"value": true,
|
|
"failure_message": "The guards won't let you pass without an appointment"
|
|
}
|
|
]
|
|
},
|
|
"forest_path": {
|
|
"direction": "south",
|
|
"description": "A dark forest path leads south",
|
|
"conditions": [
|
|
{
|
|
"type": "item",
|
|
"item": "torch",
|
|
"failure_message": "The forest is too dark to navigate without light"
|
|
},
|
|
{
|
|
"type": "time",
|
|
"allowed_times": ["morning", "afternoon"],
|
|
"failure_message": "The forest is too dangerous at night"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"items": ["rusty_coin", "old_newspaper"],
|
|
"npcs": ["town_crier", "fruit_vendor"]
|
|
},
|
|
|
|
"millers_house": {
|
|
"name": "Miller's House",
|
|
"description": "A modest wooden house with a large water wheel beside it.",
|
|
"connections": {
|
|
"village_center": {
|
|
"direction": "south",
|
|
"description": "Return to the village center",
|
|
"conditions": []
|
|
},
|
|
"mill_basement": {
|
|
"direction": "down",
|
|
"description": "Wooden stairs lead down to the mill's basement",
|
|
"conditions": [
|
|
{
|
|
"type": "character_relationship",
|
|
"character": "miller_gareth",
|
|
"relationship_level": "trusted",
|
|
"failure_message": "The miller doesn't trust you enough to let you into his basement"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"items": ["flour_sack"],
|
|
"npcs": ["miller_gareth"]
|
|
},
|
|
|
|
"tavern": {
|
|
"name": "The Grain & Grapes Tavern",
|
|
"description": "A cozy tavern filled with the aroma of ale and roasted meat.",
|
|
"connections": {
|
|
"village_center": {
|
|
"direction": "west",
|
|
"description": "Exit to the village center",
|
|
"conditions": []
|
|
},
|
|
"tavern_upstairs": {
|
|
"direction": "up",
|
|
"description": "Stairs lead to the guest rooms above",
|
|
"conditions": [
|
|
{
|
|
"type": "item",
|
|
"item": "room_key",
|
|
"failure_message": "You need a room key to go upstairs"
|
|
}
|
|
]
|
|
},
|
|
"secret_tunnel": {
|
|
"direction": "hidden",
|
|
"description": "A hidden passage behind the wine barrels",
|
|
"conditions": [
|
|
{
|
|
"type": "world_event",
|
|
"event": "secret_passage_discovered",
|
|
"value": true,
|
|
"failure_message": "You don't know about any secret passages"
|
|
},
|
|
{
|
|
"type": "item",
|
|
"item": "crowbar",
|
|
"failure_message": "You need something to pry open the passage"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"items": ["ale_mug", "mysterious_note"],
|
|
"npcs": ["tavern_owner_rosa", "drunk_patron"]
|
|
},
|
|
|
|
"forest_path": {
|
|
"name": "Dark Forest Path",
|
|
"description": "Ancient trees tower overhead, their branches blocking most sunlight.",
|
|
"connections": {
|
|
"village_center": {
|
|
"direction": "north",
|
|
"description": "Return to the village",
|
|
"conditions": []
|
|
},
|
|
"bandit_camp": {
|
|
"direction": "east",
|
|
"description": "Trampled underbrush leads deeper into the forest",
|
|
"conditions": [
|
|
{
|
|
"type": "character_stat",
|
|
"stat": "stealth",
|
|
"minimum_value": 3,
|
|
"failure_message": "You're making too much noise - the bandits will hear you"
|
|
}
|
|
]
|
|
},
|
|
"ancient_ruins": {
|
|
"direction": "west",
|
|
"description": "Stone ruins are barely visible through the trees",
|
|
"conditions": [
|
|
{
|
|
"type": "multiple",
|
|
"operator": "AND",
|
|
"conditions": [
|
|
{
|
|
"type": "character_knowledge",
|
|
"knowledge": "ruins_location",
|
|
"failure_message": "You don't know about any ruins in this area"
|
|
},
|
|
{
|
|
"type": "character_stat",
|
|
"stat": "magic_level",
|
|
"minimum_value": 2,
|
|
"failure_message": "The magical barrier prevents you from approaching"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"items": ["healing_herb", "broken_sword"],
|
|
"npcs": ["forest_spirit"]
|
|
}
|
|
},
|
|
|
|
"condition_types": {
|
|
"item": "Player must possess specific item",
|
|
"character_stat": "Player stat must meet minimum requirement",
|
|
"world_event": "Specific world event must be true/false",
|
|
"time": "Must be specific time of day",
|
|
"character_relationship": "Must have specific relationship level with NPC",
|
|
"character_knowledge": "Player must have learned specific information",
|
|
"multiple": "Combine multiple conditions with AND/OR logic"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7.2 Key Design Features
|
|
|
|
#### 7.2.1 Flexible Conditions
|
|
|
|
Simple single requirements (need torch)
|
|
Complex multi-part conditions (need item AND high stat)
|
|
Dynamic conditions based on world state
|
|
|
|
Directional Movement:
|
|
|
|
Traditional compass directions
|
|
Special directions (up, down, hidden)
|
|
Descriptive text for each connection
|
|
|
|
Failure Handling:
|
|
|
|
Custom messages for each blocked path
|
|
Clear feedback about what's needed
|
|
|
|
Progressive Unlocking:
|
|
|
|
Areas become accessible as story progresses
|
|
Relationships with NPCs open new areas
|
|
Player character growth enables new paths
|
|
|
|
This structure allows for rich, interconnected worlds where exploration feels natural and progression is gated by meaningful story and character development rather than arbitrary barriers.
|
|
|
|
JSON vs YAML for Game Files
|
|
|
|
YAML is better for your use case:
|
|
|
|
YAML Advantages:
|
|
|
|
Human readable/writable - Game designers can easily author content
|
|
Comments support - Essential for documenting complex game logic
|
|
Multi-line strings - Perfect for character dialogue and descriptions
|
|
Less verbose - Cleaner structure for nested data
|
|
Better for version control - Easier to see diffs and merge changes
|
|
|
|
JSON Advantages:
|
|
|
|
Parsing performance - Slightly faster, but negligible for game files
|
|
Ubiquitous support - But YAML parsers are common in Rust
|
|
|
|
Example comparison:
|
|
|
|
yaml
|
|
|
|
# YAML - much cleaner for game content
|
|
areas:
|
|
village_center:
|
|
name: Village Center
|
|
description: |
|
|
A bustling town square with a stone fountain.
|
|
Villagers go about their daily business,
|
|
preparing for the upcoming harvest festival.
|
|
connections:
|
|
tavern:
|
|
direction: east
|
|
conditions:
|
|
- type: time
|
|
allowed_times: [morning, afternoon]
|
|
failure_message: "The tavern is closed at night"
|
|
|
|
vs the equivalent JSON with escaped strings and no comments.
|
|
|
|
Dependency System
|
|
|
|
Modular Structure:
|
|
|
|
yaml
|
|
|
|
# main_game.yaml
|
|
game_info:
|
|
name: "Village of Shadows"
|
|
dependencies:
|
|
- "maps/village_map.yaml"
|
|
- "characters/villagers.yaml"
|
|
- "items/village_items.yaml"
|
|
- "events/harvest_festival.yaml"
|
|
|
|
# characters/villagers.yaml
|
|
characters:
|
|
miller_gareth:
|
|
import_from: "archetypes/worried_craftsman.yaml"
|
|
overrides:
|
|
profession: "miller"
|
|
specific_concerns: ["mill_wheel", "grain_quality"]
|
|
|
|
# archetypes/worried_craftsman.yaml
|
|
personality_template:
|
|
traits: ["anxious", "hardworking", "detail_oriented"]
|
|
speech_patterns: ["tends to ramble", "mentions work frequently"]
|
|
|
|
Dependency Types:
|
|
|
|
Maps - Area definitions and connections
|
|
Characters - NPC definitions and dialogue trees
|
|
Items - Object definitions and interactions
|
|
Events - Story progression and world state changes
|
|
Archetypes - Reusable character/item templates
|
|
|
|
Character System Structure
|
|
|
|
yaml
|
|
|
|
```yaml
|
|
characters:
|
|
miller_gareth:
|
|
# Basic Info
|
|
name: "Gareth the Miller"
|
|
profession: "Miller"
|
|
location: "millers_house"
|
|
|
|
# Personality Core
|
|
personality:
|
|
traits: ["anxious", "hardworking", "talkative", "kind"]
|
|
values: ["honest_work", "community", "tradition"]
|
|
fears: ["mill_breaking", "poor_harvest", "letting_people_down"]
|
|
goals: ["keep_mill_running", "help_village", "save_money"]
|
|
|
|
# Speech Patterns
|
|
speech:
|
|
style: "rambling"
|
|
common_phrases:
|
|
- "Well, you see..."
|
|
- "Back in my day..."
|
|
- "The grain tells me..."
|
|
topics_of_interest: ["milling", "grain_quality", "weather", "village_gossip"]
|
|
vocabulary_level: "simple"
|
|
accent: "rural"
|
|
|
|
# Knowledge & Relationships
|
|
knowledge:
|
|
areas: ["village_center", "millers_house", "grain_fields"]
|
|
secrets: ["mill_basement_contents", "old_mayors_corruption"]
|
|
skills: ["milling", "grain_assessment", "basic_repairs"]
|
|
|
|
relationships:
|
|
player:
|
|
initial_disposition: "cautious"
|
|
relationship_stages:
|
|
stranger: "polite but distant"
|
|
acquaintance: "friendly but reserved"
|
|
trusted: "opens up about problems"
|
|
friend: "shares secrets and asks for help"
|
|
|
|
tavern_owner_rosa:
|
|
status: "friend"
|
|
history: "childhood_friends"
|
|
current_dynamic: "trade_grain_for_ale"
|
|
|
|
# Memory System
|
|
memory:
|
|
short_term: [] # Recent conversations
|
|
long_term: [] # Important events with player
|
|
character_impression: "unknown" # Updated based on interactions
|
|
|
|
# Dialogue System
|
|
dialogue:
|
|
greeting:
|
|
stranger: |
|
|
*wipes flour-covered hands on apron*
|
|
Oh! A visitor! Don't get many of those these days.
|
|
You wouldn't be interested in some fine flour, would you?
|
|
|
|
acquaintance: |
|
|
Ah, good to see you again! How've you been?
|
|
The mill's been keeping me busy as always.
|
|
|
|
trusted: |
|
|
*sighs heavily*
|
|
I'm glad you're here. Been having some troubles
|
|
with the mill wheel lately, and I could use someone
|
|
I can trust to talk to.
|
|
|
|
topics:
|
|
mill_problems:
|
|
conditions:
|
|
- relationship: "trusted"
|
|
responses:
|
|
- trigger: "first_time"
|
|
text: |
|
|
*looks around nervously*
|
|
Well, you see, the wheel's been making strange noises.
|
|
Not the usual creaking - something... different.
|
|
And there's been odd scratching sounds from the basement at night.
|
|
|
|
village_gossip:
|
|
conditions: []
|
|
responses:
|
|
- trigger: "default"
|
|
text: |
|
|
*leans in conspiratorially*
|
|
Rosa at the tavern's been asking about strangers lately.
|
|
Says some folk have been asking odd questions about the old days.
|
|
|
|
reactions:
|
|
player_helps_with_mill:
|
|
immediate: "You're a lifesaver! I don't know what I'd have done without you."
|
|
relationship_change: "+2"
|
|
memory_addition: "Player helped repair the mill wheel"
|
|
|
|
player_asks_about_basement:
|
|
conditions:
|
|
- relationship: "below_trusted"
|
|
response: "*shifts uncomfortably* Oh, just old milling equipment down there."
|
|
|
|
# Dynamic Behavior
|
|
behaviors:
|
|
default_actions: ["mill_maintenance", "check_grain_quality", "worry_about_weather"]
|
|
|
|
conditional_behaviors:
|
|
- condition: "mill_wheel_broken"
|
|
actions: ["pace_nervously", "mutter_about_repairs"]
|
|
|
|
- condition: "player_relationship_trusted"
|
|
actions: ["confide_problems", "ask_for_help"]
|
|
|
|
# Context for AI
|
|
ai_context:
|
|
personality_prompt: |
|
|
You are Gareth, a worried but kind miller. You tend to ramble about
|
|
grain and milling processes. You're anxious about your mill breaking
|
|
down and letting the village down. You speak in a rural dialect and
|
|
often mention your work.
|
|
|
|
knowledge_limits: |
|
|
You know about milling, local farming, village gossip, and basic
|
|
repairs. You don't know about magic, distant lands, or court politics.
|
|
|
|
conversation_style: |
|
|
You tend to go off on tangents about grain quality or mill maintenance.
|
|
You're friendly but worry about your problems. You often wipe your
|
|
hands on your apron when nervous.
|
|
```
|
|
|
|
This structure provides:
|
|
|
|
- Rich personality definition for consistent AI behavior
|
|
- Relationship progression that affects dialogue options
|
|
- Memory system for persistent character development
|
|
- Conditional content based on world state and relationships
|
|
- Clear AI context to prevent hallucination while maintaining personality
|
|
|
|
Yes, YAML is very easy to parse in Rust with excellent serde support!
|
|
|
|
## Primary YAML Crate
|
|
|
|
serde_yaml is the standard choice:
|
|
|
|
toml
|
|
|
|
[dependencies]
|
|
serde = { version = "1.0", features = ["derive"] }
|
|
serde_yaml = "0.9"
|
|
|
|
## Basic Usage
|
|
|
|
### Deserializing YAML
|
|
|
|
```rust
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
struct GameArea {
|
|
name: String,
|
|
description: String,
|
|
connections: HashMap<String, Connection>,
|
|
items: Vec<String>,
|
|
npcs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
struct Connection {
|
|
direction: String,
|
|
description: String,
|
|
conditions: Vec<Condition>,
|
|
}
|
|
|
|
// Parse YAML file
|
|
let yaml_content = std::fs::read_to_string("game_world.yaml")?;
|
|
let areas: HashMap<String, GameArea> = serde_yaml::from_str(&yaml_content)?;
|
|
```
|
|
|
|
### Advantages of YAML + Serde
|
|
|
|
Automatic Deserialization:
|
|
- Serde handles all the parsing automatically
|
|
- Type-safe deserialization into Rust structs
|
|
- Excellent error messages for malformed YAML
|
|
|
|
Flexible Data Types:
|
|
- Maps to Rust's HashMap, Vec, Option, String, etc.
|
|
- Supports enums, nested structs, and custom types
|
|
- Handles optional fields gracefully
|
|
|
|
Comments Preserved:
|
|
- YAML comments are ignored during parsing (which is what you want)
|
|
- No need to strip comments manually
|
|
|
|
## Example for Your Game Engine
|
|
|
|
```rust
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
struct GameWorld {
|
|
game_info: GameInfo,
|
|
world_state: WorldState,
|
|
areas: HashMap<String, GameArea>,
|
|
characters: HashMap<String, Character>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
struct Character {
|
|
name: String,
|
|
profession: String,
|
|
location: String,
|
|
personality: Personality,
|
|
speech: SpeechPattern,
|
|
knowledge: Knowledge,
|
|
relationships: HashMap<String, Relationship>,
|
|
dialogue: HashMap<String, DialogueOption>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
struct Condition {
|
|
#[serde(rename = "type")]
|
|
condition_type: String,
|
|
|
|
// Flatten all other fields into this map
|
|
#[serde(flatten)]
|
|
parameters: HashMap<String, serde_yaml::Value>,
|
|
}
|
|
|
|
// Load multiple files
|
|
let world: GameWorld = serde_yaml::from_str(&fs::read_to_string("main_game.yaml")?)?;
|
|
let characters: HashMap<String, Character> = serde_yaml::from_str(&fs::read_to_string("characters.yaml")?)?;
|
|
|
|
```
|
|
|
|
## Performance & Considerations
|
|
|
|
### Performance
|
|
|
|
YAML parsing is slightly slower than JSON, but negligible for game files
|
|
Typically happens only at game startup
|
|
Very fast for the file sizes you'd be working with
|
|
|
|
### Error Handling
|
|
|
|
- Serde provides detailed error messages with line numbers
|
|
Easy to catch and display meaningful errors to game designers
|
|
|
|
Alternative: If you need maximum performance and don't mind losing comments, you could use JSON for production and YAML for development, converting between them as needed.
|
|
|
|
Verdict: YAML + serde is an excellent choice for your game engine. The developer experience benefits far outweigh the minor performance trade-off, and serde makes it nearly as easy to work with as JSON while being much more human-friendly for content creators.
|
|
|
|
For save states, I'd recommend JSON over YAML despite using YAML for game content. Here's why:
|
|
Save States: JSON vs YAML
|
|
|
|
Use JSON for saves:
|
|
- Performance - Faster parsing/writing during frequent saves
|
|
- Compact size - Smaller file sizes for save data
|
|
- No comments needed - Save data is machine-generated
|
|
- Standard format - Expected format for save files
|
|
- Atomic writes - Easier to ensure save integrity
|
|
|
|
Keep YAML for:
|
|
- Game content files (characters, maps, dialogue)
|
|
- Configuration files
|
|
- Anything humans need to edit
|
|
|
|
Save State Structure
|
|
|
|
Main save file (JSON):
|
|
|
|
```json
|
|
{
|
|
"save_info": {
|
|
"version": "1.0",
|
|
"created_at": "2025-07-08T14:30:00Z",
|
|
"game_time": 1547,
|
|
"player_location": "village_center",
|
|
"save_name": "Village Investigation",
|
|
"playtime_minutes": 127
|
|
},
|
|
"dependencies": {
|
|
"world_state": "saves/world_state.json",
|
|
"character_states": "saves/character_states.json",
|
|
"player_data": "saves/player_data.json",
|
|
"conversation_history": "saves/conversation_history.json",
|
|
"inventory": "saves/inventory.json"
|
|
}
|
|
}
|
|
```
|
|
|
|
### World State (world_state.json)
|
|
|
|
```json
|
|
{
|
|
"current_state": {
|
|
"time_of_day": "evening",
|
|
"weather": "rainy",
|
|
"season": "autumn"
|
|
},
|
|
"events": {
|
|
"mill_repaired": true,
|
|
"bandits_defeated": false,
|
|
"mayor_met": true,
|
|
"secret_passage_discovered": true,
|
|
"harvest_festival_completed": true,
|
|
"ancient_ruins_explored": false
|
|
},
|
|
"area_states": {
|
|
"village_center": {
|
|
"items_present": ["old_newspaper"],
|
|
"items_taken": ["rusty_coin"],
|
|
"visited_count": 15,
|
|
"last_visited": 1543
|
|
},
|
|
"millers_house": {
|
|
"items_present": [],
|
|
"items_taken": ["flour_sack"],
|
|
"visited_count": 8,
|
|
"last_visited": 1547,
|
|
"custom_state": {
|
|
"mill_wheel_condition": "repaired",
|
|
"basement_door_status": "unlocked"
|
|
}
|
|
},
|
|
"tavern": {
|
|
"items_present": ["mysterious_note"],
|
|
"items_taken": ["ale_mug"],
|
|
"visited_count": 12,
|
|
"last_visited": 1540,
|
|
"custom_state": {
|
|
"secret_passage_opened": true,
|
|
"room_rented": false
|
|
}
|
|
}
|
|
},
|
|
"global_variables": {
|
|
"festival_preparations": 100,
|
|
"village_trust_level": 75,
|
|
"bandit_threat_level": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
### Character States (character_states.json)
|
|
|
|
```json
|
|
{
|
|
"miller_gareth": {
|
|
"current_location": "millers_house",
|
|
"relationship_with_player": "trusted",
|
|
"relationship_value": 85,
|
|
"current_mood": "relieved",
|
|
"health": 100,
|
|
"memory": {
|
|
"short_term": [
|
|
{
|
|
"event": "player_helped_repair_mill",
|
|
"timestamp": 1545,
|
|
"importance": "high"
|
|
},
|
|
{
|
|
"event": "discussed_basement_noises",
|
|
"timestamp": 1547,
|
|
"importance": "medium"
|
|
}
|
|
],
|
|
"long_term": [
|
|
{
|
|
"fact": "Player is trustworthy and helpful",
|
|
"established": 1545,
|
|
"confidence": "high"
|
|
},
|
|
{
|
|
"fact": "Player knows about basement secret",
|
|
"established": 1547,
|
|
"confidence": "high"
|
|
}
|
|
],
|
|
"player_impression": "A capable and trustworthy person who helped me in my time of need"
|
|
},
|
|
"dialogue_flags": {
|
|
"basement_revealed": true,
|
|
"mill_problem_solved": true,
|
|
"personal_story_shared": false
|
|
},
|
|
"custom_state": {
|
|
"stress_level": 20,
|
|
"work_efficiency": 90,
|
|
"last_maintenance_check": 1546
|
|
}
|
|
},
|
|
"tavern_owner_rosa": {
|
|
"current_location": "tavern",
|
|
"relationship_with_player": "acquaintance",
|
|
"relationship_value": 45,
|
|
"current_mood": "curious",
|
|
"health": 100,
|
|
"memory": {
|
|
"short_term": [
|
|
{
|
|
"event": "player_asked_about_strangers",
|
|
"timestamp": 1540,
|
|
"importance": "medium"
|
|
}
|
|
],
|
|
"long_term": [
|
|
{
|
|
"fact": "Player asks good questions",
|
|
"established": 1540,
|
|
"confidence": "medium"
|
|
}
|
|
],
|
|
"player_impression": "Seems like someone who pays attention to details"
|
|
},
|
|
"dialogue_flags": {
|
|
"stranger_topic_introduced": true,
|
|
"secret_passage_mentioned": false
|
|
},
|
|
"custom_state": {
|
|
"suspicion_level": 30,
|
|
"business_mood": "good"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Player Data (player_data.json)
|
|
|
|
```json
|
|
{
|
|
"character": {
|
|
"name": "Traveler",
|
|
"level": 3,
|
|
"experience": 350,
|
|
"health": 85,
|
|
"max_health": 100,
|
|
"stats": {
|
|
"strength": 4,
|
|
"stealth": 3,
|
|
"magic_level": 2,
|
|
"charisma": 5
|
|
}
|
|
},
|
|
"knowledge": {
|
|
"areas_discovered": [
|
|
"village_center",
|
|
"millers_house",
|
|
"tavern",
|
|
"forest_path"
|
|
],
|
|
"secrets_learned": [
|
|
"ruins_location",
|
|
"mill_basement_contents",
|
|
"secret_passage_location"
|
|
],
|
|
"skills_acquired": [
|
|
"basic_repair",
|
|
"local_knowledge"
|
|
]
|
|
},
|
|
"quests": {
|
|
"active": [
|
|
{
|
|
"id": "investigate_bandits",
|
|
"title": "Investigate Bandit Activity",
|
|
"description": "Look into the bandit sightings near the forest",
|
|
"progress": 60,
|
|
"objectives": [
|
|
{
|
|
"description": "Talk to villagers about bandit sightings",
|
|
"completed": true
|
|
},
|
|
{
|
|
"description": "Investigate the forest path",
|
|
"completed": false
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"completed": [
|
|
{
|
|
"id": "help_miller",
|
|
"title": "Help the Miller",
|
|
"completed_at": 1545,
|
|
"reward_given": true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Conversation History (conversation_history.json)
|
|
|
|
```json
|
|
{
|
|
"conversations": [
|
|
{
|
|
"id": "conv_001",
|
|
"character": "miller_gareth",
|
|
"location": "millers_house",
|
|
"timestamp": 1547,
|
|
"messages": [
|
|
{
|
|
"speaker": "player",
|
|
"text": "I heard strange noises coming from your basement",
|
|
"timestamp": 1547
|
|
},
|
|
{
|
|
"speaker": "miller_gareth",
|
|
"text": "You did? Well, I... I suppose I should tell you. There's been scratching sounds at night. I'm too scared to investigate myself.",
|
|
"timestamp": 1547,
|
|
"mood": "nervous",
|
|
"relationship_change": 5
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"summary": {
|
|
"total_conversations": 23,
|
|
"characters_met": ["miller_gareth", "tavern_owner_rosa", "town_crier"],
|
|
"most_talked_to": "miller_gareth",
|
|
"conversation_themes": ["mill_problems", "village_gossip", "bandit_concerns"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Inventory (inventory.json)
|
|
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"id": "torch",
|
|
"name": "Torch",
|
|
"description": "A sturdy torch that provides light",
|
|
"quantity": 2,
|
|
"equipped": false
|
|
},
|
|
{
|
|
"id": "room_key",
|
|
"name": "Tavern Room Key",
|
|
"description": "A brass key for the tavern's guest room",
|
|
"quantity": 1,
|
|
"equipped": false
|
|
},
|
|
{
|
|
"id": "crowbar",
|
|
"name": "Crowbar",
|
|
"description": "A heavy iron crowbar useful for prying things open",
|
|
"quantity": 1,
|
|
"equipped": true
|
|
}
|
|
],
|
|
"capacity": {
|
|
"current": 3,
|
|
"maximum": 10
|
|
}
|
|
}
|
|
```
|
|
|
|
## Benefits of This Structure
|
|
|
|
Performance:
|
|
|
|
Fast loading/saving with JSON
|
|
Only load relevant files when needed
|
|
Separate files allow partial saves
|
|
|
|
Maintainability:
|
|
|
|
Clear separation of concerns
|
|
Easy to debug specific save issues
|
|
Modular structure matches your game engine
|
|
|
|
Flexibility:
|
|
|
|
Can compress individual files if needed
|
|
Easy to add new save categories
|
|
Version control friendly for development
|
|
|
|
Save Management:
|
|
|
|
Multiple save slots just use different directories
|
|
Can validate save integrity per component
|
|
Easy to implement save/load progress indicators
|
|
|
|
This gives you the best of both worlds: human-readable YAML for content creation and efficient JSON for runtime save data.
|
|
|
|
You'll need several additional systems beyond area connections to support complex game mechanics:
|
|
### 9.1 Combat System
|
|
|
|
Enemy Definitions:
|
|
|
|
```yaml
|
|
# enemies/forest_enemies.yaml
|
|
enemies:
|
|
forest_bandit:
|
|
name: "Forest Bandit"
|
|
stats:
|
|
health: 45
|
|
max_health: 45
|
|
attack: 8
|
|
defense: 3
|
|
speed: 6
|
|
behaviors:
|
|
- type: "aggressive"
|
|
trigger: "player_enters_area"
|
|
actions: ["attack", "call_for_help"]
|
|
- type: "flee"
|
|
trigger: "health_below_25_percent"
|
|
actions: ["run_to_bandit_camp"]
|
|
loot_table:
|
|
- item: "bandit_sword"
|
|
chance: 60
|
|
- item: "gold_coins"
|
|
quantity: [3, 8]
|
|
chance: 100
|
|
ai_context: |
|
|
You're a desperate bandit who attacks travelers for money.
|
|
You're not particularly brave and will flee when badly injured.
|
|
```
|
|
|
|
### 9.2 Advanced Conversation System
|
|
|
|
Dialogue Trees with Complex Triggers:
|
|
|
|
```yaml
|
|
# characters/miller_gareth.yaml
|
|
dialogue:
|
|
topics:
|
|
basement_investigation:
|
|
unlock_conditions:
|
|
- type: "relationship"
|
|
level: "trusted"
|
|
- type: "world_event"
|
|
event: "mill_repaired"
|
|
value: true
|
|
- type: "NOT"
|
|
condition:
|
|
type: "dialogue_flag"
|
|
flag: "basement_revealed"
|
|
|
|
conversation_flow:
|
|
entry:
|
|
npc_text: |
|
|
*shifts nervously and glances toward the basement door*
|
|
Well, since you helped with the mill... I suppose I can trust you.
|
|
There's been strange sounds coming from down there at night.
|
|
|
|
player_options:
|
|
- text: "What kind of sounds?"
|
|
leads_to: "describe_sounds"
|
|
requirements: []
|
|
|
|
- text: "Have you investigated?"
|
|
leads_to: "investigation_attempt"
|
|
requirements: []
|
|
|
|
- text: "I could take a look for you"
|
|
leads_to: "offer_help"
|
|
requirements:
|
|
- type: "character_stat"
|
|
stat: "courage"
|
|
minimum: 3
|
|
|
|
describe_sounds:
|
|
npc_text: |
|
|
Scratching, mostly. Like claws on wood. And sometimes...
|
|
*whispers* sometimes I hear whispers. Not human whispers.
|
|
|
|
effects:
|
|
- type: "add_knowledge"
|
|
knowledge: "basement_has_creature"
|
|
- type: "increase_fear"
|
|
character: "miller_gareth"
|
|
amount: 10
|
|
|
|
player_options:
|
|
- text: "That sounds supernatural"
|
|
leads_to: "supernatural_discussion"
|
|
requirements:
|
|
- type: "character_knowledge"
|
|
knowledge: "magic_exists"
|
|
|
|
- text: "Probably just rats"
|
|
leads_to: "dismiss_concern"
|
|
effects:
|
|
- type: "change_relationship"
|
|
amount: -5
|
|
reason: "dismissed_concerns"
|
|
```
|
|
|
|
### 9.3 Quest System
|
|
|
|
Complex Quest Logic:
|
|
|
|
```yaml
|
|
|
|
# quests/investigate_bandits.yaml
|
|
quests:
|
|
investigate_bandits:
|
|
name: "Investigate Bandit Activity"
|
|
description: "The villagers are concerned about bandit activity in the forest"
|
|
|
|
start_conditions:
|
|
- type: "talked_to_character"
|
|
character: "town_crier"
|
|
topic: "bandit_concerns"
|
|
- type: "world_event"
|
|
event: "bandit_sightings_reported"
|
|
value: true
|
|
|
|
objectives:
|
|
gather_information:
|
|
description: "Talk to villagers about bandit sightings"
|
|
type: "collection"
|
|
target_count: 3
|
|
progress_tracking:
|
|
- character: "miller_gareth"
|
|
topic: "bandit_concerns"
|
|
weight: 1
|
|
- character: "tavern_owner_rosa"
|
|
topic: "stranger_sightings"
|
|
weight: 1
|
|
- character: "guard_captain_marcus"
|
|
topic: "patrol_reports"
|
|
weight: 1
|
|
|
|
investigate_forest:
|
|
description: "Search the forest for bandit signs"
|
|
type: "exploration"
|
|
unlock_conditions:
|
|
- type: "objective_completed"
|
|
objective: "gather_information"
|
|
requirements:
|
|
- type: "visit_area"
|
|
area: "forest_path"
|
|
- type: "find_item"
|
|
item: "bandit_tracks"
|
|
location: "forest_path"
|
|
|
|
confront_bandits:
|
|
description: "Deal with the bandit threat"
|
|
type: "choice"
|
|
unlock_conditions:
|
|
- type: "objective_completed"
|
|
objective: "investigate_forest"
|
|
options:
|
|
- choice: "attack_bandit_camp"
|
|
requirements:
|
|
- type: "character_stat"
|
|
stat: "strength"
|
|
minimum: 5
|
|
effects:
|
|
- type: "start_combat"
|
|
enemy_group: "bandit_patrol"
|
|
- type: "set_world_event"
|
|
event: "chose_violent_approach"
|
|
value: true
|
|
|
|
- choice: "negotiate_with_bandits"
|
|
requirements:
|
|
- type: "character_stat"
|
|
stat: "charisma"
|
|
minimum: 6
|
|
effects:
|
|
- type: "start_dialogue"
|
|
character: "bandit_leader"
|
|
topic: "negotiation"
|
|
```
|
|
|
|
### 9.4 Event System
|
|
|
|
Scripted Events and Triggers:
|
|
|
|
```yaml
|
|
# events/harvest_festival.yaml
|
|
events:
|
|
harvest_festival:
|
|
name: "Harvest Festival"
|
|
description: "The annual village celebration"
|
|
|
|
start_triggers:
|
|
- type: "time_based"
|
|
game_time: 2000
|
|
conditions:
|
|
- type: "world_event"
|
|
event: "mill_repaired"
|
|
value: true
|
|
- type: "quest_status"
|
|
quest: "investigate_bandits"
|
|
status: "completed"
|
|
|
|
phases:
|
|
preparation:
|
|
duration: 200
|
|
description: "Villagers prepare for the festival"
|
|
effects:
|
|
- type: "modify_character_behavior"
|
|
character: "all_villagers"
|
|
new_behavior: "festival_preparation"
|
|
- type: "add_temporary_areas"
|
|
areas: ["festival_grounds", "merchant_stalls"]
|
|
|
|
celebration:
|
|
duration: 300
|
|
description: "The festival is in full swing"
|
|
effects:
|
|
- type: "spawn_temporary_npcs"
|
|
npcs: ["traveling_merchant", "festival_musician"]
|
|
- type: "modify_dialogue"
|
|
characters: "all_villagers"
|
|
add_topics: ["festival_joy", "harvest_success"]
|
|
|
|
random_events:
|
|
- event: "pickpocket_attempt"
|
|
chance: 15
|
|
conditions:
|
|
- type: "player_has_valuable_items"
|
|
minimum_value: 50
|
|
|
|
- event: "mysterious_stranger_appears"
|
|
chance: 25
|
|
conditions:
|
|
- type: "world_event"
|
|
event: "ancient_ruins_explored"
|
|
value: true
|
|
|
|
completion_effects:
|
|
- type: "set_world_event"
|
|
event: "harvest_festival_completed"
|
|
value: true
|
|
- type: "increase_relationship"
|
|
character: "all_villagers"
|
|
amount: 10
|
|
reason: "festival_participation"
|
|
```
|
|
|
|
### 9.5 Conditional Logic System
|
|
|
|
Complex Condition Engine:
|
|
|
|
```yaml
|
|
|
|
# logic/conditions.yaml
|
|
condition_templates:
|
|
multiple_and:
|
|
type: "multiple"
|
|
operator: "AND"
|
|
conditions: []
|
|
|
|
multiple_or:
|
|
type: "multiple"
|
|
operator: "OR"
|
|
conditions: []
|
|
|
|
conditional_chain:
|
|
type: "chain"
|
|
conditions:
|
|
- if:
|
|
type: "character_stat"
|
|
stat: "stealth"
|
|
minimum: 4
|
|
then:
|
|
type: "set_world_event"
|
|
event: "sneaked_past_guards"
|
|
value: true
|
|
else:
|
|
type: "start_combat"
|
|
enemy_group: "guard_patrol"
|
|
|
|
time_and_weather:
|
|
type: "multiple"
|
|
operator: "AND"
|
|
conditions:
|
|
- type: "time_of_day"
|
|
time: "night"
|
|
- type: "weather"
|
|
weather: "stormy"
|
|
failure_message: "You need the cover of a stormy night"
|
|
```
|
|
|
|
### 9.6 Dynamic World State
|
|
|
|
World Reactions to Player Actions:
|
|
|
|
```yaml
|
|
# world/dynamic_systems.yaml
|
|
world_reactions:
|
|
bandit_defeat_consequences:
|
|
trigger:
|
|
type: "world_event"
|
|
event: "bandits_defeated"
|
|
value: true
|
|
|
|
effects:
|
|
- type: "modify_area"
|
|
area: "forest_path"
|
|
changes:
|
|
safety_level: "safe"
|
|
new_connections:
|
|
merchant_road:
|
|
direction: "northwest"
|
|
description: "A now-safe merchant road"
|
|
conditions: []
|
|
|
|
- type: "modify_all_characters"
|
|
filter:
|
|
type: "location"
|
|
locations: ["village_center", "tavern"]
|
|
changes:
|
|
add_dialogue_topic: "bandit_victory_celebration"
|
|
relationship_bonus: 15
|
|
|
|
- type: "spawn_events"
|
|
events: ["merchant_caravan_arrives", "celebration_feast"]
|
|
```
|
|
|
|
### 9.7 AI Behavior System
|
|
|
|
Dynamic NPC Behaviors:
|
|
|
|
```yaml
|
|
# ai/behaviors.yaml
|
|
ai_behaviors:
|
|
worried_villager:
|
|
base_personality: "anxious"
|
|
triggers:
|
|
- condition:
|
|
type: "world_event"
|
|
event: "bandits_active"
|
|
value: true
|
|
behavior_changes:
|
|
dialogue_topics_add: ["safety_concerns", "bandit_fears"]
|
|
movement_restrictions: ["avoid_forest_areas"]
|
|
ai_context_append: "You're worried about bandit attacks and reluctant to travel alone."
|
|
|
|
festival_celebrant:
|
|
base_personality: "joyful"
|
|
triggers:
|
|
- condition:
|
|
type: "event_active"
|
|
event: "harvest_festival"
|
|
behavior_changes:
|
|
dialogue_topics_add: ["festival_joy", "harvest_success"]
|
|
movement_pattern: "festival_participation"
|
|
ai_context_override: "You're in a festive mood, celebrating the harvest with the community."
|
|
```
|
|
|
|
This system provides:
|
|
|
|
Complex combat with AI-driven enemies
|
|
Branching dialogue with multiple unlock conditions
|
|
Multi-objective quests with different completion paths
|
|
Dynamic world events that respond to player actions
|
|
Conditional logic that can handle complex scenarios
|
|
Behavior modification for NPCs based on world state
|
|
|
|
Each system can reference and modify the others, creating emergent gameplay where player choices have cascading effects throughout the game world.
|
|
|
|
## 8. Rust Implementation
|
|
|
|
### 8.1 Core Data Structures
|
|
|
|
```rust
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::any::Any;
|
|
|
|
// Generic container for dynamic data
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
#[serde(untagged)]
|
|
pub enum GameValue {
|
|
String(String),
|
|
Number(f64),
|
|
Bool(bool),
|
|
Array(Vec<GameValue>),
|
|
Object(HashMap<String, GameValue>),
|
|
}
|
|
|
|
// Generic condition system
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct Condition {
|
|
#[serde(rename = "type")]
|
|
pub condition_type: String,
|
|
|
|
// Flatten all other fields into this map
|
|
#[serde(flatten)]
|
|
pub parameters: HashMap<String, GameValue>,
|
|
}
|
|
|
|
// Generic effect system
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct Effect {
|
|
#[serde(rename = "type")]
|
|
pub effect_type: String,
|
|
|
|
#[serde(flatten)]
|
|
pub parameters: HashMap<String, GameValue>,
|
|
}
|
|
|
|
// Generic trait system for inheritable behaviors
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct Trait {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub modifiers: HashMap<String, GameValue>,
|
|
pub behaviors: Vec<String>,
|
|
pub conditions: Vec<Condition>,
|
|
}
|
|
```
|
|
|
|
Game World Structure
|
|
|
|
```rust
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct GameWorld {
|
|
pub game_info: GameInfo,
|
|
pub world_state: WorldState,
|
|
pub areas: HashMap<String, GameArea>,
|
|
pub characters: HashMap<String, Character>,
|
|
pub items: HashMap<String, Item>,
|
|
pub traits: HashMap<String, Trait>,
|
|
pub templates: HashMap<String, Template>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct GameInfo {
|
|
pub name: String,
|
|
pub version: String,
|
|
pub description: String,
|
|
pub dependencies: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct WorldState {
|
|
pub current_time: u64,
|
|
pub weather: String,
|
|
pub events: HashMap<String, GameValue>,
|
|
pub global_variables: HashMap<String, GameValue>,
|
|
}
|
|
```
|
|
|
|
Area System
|
|
|
|
```rust
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct GameArea {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub connections: HashMap<String, Connection>,
|
|
pub items: Vec<String>,
|
|
pub npcs: Vec<String>,
|
|
pub properties: HashMap<String, GameValue>,
|
|
pub events: Vec<AreaEvent>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct Connection {
|
|
pub direction: String,
|
|
pub description: String,
|
|
pub conditions: Vec<Condition>,
|
|
pub effects: Vec<Effect>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct AreaEvent {
|
|
pub name: String,
|
|
pub triggers: Vec<Condition>,
|
|
pub effects: Vec<Effect>,
|
|
pub chance: Option<f64>,
|
|
}
|
|
```
|
|
|
|
Inheritable Character System
|
|
|
|
```rust
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct Character {
|
|
pub name: String,
|
|
pub description: String,
|
|
|
|
// Inheritance system
|
|
pub inherits_from: Option<String>,
|
|
pub traits: Vec<String>,
|
|
|
|
// Core properties
|
|
pub stats: HashMap<String, GameValue>,
|
|
pub location: String,
|
|
pub properties: HashMap<String, GameValue>,
|
|
|
|
// Behavior system
|
|
pub behaviors: Vec<String>,
|
|
pub dialogue: DialogueSystem,
|
|
pub relationships: HashMap<String, Relationship>,
|
|
|
|
// AI context
|
|
pub ai_context: Option<String>,
|
|
pub personality_prompt: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct DialogueSystem {
|
|
pub topics: HashMap<String, DialogueTopic>,
|
|
pub greetings: HashMap<String, String>,
|
|
pub reactions: HashMap<String, DialogueReaction>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct DialogueTopic {
|
|
pub unlock_conditions: Vec<Condition>,
|
|
pub responses: Vec<DialogueResponse>,
|
|
pub effects: Vec<Effect>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct DialogueResponse {
|
|
pub text: String,
|
|
pub conditions: Vec<Condition>,
|
|
pub effects: Vec<Effect>,
|
|
pub next_topic: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct DialogueReaction {
|
|
pub trigger: String,
|
|
pub response: String,
|
|
pub effects: Vec<Effect>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct Relationship {
|
|
pub level: String,
|
|
pub value: f64,
|
|
pub history: Vec<String>,
|
|
}
|
|
```
|
|
|
|
Inheritable Item System
|
|
|
|
```rust
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
```rust
|
|
pub struct Item {
|
|
pub name: String,
|
|
pub description: String,
|
|
|
|
// Inheritance
|
|
pub inherits_from: Option<String>,
|
|
pub traits: Vec<String>,
|
|
|
|
// Properties
|
|
pub properties: HashMap<String, GameValue>,
|
|
pub stats: HashMap<String, GameValue>,
|
|
|
|
// Behavior
|
|
pub actions: Vec<ItemAction>,
|
|
pub effects: Vec<Effect>,
|
|
pub conditions: Vec<Condition>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct ItemAction {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub conditions: Vec<Condition>,
|
|
pub effects: Vec<Effect>,
|
|
}
|
|
|
|
Template System
|
|
|
|
```rust
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct Template {
|
|
pub name: String,
|
|
pub template_type: String, // "character", "item", "area", etc.
|
|
pub properties: HashMap<String, GameValue>,
|
|
pub traits: Vec<String>,
|
|
pub behaviors: Vec<String>,
|
|
}
|
|
```
|
|
|
|
Runtime System for Inheritance
|
|
|
|
```rust
|
|
|
|
pub struct GameEngine {
|
|
pub world: GameWorld,
|
|
pub loaded_files: HashMap<String, GameWorld>,
|
|
pub resolved_cache: HashMap<String, Box<dyn Any>>,
|
|
}
|
|
|
|
impl GameEngine {
|
|
pub fn resolve_character(&mut self, character_id: &str) -> Character {
|
|
// Check cache first
|
|
if let Some(cached) = self.resolved_cache.get(character_id) {
|
|
if let Some(character) = cached.downcast_ref::<Character>() {
|
|
return character.clone();
|
|
}
|
|
}
|
|
|
|
let mut character = self.world.characters[character_id].clone();
|
|
|
|
// Apply inheritance
|
|
if let Some(parent_id) = &character.inherits_from {
|
|
let parent = self.resolve_character(parent_id);
|
|
character = self.merge_character(parent, character);
|
|
}
|
|
|
|
// Apply traits
|
|
for trait_name in &character.traits.clone() {
|
|
if let Some(trait_def) = self.world.traits.get(trait_name) {
|
|
character = self.apply_trait_to_character(character, trait_def);
|
|
}
|
|
}
|
|
|
|
// Cache result
|
|
self.resolved_cache.insert(
|
|
character_id.to_string(),
|
|
Box::new(character.clone())
|
|
);
|
|
|
|
character
|
|
}
|
|
|
|
fn merge_character(&self, parent: Character, child: Character) -> Character {
|
|
let mut merged = parent;
|
|
|
|
// Override with child properties
|
|
merged.name = child.name;
|
|
merged.description = child.description;
|
|
|
|
// Merge stats (child overrides parent)
|
|
for (key, value) in child.stats {
|
|
merged.stats.insert(key, value);
|
|
}
|
|
|
|
// Merge properties
|
|
for (key, value) in child.properties {
|
|
merged.properties.insert(key, value);
|
|
}
|
|
|
|
// Combine traits
|
|
merged.traits.extend(child.traits);
|
|
merged.traits.dedup();
|
|
|
|
// Merge dialogue topics
|
|
for (topic, dialogue) in child.dialogue.topics {
|
|
merged.dialogue.topics.insert(topic, dialogue);
|
|
}
|
|
|
|
merged
|
|
}
|
|
|
|
fn apply_trait_to_character(&self, mut character: Character, trait_def: &Trait) -> Character {
|
|
// Apply trait modifiers
|
|
for (key, modifier) in &trait_def.modifiers {
|
|
match modifier {
|
|
GameValue::Number(n) => {
|
|
// Add to existing stat or create new one
|
|
if let Some(GameValue::Number(existing)) = character.stats.get(key) {
|
|
character.stats.insert(key.clone(), GameValue::Number(existing + n));
|
|
} else {
|
|
character.stats.insert(key.clone(), GameValue::Number(*n));
|
|
}
|
|
}
|
|
_ => {
|
|
character.properties.insert(key.clone(), modifier.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add trait behaviors
|
|
character.behaviors.extend(trait_def.behaviors.clone());
|
|
|
|
character
|
|
}
|
|
}
|
|
```
|
|
|
|
Condition/Effect Evaluation System
|
|
|
|
|
|
```rust
|
|
impl GameEngine {
|
|
pub fn evaluate_condition(&self, condition: &Condition, context: &GameContext) -> bool {
|
|
match condition.condition_type.as_str() {
|
|
"character_stat" => {
|
|
let character_id = self.get_string_param(&condition.parameters, "character")
|
|
.unwrap_or(&context.current_character);
|
|
let stat_name = self.get_string_param(&condition.parameters, "stat").unwrap();
|
|
let minimum = self.get_number_param(&condition.parameters, "minimum").unwrap_or(0.0);
|
|
|
|
if let Some(character) = self.world.characters.get(character_id) {
|
|
if let Some(GameValue::Number(stat_value)) = character.stats.get(stat_name) {
|
|
return *stat_value >= minimum;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
"world_event" => {
|
|
let event_name = self.get_string_param(&condition.parameters, "event").unwrap();
|
|
let expected_value = self.get_param(&condition.parameters, "value").unwrap();
|
|
|
|
if let Some(actual_value) = self.world.world_state.events.get(event_name) {
|
|
return self.values_match(actual_value, expected_value);
|
|
}
|
|
false
|
|
}
|
|
"multiple" => {
|
|
let operator = self.get_string_param(&condition.parameters, "operator").unwrap();
|
|
let conditions = self.get_array_param(&condition.parameters, "conditions").unwrap();
|
|
|
|
match operator {
|
|
"AND" => conditions.iter().all(|c| self.evaluate_condition_value(c, context)),
|
|
"OR" => conditions.iter().any(|c| self.evaluate_condition_value(c, context)),
|
|
_ => false
|
|
}
|
|
}
|
|
_ => {
|
|
// Handle unknown condition types gracefully
|
|
eprintln!("Unknown condition type: {}", condition.condition_type);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper methods for parameter extraction
|
|
fn get_string_param(&self, params: &HashMap<String, GameValue>, key: &str) -> Option<&String> {
|
|
if let Some(GameValue::String(s)) = params.get(key) {
|
|
Some(s)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_number_param(&self, params: &HashMap<String, GameValue>, key: &str) -> Option<f64> {
|
|
if let Some(GameValue::Number(n)) = params.get(key) {
|
|
Some(*n)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct GameContext {
|
|
pub current_character: String,
|
|
pub current_area: String,
|
|
pub player_id: String,
|
|
}
|
|
```
|
|
|
|
Usage Example
|
|
|
|
```rust
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let yaml_content = std::fs::read_to_string("game_world.yaml")?;
|
|
let world: GameWorld = serde_yaml::from_str(&yaml_content)?;
|
|
|
|
let mut engine = GameEngine {
|
|
world,
|
|
loaded_files: HashMap::new(),
|
|
resolved_cache: HashMap::new(),
|
|
};
|
|
|
|
// Resolve a character with inheritance
|
|
let miller = engine.resolve_character("miller_gareth");
|
|
println!("Miller stats: {:?}", miller.stats);
|
|
|
|
// Evaluate conditions
|
|
let context = GameContext {
|
|
current_character: "miller_gareth".to_string(),
|
|
current_area: "millers_house".to_string(),
|
|
player_id: "player".to_string(),
|
|
};
|
|
|
|
let condition = Condition {
|
|
condition_type: "character_stat".to_string(),
|
|
parameters: [
|
|
("character".to_string(), GameValue::String("miller_gareth".to_string())),
|
|
("stat".to_string(), GameValue::String("trust_level".to_string())),
|
|
("minimum".to_string(), GameValue::Number(50.0)),
|
|
].iter().cloned().collect(),
|
|
};
|
|
|
|
let result = engine.evaluate_condition(&condition, &context);
|
|
println!("Condition result: {}", result);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
This approach gives you:
|
|
|
|
Flexible YAML parsing without complex struct hierarchies
|
|
Runtime inheritance for characters and items
|
|
Generic condition/effect system that can handle any parameters
|
|
Caching for performance
|
|
Extensibility - easy to add new condition/effect types
|
|
Type safety where it matters, flexibility where it doesn't
|
|
|
|
The key insight is using `HashMap<String, GameValue>` for dynamic data and resolving inheritance at runtime rather than trying to model everything statically in the type system. |