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:
1 parent
f85c27a742
commit
bd211b6036
34 files changed
+5479
-233
No files matched your search
+133
-2
@@ -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()
|
||||
|
||||
|
||||
Reference in new issue
Block a user