Implement phases 3-4 + all modules: decision engine, automapper, character creator, quest system, progression optimizer, and hybrid LLM integration

- Phase 4 (Decision Engine): ActionQueue with 7 priority tiers,
  4 behavior states (Idle/Explore/Combat/Loot), CombatAI with skill
  rotation and flee logic, GoalManager with survival override,
  DecisionEngine state machine integrated into the main game loop

- Phase 3 (Automapper): Complete hybrid SQLite/Memvid architecture
  with WorldMapper orchestrator, room fingerprinting (SHA-256),
  bidirectional exit linking, exploration queue, A* pathfinding,
  HistoricalStorage with JSON fallback, SemanticSearch with keyword
  fallback, SyncManager with configurable triggers

- Character Creator: State machine for automated character creation
  with stat rolling/optimization per class weights, race/class/gender
  selection, reroll logic with configurable thresholds

- Quest System: Quest lifecycle tracking (discover->accept->complete),
  NLP-based objective extraction (kill/fetch/talk/explore), EXP/time
  prioritization, pending kill target tracking

- Progression Optimizer: Grinding spot finder (EXP/hour), equipment
  comparison by weighted stats, skill rotation optimizer (DPS vs
  mana efficiency), session summary statistics

- LLM Integration (hybrid): Async Ollama client with retry/backoff,
  LLMBrain coordinator for quest parsing, social AI, decision advice,
  and ambiguous text parsing fallback. SocialAI for detecting and
  responding to player messages (say/tell/shout/group/trade).
  Graceful degradation when Ollama is unavailable.

All 214 tests passing.
This commit is contained in:
truetype committed 2026-02-08 20:55:53 +01:00
1 parent f85c27a742
commit bd211b6036
34 files changed
+5479 -233

No files matched your search

