villsim/tools/excel_to_config.py

245 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""
Convert Excel balance sheet back to SimulationConfig.
Reads parameter values from the Excel file and generates a config.json.
Usage:
python excel_to_config.py [--input balance_sheet.xlsx] [--output config.json]
"""
import argparse
import json
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from openpyxl import load_workbook
from backend.config import (
SimulationConfig,
AgentStatsConfig,
ResourceConfig,
ActionConfig,
WorldConfig,
MarketConfig,
EconomyConfig,
)
# Parameter to config section mapping
PARAM_MAPPING = {
# AgentStatsConfig
'max_energy': ('agent_stats', int),
'max_hunger': ('agent_stats', int),
'max_thirst': ('agent_stats', int),
'max_heat': ('agent_stats', int),
'start_energy': ('agent_stats', int),
'start_hunger': ('agent_stats', int),
'start_thirst': ('agent_stats', int),
'start_heat': ('agent_stats', int),
'energy_decay': ('agent_stats', int),
'hunger_decay': ('agent_stats', int),
'thirst_decay': ('agent_stats', int),
'heat_decay': ('agent_stats', int),
'critical_threshold': ('agent_stats', float),
'low_energy_threshold': ('agent_stats', int),
# ResourceConfig
'meat_decay': ('resources', int),
'berries_decay': ('resources', int),
'clothes_decay': ('resources', int),
'meat_hunger': ('resources', int),
'meat_energy': ('resources', int),
'berries_hunger': ('resources', int),
'berries_thirst': ('resources', int),
'water_thirst': ('resources', int),
'fire_heat': ('resources', int),
# ActionConfig
'sleep_energy': ('actions', int),
'rest_energy': ('actions', int),
'hunt_energy': ('actions', int),
'gather_energy': ('actions', int),
'chop_wood_energy': ('actions', int),
'get_water_energy': ('actions', int),
'weave_energy': ('actions', int),
'build_fire_energy': ('actions', int),
'trade_energy': ('actions', int),
'hunt_success': ('actions', float),
'chop_wood_success': ('actions', float),
'hunt_meat_min': ('actions', int),
'hunt_meat_max': ('actions', int),
'hunt_hide_min': ('actions', int),
'hunt_hide_max': ('actions', int),
'gather_min': ('actions', int),
'gather_max': ('actions', int),
'chop_wood_min': ('actions', int),
'chop_wood_max': ('actions', int),
# WorldConfig
'width': ('world', int),
'height': ('world', int),
'initial_agents': ('world', int),
'day_steps': ('world', int),
'night_steps': ('world', int),
'inventory_slots': ('world', int),
'starting_money': ('world', int),
# MarketConfig
'turns_before_discount': ('market', int),
'discount_rate': ('market', float),
'base_price_multiplier': ('market', float),
# EconomyConfig
'energy_to_money_ratio': ('economy', float),
'wealth_desire': ('economy', float),
'buy_efficiency_threshold': ('economy', float),
'min_wealth_target': ('economy', int),
'max_price_markup': ('economy', float),
'min_price_discount': ('economy', float),
# SimulationConfig (root level)
'auto_step_interval': ('root', float),
}
def parse_excel(input_path: str) -> dict:
"""Parse the Excel balance sheet and extract parameter values."""
wb = load_workbook(input_path, data_only=True) # data_only=True to get calculated values
ws = wb.active
# Organize parameters by section
config_data = {
'agent_stats': {},
'resources': {},
'actions': {},
'world': {},
'market': {},
'economy': {},
'auto_step_interval': 1.0,
}
found_params = []
skipped_rows = []
# Iterate through rows looking for parameter names in column A
for row in range(1, ws.max_row + 1):
param_name = ws.cell(row=row, column=1).value
param_value = ws.cell(row=row, column=2).value
# Skip empty rows or non-parameter rows
if not param_name or not isinstance(param_name, str):
continue
# Strip whitespace
param_name = param_name.strip()
# Check if this is a known parameter
if param_name in PARAM_MAPPING:
section, type_converter = PARAM_MAPPING[param_name]
# Skip if value is None or empty
if param_value is None:
skipped_rows.append(f" ⚠️ {param_name}: no value found (row {row})")
continue
try:
# Convert to appropriate type
converted_value = type_converter(param_value)
if section == 'root':
config_data['auto_step_interval'] = converted_value
else:
config_data[section][param_name] = converted_value
found_params.append(f"{param_name} = {converted_value}")
except (ValueError, TypeError) as e:
skipped_rows.append(f" ⚠️ {param_name}: conversion error ({e})")
return config_data, found_params, skipped_rows
def create_config_from_data(config_data: dict) -> SimulationConfig:
"""Create a SimulationConfig from parsed data."""
return SimulationConfig(
agent_stats=AgentStatsConfig(**config_data['agent_stats']),
resources=ResourceConfig(**config_data['resources']),
actions=ActionConfig(**config_data['actions']),
world=WorldConfig(**config_data['world']),
market=MarketConfig(**config_data['market']),
economy=EconomyConfig(**config_data['economy']),
auto_step_interval=config_data['auto_step_interval'],
)
def save_config_json(config: SimulationConfig, output_path: str):
"""Save config to JSON file."""
with open(output_path, 'w') as f:
json.dump(config.to_dict(), f, indent=2)
def main():
parser = argparse.ArgumentParser(description="Convert Excel balance sheet to config")
parser.add_argument("--input", "-i", default="balance_sheet.xlsx",
help="Input Excel file path")
parser.add_argument("--output", "-o", default="config.json",
help="Output JSON config file path")
parser.add_argument("--verbose", "-v", action="store_true",
help="Show detailed parsing output")
args = parser.parse_args()
# Check if input file exists
if not Path(args.input).exists():
print(f"❌ Error: Input file not found: {args.input}")
sys.exit(1)
print(f"📂 Reading Excel file: {args.input}")
# Parse Excel
config_data, found_params, skipped_rows = parse_excel(args.input)
if args.verbose:
print("\n📊 Found parameters:")
for param in found_params:
print(param)
if skipped_rows:
print("\n⚠️ Skipped rows:")
for skip in skipped_rows:
print(skip)
# Create config
try:
config = create_config_from_data(config_data)
except TypeError as e:
print(f"❌ Error creating config: {e}")
print(" Some required parameters may be missing from the Excel file.")
sys.exit(1)
# Save to JSON
save_config_json(config, args.output)
# Summary
sections = ['agent_stats', 'resources', 'actions', 'world', 'market', 'economy']
total_params = sum(len(config_data[k]) for k in sections)
total_params += 1 # auto_step_interval
print(f"\n✅ Config saved to: {args.output}")
print(f" 📊 {total_params} parameters imported")
print(f" 🔧 {len(skipped_rows)} rows skipped")
# Print summary of values by section
print("\n📋 Configuration summary:")
print(f" 👤 Agent Stats: {len(config_data['agent_stats'])} params")
print(f" 🍖 Resources: {len(config_data['resources'])} params")
print(f" ⚡ Actions: {len(config_data['actions'])} params")
print(f" 🌍 World: {len(config_data['world'])} params")
print(f" 💰 Market: {len(config_data['market'])} params")
print(f" 📈 Economy: {len(config_data['economy'])} params")
if __name__ == "__main__":
main()