739 lines
26 KiB
Python
739 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Comprehensive Balance Optimizer for Village Simulation
|
|
|
|
This script runs simulations and optimizes config values for:
|
|
- High survival rate (target: >50% at end)
|
|
- Religion diversity (no single religion >60%)
|
|
- Faction survival (all factions have living members)
|
|
- Active market (trades happening, money circulating)
|
|
- Oil industry activity (drilling and refining)
|
|
|
|
Usage:
|
|
python tools/optimize_balance.py [--iterations 20] [--steps 1000]
|
|
python tools/optimize_balance.py --quick-test
|
|
python tools/optimize_balance.py --analyze-current
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import random
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
# Add parent directory for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from backend.config import get_config, reload_config
|
|
from backend.core.engine import GameEngine
|
|
from backend.core.logger import reset_simulation_logger
|
|
from backend.domain.action import reset_action_config_cache
|
|
from backend.domain.resources import reset_resource_cache
|
|
|
|
|
|
@dataclass
|
|
class BalanceMetrics:
|
|
"""Comprehensive metrics for simulation balance."""
|
|
total_turns: int = 0
|
|
initial_population: int = 0
|
|
final_population: int = 0
|
|
|
|
# Survival tracking
|
|
deaths_by_cause: dict = field(default_factory=lambda: defaultdict(int))
|
|
population_over_time: list = field(default_factory=list)
|
|
|
|
# Religion tracking
|
|
religion_counts: dict = field(default_factory=lambda: defaultdict(int))
|
|
conversions: int = 0
|
|
|
|
# Faction tracking
|
|
faction_counts: dict = field(default_factory=lambda: defaultdict(int))
|
|
wars_declared: int = 0
|
|
peace_treaties: int = 0
|
|
|
|
# Market tracking
|
|
total_listings: int = 0
|
|
total_trades: int = 0
|
|
trade_volume: int = 0
|
|
trade_value: int = 0
|
|
trades_by_resource: dict = field(default_factory=lambda: defaultdict(int))
|
|
|
|
# Action diversity
|
|
action_counts: dict = field(default_factory=lambda: defaultdict(int))
|
|
|
|
# Oil industry
|
|
oil_drilled: int = 0
|
|
fuel_refined: int = 0
|
|
|
|
# Economy
|
|
money_circulation: list = field(default_factory=list)
|
|
avg_wealth: list = field(default_factory=list)
|
|
wealth_gini: list = field(default_factory=list)
|
|
|
|
@property
|
|
def survival_rate(self) -> float:
|
|
"""Final survival rate."""
|
|
if self.initial_population == 0:
|
|
return 0
|
|
return self.final_population / self.initial_population
|
|
|
|
@property
|
|
def religion_diversity(self) -> float:
|
|
"""Religion diversity score (0-1, higher = more diverse)."""
|
|
if not self.religion_counts:
|
|
return 0
|
|
total = sum(self.religion_counts.values())
|
|
if total == 0:
|
|
return 0
|
|
max_count = max(self.religion_counts.values())
|
|
# Perfect diversity = 20% each (5 religions), worst = 100% one religion
|
|
return 1.0 - (max_count / total)
|
|
|
|
@property
|
|
def dominant_religion_pct(self) -> float:
|
|
"""Percentage held by dominant religion."""
|
|
if not self.religion_counts:
|
|
return 0
|
|
total = sum(self.religion_counts.values())
|
|
if total == 0:
|
|
return 0
|
|
return max(self.religion_counts.values()) / total
|
|
|
|
@property
|
|
def factions_alive(self) -> int:
|
|
"""Number of factions with living members."""
|
|
return len([f for f, c in self.faction_counts.items() if c > 0])
|
|
|
|
@property
|
|
def faction_diversity(self) -> float:
|
|
"""Faction diversity (0-1)."""
|
|
if not self.faction_counts:
|
|
return 0
|
|
alive = self.factions_alive
|
|
# We have 5 non-neutral factions
|
|
return alive / 5.0
|
|
|
|
@property
|
|
def market_activity(self) -> float:
|
|
"""Market activity score."""
|
|
if self.total_turns == 0:
|
|
return 0
|
|
trades_per_turn = self.total_trades / self.total_turns
|
|
# Target: 0.3 trades per turn per 10 agents
|
|
return min(1.0, trades_per_turn / 0.3)
|
|
|
|
@property
|
|
def trade_diversity(self) -> float:
|
|
"""How many different resources are being traded."""
|
|
resources_traded = len([r for r, c in self.trades_by_resource.items() if c > 0])
|
|
return resources_traded / 6.0 # 6 tradeable resources
|
|
|
|
@property
|
|
def oil_industry_activity(self) -> float:
|
|
"""Oil industry health score."""
|
|
total_oil_ops = self.oil_drilled + self.fuel_refined
|
|
# Target: 5% of actions should be oil-related
|
|
total_actions = sum(self.action_counts.values())
|
|
if total_actions == 0:
|
|
return 0
|
|
return min(1.0, (total_oil_ops / total_actions) / 0.05)
|
|
|
|
@property
|
|
def economy_health(self) -> float:
|
|
"""Overall economy health."""
|
|
if not self.avg_wealth:
|
|
return 0
|
|
final_wealth = self.avg_wealth[-1]
|
|
# Target: average wealth should stay above 50
|
|
return min(1.0, final_wealth / 50)
|
|
|
|
def score(self) -> float:
|
|
"""Calculate overall balance score (0-100)."""
|
|
score = 0
|
|
|
|
# Survival rate (0-30 points) - CRITICAL
|
|
# Target: at least 30% survival
|
|
survival_score = min(30, self.survival_rate * 100)
|
|
score += survival_score
|
|
|
|
# Religion diversity (0-15 points)
|
|
# Target: no single religion > 50%
|
|
religion_score = self.religion_diversity * 15
|
|
score += religion_score
|
|
|
|
# Faction survival (0-15 points)
|
|
# Target: at least 4 of 5 factions alive
|
|
faction_score = self.faction_diversity * 15
|
|
score += faction_score
|
|
|
|
# Market activity (0-15 points)
|
|
market_score = self.market_activity * 15
|
|
score += market_score
|
|
|
|
# Trade diversity (0-10 points)
|
|
trade_div_score = self.trade_diversity * 10
|
|
score += trade_div_score
|
|
|
|
# Oil industry (0-10 points)
|
|
oil_score = self.oil_industry_activity * 10
|
|
score += oil_score
|
|
|
|
# Economy health (0-5 points)
|
|
econ_score = self.economy_health * 5
|
|
score += econ_score
|
|
|
|
return score
|
|
|
|
|
|
def run_simulation(config_overrides: dict, num_steps: int = 1000, num_agents: int = 100) -> BalanceMetrics:
|
|
"""Run a simulation with custom config and return metrics."""
|
|
# Apply config overrides
|
|
config_path = Path("config.json")
|
|
with open(config_path) as f:
|
|
config = json.load(f)
|
|
|
|
# Deep merge overrides
|
|
for section, values in config_overrides.items():
|
|
if section in config:
|
|
config[section].update(values)
|
|
else:
|
|
config[section] = values
|
|
|
|
# Save temp config
|
|
temp_config = Path("config_temp.json")
|
|
with open(temp_config, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
# Reload config
|
|
reload_config(str(temp_config))
|
|
reset_action_config_cache()
|
|
reset_resource_cache()
|
|
|
|
# Initialize engine - need to set initial_agents BEFORE reset() calls initialize()
|
|
GameEngine._instance = None # Reset singleton
|
|
engine = GameEngine()
|
|
# Note: reset() already calls world.initialize(), so we must set initial_agents first
|
|
# Get the config and modify it before reset
|
|
sim_config = get_config()
|
|
engine.world.config.initial_agents = num_agents
|
|
# Reset creates a new world and initializes it
|
|
from backend.core.world import World, WorldConfig
|
|
world_config = WorldConfig(initial_agents=num_agents)
|
|
engine.reset(config=world_config)
|
|
|
|
# Suppress logging
|
|
import logging
|
|
logging.getLogger("simulation").setLevel(logging.ERROR)
|
|
|
|
metrics = BalanceMetrics()
|
|
metrics.initial_population = num_agents
|
|
|
|
# Run simulation
|
|
for step in range(num_steps):
|
|
if not engine.is_running:
|
|
break
|
|
|
|
turn_log = engine.next_step()
|
|
metrics.total_turns += 1
|
|
|
|
# Track population
|
|
living = len(engine.world.get_living_agents())
|
|
metrics.population_over_time.append(living)
|
|
|
|
# Track money
|
|
agents = engine.world.get_living_agents()
|
|
if agents:
|
|
total_money = sum(a.money for a in agents)
|
|
avg_money = total_money / len(agents)
|
|
metrics.money_circulation.append(total_money)
|
|
metrics.avg_wealth.append(avg_money)
|
|
|
|
# Gini coefficient
|
|
moneys = sorted([a.money for a in agents])
|
|
n = len(moneys)
|
|
if n > 1 and total_money > 0:
|
|
sum_of_diffs = sum(abs(m1 - m2) for m1 in moneys for m2 in moneys)
|
|
gini = sum_of_diffs / (2 * n * total_money)
|
|
else:
|
|
gini = 0
|
|
metrics.wealth_gini.append(gini)
|
|
|
|
# Process actions
|
|
for action_data in turn_log.agent_actions:
|
|
decision = action_data.get("decision", {})
|
|
result = action_data.get("result", {})
|
|
action_type = decision.get("action", "unknown")
|
|
|
|
metrics.action_counts[action_type] += 1
|
|
|
|
# Track specific actions
|
|
if action_type == "drill_oil" and result.get("success"):
|
|
for res in result.get("resources_gained", []):
|
|
if res.get("type") == "oil":
|
|
metrics.oil_drilled += res.get("quantity", 0)
|
|
|
|
elif action_type == "refine" and result.get("success"):
|
|
for res in result.get("resources_gained", []):
|
|
if res.get("type") == "fuel":
|
|
metrics.fuel_refined += res.get("quantity", 0)
|
|
|
|
elif action_type == "preach" and result.get("success"):
|
|
if "converted" in result.get("message", "").lower():
|
|
metrics.conversions += 1
|
|
|
|
elif action_type == "declare_war" and result.get("success"):
|
|
metrics.wars_declared += 1
|
|
|
|
elif action_type == "make_peace" and result.get("success"):
|
|
metrics.peace_treaties += 1
|
|
|
|
elif action_type == "trade" and result.get("success"):
|
|
message = result.get("message", "")
|
|
if "Listed" in message:
|
|
metrics.total_listings += 1
|
|
elif "Bought" in message:
|
|
match = re.search(r"Bought (\d+) (\w+) for (\d+)c", message)
|
|
if match:
|
|
qty = int(match.group(1))
|
|
res = match.group(2)
|
|
value = int(match.group(3))
|
|
metrics.total_trades += 1
|
|
metrics.trade_volume += qty
|
|
metrics.trade_value += value
|
|
metrics.trades_by_resource[res] += 1
|
|
|
|
# Process deaths
|
|
for death_name in turn_log.deaths:
|
|
for agent in engine.world.agents:
|
|
if agent.name == death_name and agent.death_reason:
|
|
metrics.deaths_by_cause[agent.death_reason] += 1
|
|
break
|
|
|
|
# Collect final stats
|
|
living_agents = engine.world.get_living_agents()
|
|
metrics.final_population = len(living_agents)
|
|
|
|
# Count religions and factions
|
|
for agent in living_agents:
|
|
metrics.religion_counts[agent.religion.religion.value] += 1
|
|
metrics.faction_counts[agent.diplomacy.faction.value] += 1
|
|
|
|
# Cleanup
|
|
engine.logger.close()
|
|
temp_config.unlink(missing_ok=True)
|
|
|
|
return metrics
|
|
|
|
|
|
def generate_balanced_config() -> dict:
|
|
"""Generate a config focused on balance."""
|
|
return {
|
|
"agent_stats": {
|
|
"start_hunger": random.randint(85, 95),
|
|
"start_thirst": random.randint(85, 95),
|
|
"hunger_decay": random.randint(1, 2),
|
|
"thirst_decay": random.randint(1, 3),
|
|
"heat_decay": random.randint(1, 2),
|
|
"faith_decay": random.randint(1, 2),
|
|
"critical_threshold": round(random.uniform(0.12, 0.20), 2),
|
|
},
|
|
"resources": {
|
|
"meat_hunger": random.randint(40, 55),
|
|
"berries_hunger": random.randint(12, 18),
|
|
"water_thirst": random.randint(50, 70),
|
|
"fire_heat": random.randint(25, 40),
|
|
},
|
|
"actions": {
|
|
"hunt_success": round(random.uniform(0.75, 0.90), 2),
|
|
"drill_oil_success": round(random.uniform(0.70, 0.85), 2),
|
|
"hunt_meat_min": random.randint(3, 4),
|
|
"hunt_meat_max": random.randint(5, 7),
|
|
"gather_min": random.randint(4, 5),
|
|
"gather_max": random.randint(6, 8),
|
|
# preach_convert_chance is in actions, not religion
|
|
"preach_convert_chance": round(random.uniform(0.03, 0.08), 2),
|
|
},
|
|
"religion": {
|
|
"conversion_resistance": round(random.uniform(0.65, 0.85), 2),
|
|
"zealot_threshold": round(random.uniform(0.80, 0.92), 2),
|
|
"same_religion_bonus": round(random.uniform(0.08, 0.15), 2),
|
|
"different_religion_penalty": round(random.uniform(0.02, 0.06), 2),
|
|
},
|
|
"diplomacy": {
|
|
"starting_relations": random.randint(55, 70),
|
|
"relation_decay": random.randint(0, 1),
|
|
"trade_relation_boost": random.randint(6, 10),
|
|
"war_exhaustion_rate": random.randint(8, 15),
|
|
"war_threshold": random.randint(15, 25),
|
|
},
|
|
"economy": {
|
|
"buy_efficiency_threshold": round(random.uniform(0.80, 0.95), 2),
|
|
"min_wealth_target": random.randint(25, 50),
|
|
"max_price_markup": round(random.uniform(1.4, 1.8), 1),
|
|
},
|
|
}
|
|
|
|
|
|
def mutate_config(config: dict, mutation_rate: float = 0.3) -> dict:
|
|
"""Mutate a configuration."""
|
|
new_config = json.loads(json.dumps(config))
|
|
|
|
for section, values in new_config.items():
|
|
for key, value in values.items():
|
|
if random.random() < mutation_rate:
|
|
if isinstance(value, int):
|
|
delta = max(1, abs(value) // 4)
|
|
new_config[section][key] = max(0, value + random.randint(-delta, delta))
|
|
elif isinstance(value, float):
|
|
delta = abs(value) * 0.15
|
|
new_val = value + random.uniform(-delta, delta)
|
|
new_config[section][key] = round(max(0.01, min(0.99, new_val)), 2)
|
|
|
|
return new_config
|
|
|
|
|
|
def crossover_configs(config1: dict, config2: dict) -> dict:
|
|
"""Crossover two configurations."""
|
|
new_config = {}
|
|
for section in set(config1.keys()) | set(config2.keys()):
|
|
if section in config1 and section in config2:
|
|
new_config[section] = {}
|
|
for key in set(config1[section].keys()) | set(config2[section].keys()):
|
|
if random.random() < 0.5 and key in config1[section]:
|
|
new_config[section][key] = config1[section][key]
|
|
elif key in config2[section]:
|
|
new_config[section][key] = config2[section][key]
|
|
elif section in config1:
|
|
new_config[section] = config1[section].copy()
|
|
else:
|
|
new_config[section] = config2[section].copy()
|
|
return new_config
|
|
|
|
|
|
def print_metrics(metrics: BalanceMetrics, detailed: bool = True):
|
|
"""Print metrics in a readable format."""
|
|
print(f"\n 📊 Balance Score: {metrics.score():.1f}/100")
|
|
print(f" ├─ Survival: {metrics.survival_rate*100:.0f}% ({metrics.final_population}/{metrics.initial_population})")
|
|
print(f" ├─ Religion: {metrics.religion_diversity*100:.0f}% diversity (dominant: {metrics.dominant_religion_pct*100:.0f}%)")
|
|
print(f" ├─ Factions: {metrics.factions_alive}/5 alive ({metrics.faction_diversity*100:.0f}%)")
|
|
print(f" ├─ Market: {metrics.total_trades} trades, {metrics.total_listings} listings")
|
|
print(f" ├─ Trade diversity: {metrics.trade_diversity*100:.0f}%")
|
|
print(f" ├─ Oil industry: {metrics.oil_drilled} oil, {metrics.fuel_refined} fuel")
|
|
print(f" └─ Economy: avg wealth ${metrics.avg_wealth[-1]:.0f}" if metrics.avg_wealth else " └─ Economy: N/A")
|
|
|
|
if detailed:
|
|
print(f"\n 📋 Death causes:")
|
|
for cause, count in sorted(metrics.deaths_by_cause.items(), key=lambda x: -x[1])[:5]:
|
|
print(f" - {cause}: {count}")
|
|
|
|
print(f"\n 🏛️ Religions:")
|
|
for religion, count in sorted(metrics.religion_counts.items(), key=lambda x: -x[1]):
|
|
print(f" - {religion}: {count}")
|
|
|
|
print(f"\n ⚔️ Factions:")
|
|
for faction, count in sorted(metrics.faction_counts.items(), key=lambda x: -x[1]):
|
|
print(f" - {faction}: {count}")
|
|
|
|
|
|
def optimize_balance(iterations: int = 20, steps_per_sim: int = 1000, population_size: int = 8):
|
|
"""Run genetic optimization for balance."""
|
|
print("\n" + "=" * 70)
|
|
print("🧬 BALANCE OPTIMIZER - Finding Optimal Configuration")
|
|
print("=" * 70)
|
|
print(f" Iterations: {iterations}")
|
|
print(f" Steps per simulation: {steps_per_sim}")
|
|
print(f" Population size: {population_size}")
|
|
print(f" Agents per simulation: 100")
|
|
print("=" * 70)
|
|
|
|
# Create initial population
|
|
population = []
|
|
|
|
# Start with a well-balanced baseline
|
|
baseline = {
|
|
"agent_stats": {
|
|
"start_hunger": 92,
|
|
"start_thirst": 92,
|
|
"hunger_decay": 1,
|
|
"thirst_decay": 2,
|
|
"heat_decay": 1,
|
|
"faith_decay": 1,
|
|
"critical_threshold": 0.15,
|
|
},
|
|
"resources": {
|
|
"meat_hunger": 50,
|
|
"berries_hunger": 15,
|
|
"water_thirst": 65,
|
|
"fire_heat": 35,
|
|
},
|
|
"actions": {
|
|
"hunt_success": 0.85,
|
|
"drill_oil_success": 0.80,
|
|
"hunt_meat_min": 4,
|
|
"hunt_meat_max": 6,
|
|
"gather_min": 4,
|
|
"gather_max": 7,
|
|
"preach_convert_chance": 0.05,
|
|
},
|
|
"religion": {
|
|
"conversion_resistance": 0.75,
|
|
"zealot_threshold": 0.88,
|
|
"same_religion_bonus": 0.10,
|
|
"different_religion_penalty": 0.03,
|
|
},
|
|
"diplomacy": {
|
|
"starting_relations": 65,
|
|
"relation_decay": 0,
|
|
"trade_relation_boost": 8,
|
|
"war_exhaustion_rate": 12,
|
|
"war_threshold": 18,
|
|
},
|
|
"economy": {
|
|
"buy_efficiency_threshold": 0.88,
|
|
"min_wealth_target": 35,
|
|
"max_price_markup": 1.5,
|
|
},
|
|
}
|
|
population.append(baseline)
|
|
|
|
# Add survival-focused variant
|
|
survival_focused = json.loads(json.dumps(baseline))
|
|
survival_focused["agent_stats"]["hunger_decay"] = 1
|
|
survival_focused["agent_stats"]["thirst_decay"] = 1
|
|
survival_focused["resources"]["meat_hunger"] = 55
|
|
survival_focused["resources"]["berries_hunger"] = 18
|
|
survival_focused["resources"]["water_thirst"] = 70
|
|
population.append(survival_focused)
|
|
|
|
# Add religion-balanced variant
|
|
religion_balanced = json.loads(json.dumps(baseline))
|
|
religion_balanced["religion"]["conversion_resistance"] = 0.82
|
|
religion_balanced["actions"]["preach_convert_chance"] = 0.03
|
|
religion_balanced["religion"]["zealot_threshold"] = 0.92
|
|
population.append(religion_balanced)
|
|
|
|
# Add diplomacy-stable variant
|
|
diplomacy_stable = json.loads(json.dumps(baseline))
|
|
diplomacy_stable["diplomacy"]["relation_decay"] = 0
|
|
diplomacy_stable["diplomacy"]["starting_relations"] = 70
|
|
diplomacy_stable["diplomacy"]["war_exhaustion_rate"] = 15
|
|
population.append(diplomacy_stable)
|
|
|
|
# Fill rest with random
|
|
while len(population) < population_size:
|
|
population.append(generate_balanced_config())
|
|
|
|
best_config = None
|
|
best_score = 0
|
|
best_metrics = None
|
|
|
|
for gen in range(iterations):
|
|
print(f"\n📍 Generation {gen + 1}/{iterations}")
|
|
print("-" * 50)
|
|
|
|
scored_population = []
|
|
for i, config in enumerate(population):
|
|
sys.stdout.write(f"\r Evaluating config {i + 1}/{len(population)}...")
|
|
sys.stdout.flush()
|
|
|
|
metrics = run_simulation(config, steps_per_sim, num_agents=100)
|
|
score = metrics.score()
|
|
scored_population.append((config, metrics, score))
|
|
|
|
# Sort by score
|
|
scored_population.sort(key=lambda x: x[2], reverse=True)
|
|
|
|
# Print top results
|
|
print(f"\r Top configs this generation:")
|
|
for i, (config, metrics, score) in enumerate(scored_population[:3]):
|
|
print(f"\n #{i + 1}: Score {score:.1f}")
|
|
print_metrics(metrics, detailed=False)
|
|
|
|
# Track best overall
|
|
if scored_population[0][2] > best_score:
|
|
best_config = scored_population[0][0]
|
|
best_score = scored_population[0][2]
|
|
best_metrics = scored_population[0][1]
|
|
print(f"\n ⭐ New best score: {best_score:.1f}")
|
|
|
|
# Create next generation
|
|
new_population = []
|
|
|
|
# Keep top 2 (elitism)
|
|
new_population.append(scored_population[0][0])
|
|
new_population.append(scored_population[1][0])
|
|
|
|
# Crossover and mutate
|
|
while len(new_population) < population_size:
|
|
parent1 = random.choice(scored_population[:4])[0]
|
|
parent2 = random.choice(scored_population[:4])[0]
|
|
child = crossover_configs(parent1, parent2)
|
|
child = mutate_config(child, mutation_rate=0.25)
|
|
new_population.append(child)
|
|
|
|
population = new_population
|
|
|
|
print("\n" + "=" * 70)
|
|
print("🏆 OPTIMIZATION COMPLETE")
|
|
print("=" * 70)
|
|
|
|
print(f"\n Best Score: {best_score:.1f}/100")
|
|
print_metrics(best_metrics, detailed=True)
|
|
|
|
print("\n 📝 Best Configuration:")
|
|
print("-" * 50)
|
|
print(json.dumps(best_config, indent=2))
|
|
|
|
# Save optimized config
|
|
output_path = Path("config_balanced.json")
|
|
|
|
with open("config.json") as f:
|
|
full_config = json.load(f)
|
|
|
|
for section, values in best_config.items():
|
|
if section in full_config:
|
|
full_config[section].update(values)
|
|
else:
|
|
full_config[section] = values
|
|
|
|
with open(output_path, 'w') as f:
|
|
json.dump(full_config, f, indent=2)
|
|
|
|
print(f"\n ✅ Saved optimized config to: {output_path}")
|
|
print(" To apply: cp config_balanced.json config.json")
|
|
|
|
return best_config, best_metrics
|
|
|
|
|
|
def analyze_current_config(steps: int = 500):
|
|
"""Analyze the current configuration."""
|
|
print("\n" + "=" * 70)
|
|
print("📊 ANALYZING CURRENT CONFIGURATION")
|
|
print("=" * 70)
|
|
|
|
metrics = run_simulation({}, steps, num_agents=100)
|
|
print_metrics(metrics, detailed=True)
|
|
|
|
# Provide recommendations
|
|
print("\n" + "=" * 70)
|
|
print("💡 RECOMMENDATIONS")
|
|
print("=" * 70)
|
|
|
|
if metrics.survival_rate < 0.3:
|
|
print("\n ⚠️ LOW SURVIVAL RATE")
|
|
print(" - Reduce hunger_decay and thirst_decay")
|
|
print(" - Increase food resource values (meat_hunger, berries_hunger)")
|
|
print(" - Lower critical_threshold")
|
|
|
|
if metrics.dominant_religion_pct > 0.6:
|
|
print("\n ⚠️ RELIGION DOMINANCE")
|
|
print(" - Increase conversion_resistance (try 0.75+)")
|
|
print(" - Reduce preach_convert_chance (try 0.05)")
|
|
print(" - Increase zealot_threshold (try 0.88+)")
|
|
|
|
if metrics.factions_alive < 4:
|
|
print("\n ⚠️ FACTIONS DYING OUT")
|
|
print(" - Set relation_decay to 0 or 1")
|
|
print(" - Increase starting_relations (try 65+)")
|
|
print(" - Increase war_exhaustion_rate (try 10+)")
|
|
|
|
if metrics.total_trades < metrics.total_turns * 0.1:
|
|
print("\n ⚠️ LOW MARKET ACTIVITY")
|
|
print(" - Increase buy_efficiency_threshold (try 0.9)")
|
|
print(" - Lower min_wealth_target")
|
|
print(" - Reduce max_price_markup")
|
|
|
|
if metrics.oil_drilled + metrics.fuel_refined < 50:
|
|
print("\n ⚠️ LOW OIL INDUSTRY")
|
|
print(" - Increase drill_oil_success (try 0.80)")
|
|
print(" - Check that factions with oil bonus survive")
|
|
|
|
return metrics
|
|
|
|
|
|
def quick_test(steps: int = 500):
|
|
"""Quick test with a balanced preset."""
|
|
print("\n🧪 Quick Test with Balanced Preset")
|
|
print("-" * 50)
|
|
|
|
test_config = {
|
|
"agent_stats": {
|
|
"start_hunger": 92,
|
|
"start_thirst": 92,
|
|
"hunger_decay": 1,
|
|
"thirst_decay": 2,
|
|
"heat_decay": 1,
|
|
"faith_decay": 1,
|
|
"critical_threshold": 0.15,
|
|
},
|
|
"resources": {
|
|
"meat_hunger": 50,
|
|
"berries_hunger": 16,
|
|
"water_thirst": 65,
|
|
"fire_heat": 35,
|
|
},
|
|
"actions": {
|
|
"hunt_success": 0.85,
|
|
"drill_oil_success": 0.80,
|
|
"hunt_meat_min": 4,
|
|
"hunt_meat_max": 6,
|
|
"gather_min": 4,
|
|
"gather_max": 7,
|
|
"preach_convert_chance": 0.04,
|
|
},
|
|
"religion": {
|
|
"conversion_resistance": 0.78,
|
|
"zealot_threshold": 0.90,
|
|
"same_religion_bonus": 0.10,
|
|
"different_religion_penalty": 0.03,
|
|
},
|
|
"diplomacy": {
|
|
"starting_relations": 65,
|
|
"relation_decay": 0,
|
|
"trade_relation_boost": 8,
|
|
"war_exhaustion_rate": 12,
|
|
"war_threshold": 18,
|
|
},
|
|
"economy": {
|
|
"buy_efficiency_threshold": 0.90,
|
|
"min_wealth_target": 30,
|
|
"max_price_markup": 1.5,
|
|
},
|
|
}
|
|
|
|
print("\n Testing config:")
|
|
print(json.dumps(test_config, indent=2))
|
|
|
|
metrics = run_simulation(test_config, steps, num_agents=100)
|
|
print_metrics(metrics, detailed=True)
|
|
|
|
return metrics
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Optimize Village Simulation balance")
|
|
parser.add_argument("--iterations", "-i", type=int, default=15, help="Optimization iterations")
|
|
parser.add_argument("--steps", "-s", type=int, default=800, help="Steps per simulation")
|
|
parser.add_argument("--population", "-p", type=int, default=8, help="Population size for GA")
|
|
parser.add_argument("--quick-test", "-q", action="store_true", help="Quick test balanced preset")
|
|
parser.add_argument("--analyze-current", "-a", action="store_true", help="Analyze current config")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.quick_test:
|
|
quick_test(args.steps)
|
|
elif args.analyze_current:
|
|
analyze_current_config(args.steps)
|
|
else:
|
|
optimize_balance(args.iterations, args.steps, args.population)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|