+133 -2
View File
@@ -15,6 +15,13 @@ from core.parsers import ParserFactory, OutputBuffer, EventType
from config.settings import load_config
from utils.logger import setup_logger
from utils.human_behavior import HumanBehavior
from modules.decision import DecisionEngine
from modules.decision.combat_ai import CombatConfig
from modules.mapper import WorldMapper
from modules.character import CharacterCreator, CreatorConfig
from modules.quest import QuestManager
from modules.progression import ProgressionOptimizer
from modules.llm import LLMBrain, LLMConfig, SocialAI
logger = None
@@ -37,11 +44,67 @@ async def main_loop(config):
mud_parser = ParserFactory.create(config.mud.mud_type)
output_buffer = OutputBuffer(mud_parser)
# Initialize Decision Engine (Phase 4)
combat_cfg = CombatConfig()
decision_engine = DecisionEngine(
combat_config=combat_cfg,
exploration_strategy=config.ai.exploration_strategy,
)
decision_engine.goal_manager.set_default_goals()
# Initialize WorldMapper (Phase 3 — hybrid SQLite/Memvid)
world_mapper = WorldMapper(db_path=config.mapper.active_storage.db_path)
# Initialize Character Creator (Phase 2)
creator_cfg = CreatorConfig(
preferred_race=config.character.preferred_race,
preferred_class=config.character.preferred_class,
)
character_creator = CharacterCreator(creator_cfg)
# Initialize Quest Manager & Progression Optimizer
quest_manager = QuestManager()
progression = ProgressionOptimizer()
# Initialize LLM Brain (hybrid AI)
llm_config = LLMConfig(
base_url=config.ai.llm.base_url,
model=config.ai.llm.model,
temperature=config.ai.llm.temperature,
max_tokens=config.ai.llm.max_tokens,
timeout_seconds=config.ai.llm.timeout_seconds,
max_retries=config.ai.llm.max_retries,
system_prompt=config.ai.llm.system_prompt,
)
llm_brain = LLMBrain(llm_config)
social_ai = SocialAI(
client=llm_brain.client,
character_name=config.character.name,
character_class=config.character.preferred_class,
)
if config.ai.llm.enabled:
await llm_brain.initialize()
logger.info(
"Parser: %s (mud_type=%s)",
type(mud_parser).__name__,
config.mud.mud_type,
)
logger.info(
"Decision engine ready | strategy=%s | goals=%d",
config.ai.exploration_strategy,
decision_engine.goal_manager.active_goal_count,
)
logger.info(
"WorldMapper ready | rooms=%d | exits=%d",
world_mapper.total_rooms,
world_mapper.total_exits,
)
logger.info(
"LLM Brain: %s | model=%s",
"online" if llm_brain.is_available else "offline (rule-based fallback)",
config.ai.llm.model,
)
# Connect to MUD
logger.info("Connecting to %s:%s", config.mud.host, config.mud.port)
@@ -113,12 +176,21 @@ async def main_loop(config):
elif event.event_type == EventType.ROOM and event.room:
game_state.update_room(event.room)
# Update automapper
world_mapper.on_room_enter(
title=event.room.title or "",
description=event.room.description or "",
exits=event.room.exits,
npcs=event.room.npcs,
items=event.room.items,
)
logger.info(
"Room: '%s' | Exits: %s | NPCs: %s | Items: %s",
"Room: '%s' | Exits: %s | NPCs: %s | Items: %s | mapped=%d",
event.room.title,
event.room.exits,
event.room.npcs,
event.room.items,
world_mapper.total_rooms,
)
elif event.event_type == EventType.COMBAT and event.combat:
@@ -131,6 +203,22 @@ async def main_loop(config):
event.combat.damage,
)
# --- Social AI: detect player messages in the raw block ---
if llm_brain.is_available:
social_event = social_ai.detect_social_event(block)
if social_event:
logger.info(
"Social: %s from %s: %s",
social_event["type"],
social_event["speaker"],
social_event["message"][:100],
)
reply_cmd = await social_ai.respond(social_event)
if reply_cmd:
await human_behavior.delay()
await telnet.send(reply_cmd)
logger.info("Social reply: %s", reply_cmd)
telnet.set_receive_callback(on_receive)
# Start receive loop
@@ -142,6 +230,17 @@ async def main_loop(config):
# Keep connection alive
last_keep_alive = asyncio.get_running_loop().time()
last_decision = asyncio.get_running_loop().time()
# Decision interval: how often (in seconds) the engine ticks
decision_interval_min = config.ai.decision_delay_ms[0] / 1000.0
decision_interval_max = (
config.ai.decision_delay_ms[1] / 1000.0
if len(config.ai.decision_delay_ms) > 1
else decision_interval_min
)
import random as _rng
next_decision_delay = _rng.uniform(decision_interval_min, decision_interval_max)
while telnet.connected:
current_time = asyncio.get_running_loop().time()
@@ -155,7 +254,32 @@ async def main_loop(config):
await telnet.send(config.mud.keep_alive_command)
last_keep_alive = current_time
await asyncio.sleep(1.0)
# ----- Decision Engine Tick -----
if current_time - last_decision >= next_decision_delay:
# Let the engine evaluate the world and queue actions
decision_engine.tick(game_state)
# Execute the next action from the queue
action = decision_engine.next_action()
if action:
logger.info(
"Executing: '%s' [%s] (%s)",
action.command,
action.priority.name,
action.description,
)
# Notify mapper of movement commands
if action.source == "explore" and action.priority.name == "MOVE":
world_mapper.on_move(action.command)
await human_behavior.delay()
await telnet.send(action.command)
last_decision = current_time
next_decision_delay = _rng.uniform(
decision_interval_min, decision_interval_max
)
await asyncio.sleep(0.25)
except KeyboardInterrupt:
logger.info("Shutting down...")
@@ -168,6 +292,13 @@ async def main_loop(config):
if event.event_type == EventType.ROOM and event.room:
game_state.update_room(event.room)
# Close mapper and save state
logger.info(
"Session stats: %d rooms mapped, %d exits",
world_mapper.total_rooms, world_mapper.total_exits,
)
world_mapper.close()
await telnet.disconnect()
receive_task.cancel()