[upd] Enhance performance configuration and logging optimizations in simulation engine
This commit is contained in:
parent
d190e3efe5
commit
b1b256e520
@ -250,9 +250,28 @@ class SinksConfig:
|
|||||||
fire_wood_cost_per_night: int = 1 # Wood consumed to stay warm at night
|
fire_wood_cost_per_night: int = 1 # Wood consumed to stay warm at night
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PerformanceConfig:
|
||||||
|
"""Configuration for performance optimization.
|
||||||
|
|
||||||
|
Controls logging and memory usage to keep simulation fast at high turn counts.
|
||||||
|
"""
|
||||||
|
# Logging control
|
||||||
|
logging_enabled: bool = False # Enable file logging (disable for speed)
|
||||||
|
detailed_logging: bool = False # Enable verbose per-agent logging
|
||||||
|
log_flush_interval: int = 50 # Flush logs every N turns (not every turn)
|
||||||
|
|
||||||
|
# Memory management
|
||||||
|
max_turn_logs: int = 100 # Keep only last N turn logs in memory
|
||||||
|
|
||||||
|
# Statistics calculation frequency
|
||||||
|
stats_update_interval: int = 10 # Update expensive stats every N turns
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SimulationConfig:
|
class SimulationConfig:
|
||||||
"""Master configuration containing all sub-configs."""
|
"""Master configuration containing all sub-configs."""
|
||||||
|
performance: PerformanceConfig = field(default_factory=PerformanceConfig)
|
||||||
agent_stats: AgentStatsConfig = field(default_factory=AgentStatsConfig)
|
agent_stats: AgentStatsConfig = field(default_factory=AgentStatsConfig)
|
||||||
resources: ResourceConfig = field(default_factory=ResourceConfig)
|
resources: ResourceConfig = field(default_factory=ResourceConfig)
|
||||||
actions: ActionConfig = field(default_factory=ActionConfig)
|
actions: ActionConfig = field(default_factory=ActionConfig)
|
||||||
@ -270,6 +289,7 @@ class SimulationConfig:
|
|||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert to dictionary."""
|
"""Convert to dictionary."""
|
||||||
return {
|
return {
|
||||||
|
"performance": asdict(self.performance),
|
||||||
"ai": asdict(self.ai),
|
"ai": asdict(self.ai),
|
||||||
"agent_stats": asdict(self.agent_stats),
|
"agent_stats": asdict(self.agent_stats),
|
||||||
"resources": asdict(self.resources),
|
"resources": asdict(self.resources),
|
||||||
@ -287,6 +307,7 @@ class SimulationConfig:
|
|||||||
def from_dict(cls, data: dict) -> "SimulationConfig":
|
def from_dict(cls, data: dict) -> "SimulationConfig":
|
||||||
"""Create from dictionary."""
|
"""Create from dictionary."""
|
||||||
return cls(
|
return cls(
|
||||||
|
performance=PerformanceConfig(**data.get("performance", {})),
|
||||||
ai=AIConfig(**data.get("ai", {})),
|
ai=AIConfig(**data.get("ai", {})),
|
||||||
agent_stats=AgentStatsConfig(**data.get("agent_stats", {})),
|
agent_stats=AgentStatsConfig(**data.get("agent_stats", {})),
|
||||||
resources=ResourceConfig(**data.get("resources", {})),
|
resources=ResourceConfig(**data.get("resources", {})),
|
||||||
@ -387,3 +408,9 @@ def _reset_all_caches() -> None:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from backend.core.ai import reset_ai_config_cache
|
||||||
|
reset_ai_config_cache()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -101,18 +101,34 @@ def get_energy_cost(resource_type: ResourceType) -> int:
|
|||||||
return int(energy_cost / avg_output)
|
return int(energy_cost / avg_output)
|
||||||
|
|
||||||
|
|
||||||
|
# Cached config values to avoid repeated lookups
|
||||||
|
_cached_ai_config = None
|
||||||
|
_cached_economy_config = None
|
||||||
|
|
||||||
|
|
||||||
def _get_ai_config():
|
def _get_ai_config():
|
||||||
"""Get AI-relevant configuration values."""
|
"""Get AI-relevant configuration values (cached)."""
|
||||||
from backend.config import get_config
|
global _cached_ai_config
|
||||||
config = get_config()
|
if _cached_ai_config is None:
|
||||||
return config.agent_stats
|
from backend.config import get_config
|
||||||
|
_cached_ai_config = get_config().agent_stats
|
||||||
|
return _cached_ai_config
|
||||||
|
|
||||||
|
|
||||||
def _get_economy_config():
|
def _get_economy_config():
|
||||||
"""Get economy/market configuration values."""
|
"""Get economy/market configuration values (cached)."""
|
||||||
from backend.config import get_config
|
global _cached_economy_config
|
||||||
config = get_config()
|
if _cached_economy_config is None:
|
||||||
return getattr(config, 'economy', None)
|
from backend.config import get_config
|
||||||
|
_cached_economy_config = getattr(get_config(), 'economy', None)
|
||||||
|
return _cached_economy_config
|
||||||
|
|
||||||
|
|
||||||
|
def reset_ai_config_cache():
|
||||||
|
"""Reset the cached config values (call after config reload)."""
|
||||||
|
global _cached_ai_config, _cached_economy_config
|
||||||
|
_cached_ai_config = None
|
||||||
|
_cached_economy_config = None
|
||||||
|
|
||||||
|
|
||||||
class AgentAI:
|
class AgentAI:
|
||||||
|
|||||||
@ -299,7 +299,12 @@ class GameEngine:
|
|||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.logger.close()
|
self.logger.close()
|
||||||
|
|
||||||
|
# Keep turn_logs bounded to prevent memory growth
|
||||||
|
max_logs = get_config().performance.max_turn_logs
|
||||||
self.turn_logs.append(turn_log)
|
self.turn_logs.append(turn_log)
|
||||||
|
if len(self.turn_logs) > max_logs:
|
||||||
|
# Remove oldest logs, keep only recent ones
|
||||||
|
self.turn_logs = self.turn_logs[-max_logs:]
|
||||||
return turn_log
|
return turn_log
|
||||||
|
|
||||||
def _mark_dead_agents(self, current_turn: int) -> list[Agent]:
|
def _mark_dead_agents(self, current_turn: int) -> list[Agent]:
|
||||||
@ -342,6 +347,8 @@ class GameEngine:
|
|||||||
|
|
||||||
for agent in to_remove:
|
for agent in to_remove:
|
||||||
self.world.agents.remove(agent)
|
self.world.agents.remove(agent)
|
||||||
|
# Remove from index as well
|
||||||
|
self.world._agent_index.pop(agent.id, None)
|
||||||
# Note: death was already recorded in _mark_dead_agents
|
# Note: death was already recorded in _mark_dead_agents
|
||||||
|
|
||||||
return to_remove
|
return to_remove
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
"""Simulation logger for detailed step-by-step logging."""
|
"""Simulation logger for detailed step-by-step logging.
|
||||||
|
|
||||||
|
Performance-optimized: logging can be disabled or reduced via config.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -58,61 +61,77 @@ class TurnLogEntry:
|
|||||||
|
|
||||||
|
|
||||||
class SimulationLogger:
|
class SimulationLogger:
|
||||||
"""Logger that dumps detailed simulation data to files."""
|
"""Logger that dumps detailed simulation data to files.
|
||||||
|
|
||||||
|
Performance optimized:
|
||||||
|
- Logging can be disabled entirely via config
|
||||||
|
- File flushing is batched (not every turn)
|
||||||
|
- Agent lookups use O(1) dict instead of O(n) list search
|
||||||
|
- No in-memory accumulation of all entries
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, log_dir: str = "logs"):
|
def __init__(self, log_dir: str = "logs"):
|
||||||
self.log_dir = Path(log_dir)
|
self.log_dir = Path(log_dir)
|
||||||
|
|
||||||
|
# Load performance config
|
||||||
|
from backend.config import get_config
|
||||||
|
perf_config = get_config().performance
|
||||||
|
self.logging_enabled = perf_config.logging_enabled
|
||||||
|
self.detailed_logging = perf_config.detailed_logging
|
||||||
|
self.flush_interval = perf_config.log_flush_interval
|
||||||
|
|
||||||
|
# File handles (only created if logging enabled)
|
||||||
|
self._json_file: Optional[TextIO] = None
|
||||||
|
self._summary_file: Optional[TextIO] = None
|
||||||
|
|
||||||
|
# Standard Python logging (minimal overhead even when enabled)
|
||||||
|
self.logger = logging.getLogger("simulation")
|
||||||
|
self.logger.setLevel(logging.WARNING) # Only warnings by default
|
||||||
|
|
||||||
|
# Current turn tracking
|
||||||
|
self._current_entry: Optional[TurnLogEntry] = None
|
||||||
|
# O(1) lookup for agent entries by ID
|
||||||
|
self._agent_entry_map: dict[str, AgentLogEntry] = {}
|
||||||
|
|
||||||
|
# Turn counter for flush batching
|
||||||
|
self._turns_since_flush = 0
|
||||||
|
|
||||||
|
def start_session(self, config: dict) -> None:
|
||||||
|
"""Start a new logging session."""
|
||||||
|
if not self.logging_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
self.log_dir.mkdir(exist_ok=True)
|
self.log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
# Create session-specific log file
|
# Create session-specific log file
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
self.session_file = self.log_dir / f"sim_{timestamp}.jsonl"
|
session_file = self.log_dir / f"sim_{timestamp}.jsonl"
|
||||||
self.summary_file = self.log_dir / f"sim_{timestamp}_summary.txt"
|
summary_file = self.log_dir / f"sim_{timestamp}_summary.txt"
|
||||||
|
|
||||||
# File handles
|
self._json_file = open(session_file, "w")
|
||||||
self._json_file: Optional[TextIO] = None
|
self._summary_file = open(summary_file, "w")
|
||||||
self._summary_file: Optional[TextIO] = None
|
|
||||||
|
|
||||||
# Also set up standard Python logging
|
|
||||||
self.logger = logging.getLogger("simulation")
|
|
||||||
self.logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
# File handler for detailed logs
|
|
||||||
file_handler = logging.FileHandler(self.log_dir / f"sim_{timestamp}.log")
|
|
||||||
file_handler.setLevel(logging.DEBUG)
|
|
||||||
file_handler.setFormatter(logging.Formatter(
|
|
||||||
"%(asctime)s | %(levelname)s | %(message)s"
|
|
||||||
))
|
|
||||||
self.logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
# Console handler for important events
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.INFO)
|
|
||||||
console_handler.setFormatter(logging.Formatter(
|
|
||||||
"%(asctime)s | %(message)s", datefmt="%H:%M:%S"
|
|
||||||
))
|
|
||||||
self.logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
self._entries: list[TurnLogEntry] = []
|
|
||||||
self._current_entry: Optional[TurnLogEntry] = None
|
|
||||||
|
|
||||||
def start_session(self, config: dict) -> None:
|
|
||||||
"""Start a new logging session."""
|
|
||||||
self._json_file = open(self.session_file, "w")
|
|
||||||
self._summary_file = open(self.summary_file, "w")
|
|
||||||
|
|
||||||
# Write config as first line
|
# Write config as first line
|
||||||
self._json_file.write(json.dumps({"type": "config", "data": config}) + "\n")
|
self._json_file.write(json.dumps({"type": "config", "data": config}) + "\n")
|
||||||
self._json_file.flush()
|
|
||||||
|
|
||||||
self._summary_file.write(f"Simulation Session Started: {datetime.now()}\n")
|
self._summary_file.write(f"Simulation Session Started: {datetime.now()}\n")
|
||||||
self._summary_file.write("=" * 60 + "\n\n")
|
self._summary_file.write("=" * 60 + "\n\n")
|
||||||
self._summary_file.flush()
|
|
||||||
|
|
||||||
self.logger.info(f"Logging session started: {self.session_file}")
|
if self.detailed_logging:
|
||||||
|
# Set up file handler for detailed logs
|
||||||
|
file_handler = logging.FileHandler(self.log_dir / f"sim_{timestamp}.log")
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
file_handler.setFormatter(logging.Formatter(
|
||||||
|
"%(asctime)s | %(levelname)s | %(message)s"
|
||||||
|
))
|
||||||
|
self.logger.addHandler(file_handler)
|
||||||
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
def start_turn(self, turn: int, day: int, step_in_day: int, time_of_day: str) -> None:
|
def start_turn(self, turn: int, day: int, step_in_day: int, time_of_day: str) -> None:
|
||||||
"""Start logging a new turn."""
|
"""Start logging a new turn."""
|
||||||
|
if not self.logging_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
self._current_entry = TurnLogEntry(
|
self._current_entry = TurnLogEntry(
|
||||||
turn=turn,
|
turn=turn,
|
||||||
day=day,
|
day=day,
|
||||||
@ -120,7 +139,10 @@ class SimulationLogger:
|
|||||||
time_of_day=time_of_day,
|
time_of_day=time_of_day,
|
||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Turn {turn} started (Day {day}, Step {step_in_day}, {time_of_day})")
|
self._agent_entry_map.clear()
|
||||||
|
|
||||||
|
if self.detailed_logging:
|
||||||
|
self.logger.debug(f"Turn {turn} started (Day {day}, Step {step_in_day}, {time_of_day})")
|
||||||
|
|
||||||
def log_agent_before(
|
def log_agent_before(
|
||||||
self,
|
self,
|
||||||
@ -133,39 +155,41 @@ class SimulationLogger:
|
|||||||
money: int,
|
money: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Log agent state before action."""
|
"""Log agent state before action."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create placeholder entry
|
# Create entry and add to both list and map
|
||||||
entry = AgentLogEntry(
|
entry = AgentLogEntry(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
agent_name=agent_name,
|
agent_name=agent_name,
|
||||||
profession=profession,
|
profession=profession,
|
||||||
position=position.copy(),
|
position=position,
|
||||||
stats_before=stats.copy(),
|
stats_before=stats,
|
||||||
stats_after={},
|
stats_after={},
|
||||||
decision={},
|
decision={},
|
||||||
action_result={},
|
action_result={},
|
||||||
inventory_before=inventory.copy(),
|
inventory_before=inventory,
|
||||||
inventory_after=[],
|
inventory_after=[],
|
||||||
money_before=money,
|
money_before=money,
|
||||||
money_after=money,
|
money_after=money,
|
||||||
)
|
)
|
||||||
self._current_entry.agent_entries.append(entry)
|
self._current_entry.agent_entries.append(entry)
|
||||||
|
self._agent_entry_map[agent_id] = entry
|
||||||
|
|
||||||
def log_agent_decision(self, agent_id: str, decision: dict) -> None:
|
def log_agent_decision(self, agent_id: str, decision: dict) -> None:
|
||||||
"""Log agent's AI decision."""
|
"""Log agent's AI decision."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for entry in self._current_entry.agent_entries:
|
# O(1) lookup instead of O(n) search
|
||||||
if entry.agent_id == agent_id:
|
entry = self._agent_entry_map.get(agent_id)
|
||||||
entry.decision = decision.copy()
|
if entry:
|
||||||
|
entry.decision = decision
|
||||||
|
if self.detailed_logging:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f" {entry.agent_name}: decided to {decision.get('action', '?')} "
|
f" {entry.agent_name}: decided to {decision.get('action', '?')} "
|
||||||
f"- {decision.get('reason', '')}"
|
f"- {decision.get('reason', '')}"
|
||||||
)
|
)
|
||||||
break
|
|
||||||
|
|
||||||
def log_agent_after(
|
def log_agent_after(
|
||||||
self,
|
self,
|
||||||
@ -177,102 +201,115 @@ class SimulationLogger:
|
|||||||
action_result: dict,
|
action_result: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Log agent state after action."""
|
"""Log agent state after action."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for entry in self._current_entry.agent_entries:
|
# O(1) lookup instead of O(n) search
|
||||||
if entry.agent_id == agent_id:
|
entry = self._agent_entry_map.get(agent_id)
|
||||||
entry.stats_after = stats.copy()
|
if entry:
|
||||||
entry.inventory_after = inventory.copy()
|
entry.stats_after = stats
|
||||||
entry.money_after = money
|
entry.inventory_after = inventory
|
||||||
entry.position = position.copy()
|
entry.money_after = money
|
||||||
entry.action_result = action_result.copy()
|
entry.position = position
|
||||||
break
|
entry.action_result = action_result
|
||||||
|
|
||||||
def log_market_state(self, orders_before: list, orders_after: list) -> None:
|
def log_market_state(self, orders_before: list, orders_after: list) -> None:
|
||||||
"""Log market state."""
|
"""Log market state."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
self._current_entry.market_orders_before = orders_before
|
self._current_entry.market_orders_before = orders_before
|
||||||
self._current_entry.market_orders_after = orders_after
|
self._current_entry.market_orders_after = orders_after
|
||||||
|
|
||||||
def log_trade(self, trade: dict) -> None:
|
def log_trade(self, trade: dict) -> None:
|
||||||
"""Log a trade transaction."""
|
"""Log a trade transaction."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
self._current_entry.trades.append(trade)
|
self._current_entry.trades.append(trade)
|
||||||
self.logger.debug(f" Trade: {trade.get('message', 'Unknown trade')}")
|
if self.detailed_logging:
|
||||||
|
self.logger.debug(f" Trade: {trade.get('message', 'Unknown trade')}")
|
||||||
|
|
||||||
def log_death(self, agent_name: str, cause: str) -> None:
|
def log_death(self, agent_name: str, cause: str) -> None:
|
||||||
"""Log an agent death."""
|
"""Log an agent death."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
self._current_entry.deaths.append({"name": agent_name, "cause": cause})
|
self._current_entry.deaths.append({"name": agent_name, "cause": cause})
|
||||||
|
# Always log deaths even without detailed logging
|
||||||
self.logger.info(f" DEATH: {agent_name} died from {cause}")
|
self.logger.info(f" DEATH: {agent_name} died from {cause}")
|
||||||
|
|
||||||
def log_event(self, event_type: str, event_data: dict) -> None:
|
def log_event(self, event_type: str, event_data: dict) -> None:
|
||||||
"""Log a general event (births, random events, etc.)."""
|
"""Log a general event (births, random events, etc.)."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type == "birth":
|
if event_type == "birth":
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f" BIRTH: {event_data.get('child_name', '?')} born to {event_data.get('parent_name', '?')}"
|
f" BIRTH: {event_data.get('child_name', '?')} born to {event_data.get('parent_name', '?')}"
|
||||||
)
|
)
|
||||||
elif event_type == "random_event":
|
elif event_type == "random_event" and self.detailed_logging:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f" EVENT: {event_data.get('type', '?')} affecting {event_data.get('affected', [])}"
|
f" EVENT: {event_data.get('type', '?')} affecting {event_data.get('affected', [])}"
|
||||||
)
|
)
|
||||||
else:
|
elif self.detailed_logging:
|
||||||
self.logger.debug(f" Event [{event_type}]: {event_data}")
|
self.logger.debug(f" Event [{event_type}]: {event_data}")
|
||||||
|
|
||||||
def log_statistics(self, stats: dict) -> None:
|
def log_statistics(self, stats: dict) -> None:
|
||||||
"""Log end-of-turn statistics."""
|
"""Log end-of-turn statistics."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
self._current_entry.statistics = stats.copy()
|
self._current_entry.statistics = stats
|
||||||
|
|
||||||
def end_turn(self) -> None:
|
def end_turn(self) -> None:
|
||||||
"""Finish logging the current turn and write to file."""
|
"""Finish logging the current turn and write to file."""
|
||||||
if self._current_entry is None:
|
if not self.logging_enabled or self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._entries.append(self._current_entry)
|
# Write to JSON lines file (without flush every time)
|
||||||
|
|
||||||
# Write to JSON lines file
|
|
||||||
if self._json_file:
|
if self._json_file:
|
||||||
self._json_file.write(
|
self._json_file.write(
|
||||||
json.dumps({"type": "turn", "data": self._current_entry.to_dict()}) + "\n"
|
json.dumps({"type": "turn", "data": self._current_entry.to_dict()}) + "\n"
|
||||||
)
|
)
|
||||||
self._json_file.flush()
|
|
||||||
|
|
||||||
# Write summary
|
# Write summary (without flush every time)
|
||||||
if self._summary_file:
|
if self._summary_file:
|
||||||
entry = self._current_entry
|
entry = self._current_entry
|
||||||
self._summary_file.write(
|
self._summary_file.write(
|
||||||
f"Turn {entry.turn} | Day {entry.day} Step {entry.step_in_day} ({entry.time_of_day})\n"
|
f"Turn {entry.turn} | Day {entry.day} Step {entry.step_in_day} ({entry.time_of_day})\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
for agent in entry.agent_entries:
|
if self.detailed_logging:
|
||||||
action = agent.decision.get("action", "?")
|
for agent in entry.agent_entries:
|
||||||
result = "✓" if agent.action_result.get("success", False) else "✗"
|
action = agent.decision.get("action", "?")
|
||||||
self._summary_file.write(
|
result = "✓" if agent.action_result.get("success", False) else "✗"
|
||||||
f" [{agent.agent_name}] {action} {result} | "
|
self._summary_file.write(
|
||||||
f"E:{agent.stats_after.get('energy', '?')} "
|
f" [{agent.agent_name}] {action} {result} | "
|
||||||
f"H:{agent.stats_after.get('hunger', '?')} "
|
f"E:{agent.stats_after.get('energy', '?')} "
|
||||||
f"T:{agent.stats_after.get('thirst', '?')} "
|
f"H:{agent.stats_after.get('hunger', '?')} "
|
||||||
f"${agent.money_after}\n"
|
f"T:{agent.stats_after.get('thirst', '?')} "
|
||||||
)
|
f"${agent.money_after}\n"
|
||||||
|
)
|
||||||
|
|
||||||
if entry.deaths:
|
if entry.deaths:
|
||||||
for death in entry.deaths:
|
for death in entry.deaths:
|
||||||
self._summary_file.write(f" 💀 {death['name']} died: {death['cause']}\n")
|
self._summary_file.write(f" 💀 {death['name']} died: {death['cause']}\n")
|
||||||
|
|
||||||
self._summary_file.write("\n")
|
self._summary_file.write("\n")
|
||||||
self._summary_file.flush()
|
|
||||||
|
|
||||||
self.logger.debug(f"Turn {self._current_entry.turn} completed")
|
# Batched flush - only flush every N turns
|
||||||
|
self._turns_since_flush += 1
|
||||||
|
if self._turns_since_flush >= self.flush_interval:
|
||||||
|
self._flush_files()
|
||||||
|
self._turns_since_flush = 0
|
||||||
|
|
||||||
|
# Clear current entry (don't accumulate in memory)
|
||||||
self._current_entry = None
|
self._current_entry = None
|
||||||
|
self._agent_entry_map.clear()
|
||||||
|
|
||||||
|
def _flush_files(self) -> None:
|
||||||
|
"""Flush file buffers to disk."""
|
||||||
|
if self._json_file:
|
||||||
|
self._json_file.flush()
|
||||||
|
if self._summary_file:
|
||||||
|
self._summary_file.flush()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close log files."""
|
"""Close log files."""
|
||||||
@ -283,11 +320,13 @@ class SimulationLogger:
|
|||||||
self._summary_file.write(f"\nSession ended: {datetime.now()}\n")
|
self._summary_file.write(f"\nSession ended: {datetime.now()}\n")
|
||||||
self._summary_file.close()
|
self._summary_file.close()
|
||||||
self._summary_file = None
|
self._summary_file = None
|
||||||
self.logger.info("Logging session closed")
|
|
||||||
|
|
||||||
def get_entries(self) -> list[TurnLogEntry]:
|
def get_entries(self) -> list[TurnLogEntry]:
|
||||||
"""Get all logged entries."""
|
"""Get all logged entries.
|
||||||
return self._entries.copy()
|
|
||||||
|
Note: Returns empty list when logging optimized (entries not kept in memory).
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
# Global logger instance
|
# Global logger instance
|
||||||
@ -309,4 +348,3 @@ def reset_simulation_logger() -> SimulationLogger:
|
|||||||
_logger.close()
|
_logger.close()
|
||||||
_logger = SimulationLogger()
|
_logger = SimulationLogger()
|
||||||
return _logger
|
return _logger
|
||||||
|
|
||||||
|
|||||||
@ -294,6 +294,9 @@ class OrderBook:
|
|||||||
# Record sale for price history (we need current_turn but don't have it here)
|
# Record sale for price history (we need current_turn but don't have it here)
|
||||||
# The turn will be passed via the _record_sale call from engine
|
# The turn will be passed via the _record_sale call from engine
|
||||||
self.trade_history.append(result)
|
self.trade_history.append(result)
|
||||||
|
# Keep trade history bounded to prevent memory growth
|
||||||
|
if len(self.trade_history) > 1000:
|
||||||
|
self.trade_history = self.trade_history[-500:]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def execute_multi_buy(
|
def execute_multi_buy(
|
||||||
|
|||||||
@ -68,6 +68,9 @@ class World:
|
|||||||
step_in_day: int = 0
|
step_in_day: int = 0
|
||||||
time_of_day: TimeOfDay = TimeOfDay.DAY
|
time_of_day: TimeOfDay = TimeOfDay.DAY
|
||||||
|
|
||||||
|
# Agent index for O(1) lookups by ID
|
||||||
|
_agent_index: dict = field(default_factory=dict)
|
||||||
|
|
||||||
# Statistics
|
# Statistics
|
||||||
total_agents_spawned: int = 0
|
total_agents_spawned: int = 0
|
||||||
total_agents_died: int = 0
|
total_agents_died: int = 0
|
||||||
@ -87,6 +90,10 @@ class World:
|
|||||||
"clothes": 0,
|
"clothes": 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Cached statistics (updated periodically for performance)
|
||||||
|
_cached_stats: Optional[dict] = field(default=None)
|
||||||
|
_stats_cache_turn: int = field(default=-1)
|
||||||
|
|
||||||
def spawn_agent(
|
def spawn_agent(
|
||||||
self,
|
self,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
@ -154,6 +161,7 @@ class World:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.agents.append(agent)
|
self.agents.append(agent)
|
||||||
|
self._agent_index[agent.id] = agent # Maintain index for O(1) lookups
|
||||||
self.total_agents_spawned += 1
|
self.total_agents_spawned += 1
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
@ -327,11 +335,8 @@ class World:
|
|||||||
return inheritance_info
|
return inheritance_info
|
||||||
|
|
||||||
def get_agent(self, agent_id: str) -> Optional[Agent]:
|
def get_agent(self, agent_id: str) -> Optional[Agent]:
|
||||||
"""Get an agent by ID."""
|
"""Get an agent by ID (O(1) lookup via index)."""
|
||||||
for agent in self.agents:
|
return self._agent_index.get(agent_id)
|
||||||
if agent.id == agent_id:
|
|
||||||
return agent
|
|
||||||
return None
|
|
||||||
|
|
||||||
def remove_dead_agents(self) -> list[Agent]:
|
def remove_dead_agents(self) -> list[Agent]:
|
||||||
"""Remove all dead agents from the world. Returns list of removed agents.
|
"""Remove all dead agents from the world. Returns list of removed agents.
|
||||||
@ -457,7 +462,23 @@ class World:
|
|||||||
return [a for a in self.agents if a.is_alive() and not a.is_corpse()]
|
return [a for a in self.agents if a.is_alive() and not a.is_corpse()]
|
||||||
|
|
||||||
def get_statistics(self) -> dict:
|
def get_statistics(self) -> dict:
|
||||||
"""Get current world statistics including wealth distribution and demographics."""
|
"""Get current world statistics including wealth distribution and demographics.
|
||||||
|
|
||||||
|
Uses caching based on performance config to avoid recalculating every turn.
|
||||||
|
"""
|
||||||
|
from backend.config import get_config
|
||||||
|
perf_config = get_config().performance
|
||||||
|
|
||||||
|
# Check if we can use cached stats
|
||||||
|
if (self._cached_stats is not None and
|
||||||
|
self.current_turn - self._stats_cache_turn < perf_config.stats_update_interval):
|
||||||
|
# Update just the essential changing values
|
||||||
|
self._cached_stats["current_turn"] = self.current_turn
|
||||||
|
self._cached_stats["current_day"] = self.current_day
|
||||||
|
self._cached_stats["step_in_day"] = self.step_in_day
|
||||||
|
self._cached_stats["time_of_day"] = self.time_of_day.value
|
||||||
|
return self._cached_stats
|
||||||
|
|
||||||
living = self.get_living_agents()
|
living = self.get_living_agents()
|
||||||
total_money = sum(a.money for a in living)
|
total_money = sum(a.money for a in living)
|
||||||
|
|
||||||
@ -491,11 +512,13 @@ class World:
|
|||||||
richest = moneys[-1] if moneys else 0
|
richest = moneys[-1] if moneys else 0
|
||||||
poorest = moneys[0] if moneys else 0
|
poorest = moneys[0] if moneys else 0
|
||||||
|
|
||||||
# Gini coefficient for inequality (0 = perfect equality, 1 = max inequality)
|
# Gini coefficient - O(n) algorithm instead of O(n²)
|
||||||
|
# Uses sorted list: Gini = (2 * sum(i * x_i)) / (n * sum(x_i)) - (n + 1) / n
|
||||||
n = len(moneys)
|
n = len(moneys)
|
||||||
if n > 1 and total_money > 0:
|
if n > 1 and total_money > 0:
|
||||||
sum_of_diffs = sum(abs(m1 - m2) for m1 in moneys for m2 in moneys)
|
weighted_sum = sum((i + 1) * m for i, m in enumerate(moneys))
|
||||||
gini = sum_of_diffs / (2 * n * total_money)
|
gini = (2 * weighted_sum) / (n * total_money) - (n + 1) / n
|
||||||
|
gini = max(0.0, min(1.0, gini)) # Clamp to [0, 1]
|
||||||
else:
|
else:
|
||||||
gini = 0
|
gini = 0
|
||||||
else:
|
else:
|
||||||
@ -538,6 +561,11 @@ class World:
|
|||||||
"village_storage": self.village_storage.copy(),
|
"village_storage": self.village_storage.copy(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Cache the computed stats
|
||||||
|
self._cached_stats = stats
|
||||||
|
self._stats_cache_turn = self.current_turn
|
||||||
|
return stats
|
||||||
|
|
||||||
def get_state_snapshot(self) -> dict:
|
def get_state_snapshot(self) -> dict:
|
||||||
"""Get a full snapshot of the world state for API."""
|
"""Get a full snapshot of the world state for API."""
|
||||||
return {
|
return {
|
||||||
|
|||||||
13
config.json
13
config.json
@ -1,8 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"performance": {
|
||||||
|
"logging_enabled": false,
|
||||||
|
"detailed_logging": false,
|
||||||
|
"log_flush_interval": 50,
|
||||||
|
"max_turn_logs": 100,
|
||||||
|
"stats_update_interval": 10
|
||||||
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"use_goap": true,
|
"use_goap": false,
|
||||||
"goap_max_iterations": 50,
|
"goap_max_iterations": 30,
|
||||||
"goap_max_plan_depth": 3,
|
"goap_max_plan_depth": 2,
|
||||||
"reactive_fallback": true
|
"reactive_fallback": true
|
||||||
},
|
},
|
||||||
"agent_stats": {
|
"agent_stats": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user