villsim/tools/config_to_excel.py

604 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Convert SimulationConfig to a colorful Excel balance sheet.
Compatible with Google Sheets.
Usage:
python config_to_excel.py [--output balance_sheet.xlsx] [--config config.json]
"""
import argparse
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 Workbook
from openpyxl.styles import (
Font, Fill, PatternFill, Border, Side, Alignment, NamedStyle
)
from openpyxl.utils import get_column_letter
from openpyxl.formatting.rule import FormulaRule, ColorScaleRule
from backend.config import SimulationConfig, get_config
# ============================================================================
# CLOWN COLORS PALETTE 🤡
# ============================================================================
COLORS = {
# Headers - Bright and bold
'red': 'FF0000',
'orange': 'FF8C00',
'yellow': 'FFD700',
'lime': '32CD32',
'cyan': '00CED1',
'blue': '1E90FF',
'purple': '9400D3',
'magenta': 'FF00FF',
'pink': 'FF69B4',
# Section backgrounds (lighter versions)
'light_red': 'FFB3B3',
'light_orange': 'FFD9B3',
'light_yellow': 'FFFACD',
'light_lime': 'B3FFB3',
'light_cyan': 'B3FFFF',
'light_blue': 'B3D9FF',
'light_purple': 'E6B3FF',
'light_magenta': 'FFB3FF',
'light_pink': 'FFB3D9',
# Special
'white': 'FFFFFF',
'black': '000000',
'gold': 'FFD700',
'silver': 'C0C0C0',
}
# Section color assignments
SECTION_COLORS = {
'agent_stats': ('red', 'light_red'),
'resources': ('orange', 'light_orange'),
'actions': ('lime', 'light_lime'),
'world': ('blue', 'light_blue'),
'market': ('purple', 'light_purple'),
'economy': ('cyan', 'light_cyan'),
'summary': ('magenta', 'light_magenta'),
}
def create_header_style(color_name):
"""Create a header cell style with clown colors."""
return {
'fill': PatternFill(start_color=COLORS[color_name],
end_color=COLORS[color_name],
fill_type='solid'),
'font': Font(bold=True, color=COLORS['white'], size=12),
'alignment': Alignment(horizontal='center', vertical='center'),
'border': Border(
left=Side(style='medium', color=COLORS['black']),
right=Side(style='medium', color=COLORS['black']),
top=Side(style='medium', color=COLORS['black']),
bottom=Side(style='medium', color=COLORS['black'])
)
}
def create_data_style(color_name):
"""Create a data cell style with light clown colors."""
return {
'fill': PatternFill(start_color=COLORS[color_name],
end_color=COLORS[color_name],
fill_type='solid'),
'font': Font(size=11),
'alignment': Alignment(horizontal='left', vertical='center'),
'border': Border(
left=Side(style='thin', color=COLORS['black']),
right=Side(style='thin', color=COLORS['black']),
top=Side(style='thin', color=COLORS['black']),
bottom=Side(style='thin', color=COLORS['black'])
)
}
def apply_style(cell, style_dict):
"""Apply a style dictionary to a cell."""
for attr, value in style_dict.items():
setattr(cell, attr, value)
def add_section_header(ws, row, title, header_color, col_span=4):
"""Add a section header spanning multiple columns."""
cell = ws.cell(row=row, column=1, value=title)
apply_style(cell, create_header_style(header_color))
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=col_span)
return row + 1
def add_param_row(ws, row, param_name, value, description, data_color,
value_col=2, desc_col=3, formula_col=4):
"""Add a parameter row with name, value, and description."""
# Parameter name
name_cell = ws.cell(row=row, column=1, value=param_name)
apply_style(name_cell, create_data_style(data_color))
name_cell.font = Font(bold=True, size=11)
# Value (this is what the game designer edits)
value_cell = ws.cell(row=row, column=value_col, value=value)
apply_style(value_cell, create_data_style(data_color))
value_cell.alignment = Alignment(horizontal='center', vertical='center')
value_cell.font = Font(bold=True, size=12, color=COLORS['blue'])
# Description
desc_cell = ws.cell(row=row, column=desc_col, value=description)
apply_style(desc_cell, create_data_style(data_color))
desc_cell.font = Font(italic=True, size=10, color=COLORS['black'])
return row + 1
def add_formula_row(ws, row, label, formula, data_color, description=""):
"""Add a row with a formula for balance calculations."""
# Label
label_cell = ws.cell(row=row, column=1, value=label)
apply_style(label_cell, create_data_style(data_color))
label_cell.font = Font(bold=True, size=11, color=COLORS['purple'])
# Formula
formula_cell = ws.cell(row=row, column=2, value=formula)
apply_style(formula_cell, create_data_style(data_color))
formula_cell.font = Font(bold=True, size=11, color=COLORS['red'])
formula_cell.alignment = Alignment(horizontal='center')
# Description
desc_cell = ws.cell(row=row, column=3, value=description)
apply_style(desc_cell, create_data_style(data_color))
desc_cell.font = Font(italic=True, size=10)
return row + 1
def create_balance_sheet(config: SimulationConfig, output_path: str):
"""Create a colorful Excel balance sheet from config."""
wb = Workbook()
ws = wb.active
ws.title = "Balance Sheet"
# Set column widths
ws.column_dimensions['A'].width = 30
ws.column_dimensions['B'].width = 15
ws.column_dimensions['C'].width = 50
ws.column_dimensions['D'].width = 25
row = 1
# Title
title_cell = ws.cell(row=row, column=1, value="🎮 VILLAGE SIMULATION BALANCE SHEET 🎪")
title_cell.font = Font(bold=True, size=18, color=COLORS['magenta'])
title_cell.alignment = Alignment(horizontal='center')
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=4)
row += 2
# Column headers
headers = ['Parameter', 'Value', 'Description', 'Calculated']
for col, header in enumerate(headers, 1):
cell = ws.cell(row=row, column=col, value=header)
apply_style(cell, create_header_style('cyan'))
row += 1
# Track row positions for formulas (Google Sheets compatible)
param_rows = {}
# ========================================================================
# AGENT STATS SECTION
# ========================================================================
row = add_section_header(ws, row, "👤 AGENT STATS", 'red')
header_color, data_color = SECTION_COLORS['agent_stats']
# Max values
param_rows['max_energy'] = row
row = add_param_row(ws, row, "max_energy", config.agent_stats.max_energy,
"Maximum energy an agent can have", data_color)
param_rows['max_hunger'] = row
row = add_param_row(ws, row, "max_hunger", config.agent_stats.max_hunger,
"Maximum hunger (100 = full, 0 = starving)", data_color)
param_rows['max_thirst'] = row
row = add_param_row(ws, row, "max_thirst", config.agent_stats.max_thirst,
"Maximum thirst (100 = hydrated, 0 = dehydrated)", data_color)
param_rows['max_heat'] = row
row = add_param_row(ws, row, "max_heat", config.agent_stats.max_heat,
"Maximum heat (100 = warm, 0 = freezing)", data_color)
# Starting values
param_rows['start_energy'] = row
row = add_param_row(ws, row, "start_energy", config.agent_stats.start_energy,
"Starting energy for new agents", data_color)
param_rows['start_hunger'] = row
row = add_param_row(ws, row, "start_hunger", config.agent_stats.start_hunger,
"Starting hunger for new agents", data_color)
param_rows['start_thirst'] = row
row = add_param_row(ws, row, "start_thirst", config.agent_stats.start_thirst,
"Starting thirst for new agents", data_color)
param_rows['start_heat'] = row
row = add_param_row(ws, row, "start_heat", config.agent_stats.start_heat,
"Starting heat for new agents", data_color)
# Decay rates
param_rows['energy_decay'] = row
row = add_param_row(ws, row, "energy_decay", config.agent_stats.energy_decay,
"Energy lost per turn", data_color)
param_rows['hunger_decay'] = row
row = add_param_row(ws, row, "hunger_decay", config.agent_stats.hunger_decay,
"Hunger lost per turn", data_color)
param_rows['thirst_decay'] = row
row = add_param_row(ws, row, "thirst_decay", config.agent_stats.thirst_decay,
"Thirst lost per turn", data_color)
param_rows['heat_decay'] = row
row = add_param_row(ws, row, "heat_decay", config.agent_stats.heat_decay,
"Heat lost per turn", data_color)
# Thresholds
param_rows['critical_threshold'] = row
row = add_param_row(ws, row, "critical_threshold", config.agent_stats.critical_threshold,
"Threshold (0-1) for survival mode trigger", data_color)
param_rows['low_energy_threshold'] = row
row = add_param_row(ws, row, "low_energy_threshold", config.agent_stats.low_energy_threshold,
"Minimum energy to perform work", data_color)
row += 1
# ========================================================================
# RESOURCES SECTION
# ========================================================================
row = add_section_header(ws, row, "🍖 RESOURCES", 'orange')
header_color, data_color = SECTION_COLORS['resources']
# Decay rates
param_rows['meat_decay'] = row
row = add_param_row(ws, row, "meat_decay", config.resources.meat_decay,
"Turns until meat spoils (0 = infinite)", data_color)
param_rows['berries_decay'] = row
row = add_param_row(ws, row, "berries_decay", config.resources.berries_decay,
"Turns until berries spoil", data_color)
param_rows['clothes_decay'] = row
row = add_param_row(ws, row, "clothes_decay", config.resources.clothes_decay,
"Turns until clothes wear out", data_color)
# Resource effects
param_rows['meat_hunger'] = row
row = add_param_row(ws, row, "meat_hunger", config.resources.meat_hunger,
"Hunger restored by eating meat", data_color)
param_rows['meat_energy'] = row
row = add_param_row(ws, row, "meat_energy", config.resources.meat_energy,
"Energy restored by eating meat", data_color)
param_rows['berries_hunger'] = row
row = add_param_row(ws, row, "berries_hunger", config.resources.berries_hunger,
"Hunger restored by eating berries", data_color)
param_rows['berries_thirst'] = row
row = add_param_row(ws, row, "berries_thirst", config.resources.berries_thirst,
"Thirst restored by eating berries", data_color)
param_rows['water_thirst'] = row
row = add_param_row(ws, row, "water_thirst", config.resources.water_thirst,
"Thirst restored by drinking water", data_color)
param_rows['fire_heat'] = row
row = add_param_row(ws, row, "fire_heat", config.resources.fire_heat,
"Heat restored by fire", data_color)
row += 1
# ========================================================================
# ACTIONS SECTION
# ========================================================================
row = add_section_header(ws, row, "⚡ ACTIONS", 'lime')
header_color, data_color = SECTION_COLORS['actions']
# Energy costs
param_rows['sleep_energy'] = row
row = add_param_row(ws, row, "sleep_energy", config.actions.sleep_energy,
"Energy restored by sleeping (+positive)", data_color)
param_rows['rest_energy'] = row
row = add_param_row(ws, row, "rest_energy", config.actions.rest_energy,
"Energy restored by resting (+positive)", data_color)
param_rows['hunt_energy'] = row
row = add_param_row(ws, row, "hunt_energy", config.actions.hunt_energy,
"Energy cost for hunting (-negative)", data_color)
param_rows['gather_energy'] = row
row = add_param_row(ws, row, "gather_energy", config.actions.gather_energy,
"Energy cost for gathering (-negative)", data_color)
param_rows['chop_wood_energy'] = row
row = add_param_row(ws, row, "chop_wood_energy", config.actions.chop_wood_energy,
"Energy cost for chopping wood (-negative)", data_color)
param_rows['get_water_energy'] = row
row = add_param_row(ws, row, "get_water_energy", config.actions.get_water_energy,
"Energy cost for getting water (-negative)", data_color)
param_rows['weave_energy'] = row
row = add_param_row(ws, row, "weave_energy", config.actions.weave_energy,
"Energy cost for weaving (-negative)", data_color)
param_rows['build_fire_energy'] = row
row = add_param_row(ws, row, "build_fire_energy", config.actions.build_fire_energy,
"Energy cost for building fire (-negative)", data_color)
param_rows['trade_energy'] = row
row = add_param_row(ws, row, "trade_energy", config.actions.trade_energy,
"Energy cost for trading (-negative)", data_color)
# Success chances
param_rows['hunt_success'] = row
row = add_param_row(ws, row, "hunt_success", config.actions.hunt_success,
"Success chance for hunting (0.0-1.0)", data_color)
param_rows['chop_wood_success'] = row
row = add_param_row(ws, row, "chop_wood_success", config.actions.chop_wood_success,
"Success chance for chopping wood (0.0-1.0)", data_color)
# Output quantities
param_rows['hunt_meat_min'] = row
row = add_param_row(ws, row, "hunt_meat_min", config.actions.hunt_meat_min,
"Minimum meat from successful hunt", data_color)
param_rows['hunt_meat_max'] = row
row = add_param_row(ws, row, "hunt_meat_max", config.actions.hunt_meat_max,
"Maximum meat from successful hunt", data_color)
param_rows['hunt_hide_min'] = row
row = add_param_row(ws, row, "hunt_hide_min", config.actions.hunt_hide_min,
"Minimum hides from successful hunt", data_color)
param_rows['hunt_hide_max'] = row
row = add_param_row(ws, row, "hunt_hide_max", config.actions.hunt_hide_max,
"Maximum hides from successful hunt", data_color)
param_rows['gather_min'] = row
row = add_param_row(ws, row, "gather_min", config.actions.gather_min,
"Minimum berries from gathering", data_color)
param_rows['gather_max'] = row
row = add_param_row(ws, row, "gather_max", config.actions.gather_max,
"Maximum berries from gathering", data_color)
param_rows['chop_wood_min'] = row
row = add_param_row(ws, row, "chop_wood_min", config.actions.chop_wood_min,
"Minimum wood from chopping", data_color)
param_rows['chop_wood_max'] = row
row = add_param_row(ws, row, "chop_wood_max", config.actions.chop_wood_max,
"Maximum wood from chopping", data_color)
row += 1
# ========================================================================
# WORLD SECTION
# ========================================================================
row = add_section_header(ws, row, "🌍 WORLD", 'blue')
header_color, data_color = SECTION_COLORS['world']
param_rows['width'] = row
row = add_param_row(ws, row, "width", config.world.width,
"World width in tiles", data_color)
param_rows['height'] = row
row = add_param_row(ws, row, "height", config.world.height,
"World height in tiles", data_color)
param_rows['initial_agents'] = row
row = add_param_row(ws, row, "initial_agents", config.world.initial_agents,
"Number of agents at start", data_color)
param_rows['day_steps'] = row
row = add_param_row(ws, row, "day_steps", config.world.day_steps,
"Turns in a day cycle", data_color)
param_rows['night_steps'] = row
row = add_param_row(ws, row, "night_steps", config.world.night_steps,
"Turns in a night cycle", data_color)
param_rows['inventory_slots'] = row
row = add_param_row(ws, row, "inventory_slots", config.world.inventory_slots,
"Inventory capacity per agent", data_color)
param_rows['starting_money'] = row
row = add_param_row(ws, row, "starting_money", config.world.starting_money,
"Starting money per agent", data_color)
row += 1
# ========================================================================
# MARKET SECTION
# ========================================================================
row = add_section_header(ws, row, "💰 MARKET", 'purple')
header_color, data_color = SECTION_COLORS['market']
param_rows['turns_before_discount'] = row
row = add_param_row(ws, row, "turns_before_discount", config.market.turns_before_discount,
"Turns until discount applies", data_color)
param_rows['discount_rate'] = row
row = add_param_row(ws, row, "discount_rate", config.market.discount_rate,
"Discount rate (0.0-1.0)", data_color)
param_rows['base_price_multiplier'] = row
row = add_param_row(ws, row, "base_price_multiplier", config.market.base_price_multiplier,
"Markup over production cost", data_color)
row += 1
# ========================================================================
# ECONOMY SECTION (Agent Trading Behavior)
# ========================================================================
row = add_section_header(ws, row, "📈 ECONOMY (Trading Behavior)", 'cyan')
header_color, data_color = SECTION_COLORS['economy']
param_rows['energy_to_money_ratio'] = row
row = add_param_row(ws, row, "energy_to_money_ratio", config.economy.energy_to_money_ratio,
"How much agents value money vs energy (1.5 = 1 energy ≈ 1.5 coins)", data_color)
param_rows['wealth_desire'] = row
row = add_param_row(ws, row, "wealth_desire", config.economy.wealth_desire,
"How strongly agents want wealth (0-1)", data_color)
param_rows['buy_efficiency_threshold'] = row
row = add_param_row(ws, row, "buy_efficiency_threshold", config.economy.buy_efficiency_threshold,
"Buy if price < (threshold × fair value)", data_color)
param_rows['min_wealth_target'] = row
row = add_param_row(ws, row, "min_wealth_target", config.economy.min_wealth_target,
"Minimum money agents want to keep", data_color)
param_rows['max_price_markup'] = row
row = add_param_row(ws, row, "max_price_markup", config.economy.max_price_markup,
"Maximum price = base × this multiplier", data_color)
param_rows['min_price_discount'] = row
row = add_param_row(ws, row, "min_price_discount", config.economy.min_price_discount,
"Minimum price = base × this multiplier", data_color)
row += 1
# ========================================================================
# SIMULATION CONTROL
# ========================================================================
row = add_section_header(ws, row, "⏱️ SIMULATION", 'pink')
param_rows['auto_step_interval'] = row
row = add_param_row(ws, row, "auto_step_interval", config.auto_step_interval,
"Seconds between auto steps", 'light_pink')
row += 2
# ========================================================================
# CALCULATED BALANCE METRICS (with formulas)
# ========================================================================
row = add_section_header(ws, row, "📊 BALANCE CALCULATIONS (Auto-computed)", 'magenta')
data_color = 'light_magenta'
# Turns to starve from full
energy_row = param_rows['start_energy']
decay_row = param_rows['energy_decay']
formula = f"=B{energy_row}/B{decay_row}"
row = add_formula_row(ws, row, "Turns to exhaust energy", formula, data_color,
"From start_energy at energy_decay rate")
# Turns to dehydrate from full
thirst_row = param_rows['start_thirst']
thirst_decay_row = param_rows['thirst_decay']
formula = f"=B{thirst_row}/B{thirst_decay_row}"
row = add_formula_row(ws, row, "Turns to dehydrate", formula, data_color,
"From start_thirst at thirst_decay rate")
# Turns to starve from full
hunger_row = param_rows['start_hunger']
hunger_decay_row = param_rows['hunger_decay']
formula = f"=B{hunger_row}/B{hunger_decay_row}"
row = add_formula_row(ws, row, "Turns to starve", formula, data_color,
"From start_hunger at hunger_decay rate")
# Hunt efficiency (expected meat per energy spent)
hunt_success_row = param_rows['hunt_success']
hunt_energy_row = param_rows['hunt_energy']
meat_min_row = param_rows['hunt_meat_min']
meat_max_row = param_rows['hunt_meat_max']
formula = f"=(B{hunt_success_row}*(B{meat_min_row}+B{meat_max_row})/2)/ABS(B{hunt_energy_row})"
row = add_formula_row(ws, row, "Hunt efficiency (meat/energy)", formula, data_color,
"Expected meat per energy spent")
# Gather efficiency (berries per energy)
gather_energy_row = param_rows['gather_energy']
gather_min_row = param_rows['gather_min']
gather_max_row = param_rows['gather_max']
formula = f"=((B{gather_min_row}+B{gather_max_row})/2)/ABS(B{gather_energy_row})"
row = add_formula_row(ws, row, "Gather efficiency (berries/energy)", formula, data_color,
"Expected berries per energy spent")
# Meat value (hunger restored per decay turn)
meat_hunger_row = param_rows['meat_hunger']
meat_decay_row = param_rows['meat_decay']
formula = f"=B{meat_hunger_row}*B{meat_decay_row}"
row = add_formula_row(ws, row, "Meat total value (hunger × lifetime)", formula, data_color,
"Total hunger value before spoilage")
# Water efficiency (thirst restored per energy)
water_thirst_row = param_rows['water_thirst']
get_water_row = param_rows['get_water_energy']
formula = f"=B{water_thirst_row}/ABS(B{get_water_row})"
row = add_formula_row(ws, row, "Water efficiency (thirst/energy)", formula, data_color,
"Thirst restored per energy spent")
# Sleep ROI (energy restored per turn sleeping)
sleep_row = param_rows['sleep_energy']
decay_row = param_rows['energy_decay']
formula = f"=B{sleep_row}/B{decay_row}"
row = add_formula_row(ws, row, "Sleep ROI (turns of activity)", formula, data_color,
"How many turns of activity per sleep")
# World total tiles
width_row = param_rows['width']
height_row = param_rows['height']
formula = f"=B{width_row}*B{height_row}"
row = add_formula_row(ws, row, "Total world tiles", formula, data_color,
"Width × Height")
# Tiles per agent
agents_row = param_rows['initial_agents']
formula = f"=(B{width_row}*B{height_row})/B{agents_row}"
row = add_formula_row(ws, row, "Tiles per agent", formula, data_color,
"Space available per agent")
# Full day cycle
day_row = param_rows['day_steps']
night_row = param_rows['night_steps']
formula = f"=B{day_row}+B{night_row}"
row = add_formula_row(ws, row, "Full day/night cycle", formula, data_color,
"Total turns in one cycle")
# Critical health threshold (absolute value)
max_energy_row = param_rows['max_energy']
crit_row = param_rows['critical_threshold']
formula = f"=B{max_energy_row}*B{crit_row}"
row = add_formula_row(ws, row, "Critical energy level", formula, data_color,
"Absolute energy when survival mode triggers")
# Expected discount savings
discount_row = param_rows['discount_rate']
formula = f"=B{discount_row}*100&\"%\""
row = add_formula_row(ws, row, "Discount percentage", formula, data_color,
"Market discount as percentage")
row += 2
# ========================================================================
# NOTES SECTION
# ========================================================================
row = add_section_header(ws, row, "📝 NOTES FOR GAME DESIGNER", 'gold')
notes = [
"🎯 Modify VALUES in column B to adjust game balance",
"🔴 Negative values in actions = COST (energy spent)",
"🟢 Positive values in actions = GAIN (energy restored)",
"⚖️ Check BALANCE CALCULATIONS section for auto-computed metrics",
"📊 Key ratios to monitor:",
" - Turns to deplete should be > day cycle length",
" - Hunt efficiency should balance with gather efficiency",
" - Sleep ROI determines rest frequency",
"🚨 RED FLAGS:",
" - Turns to dehydrate < day_steps (too fast!)",
" - Hunt efficiency >> Gather efficiency (hunting OP)",
" - Meat spoils before it can be used (meat_decay too low)",
]
for note in notes:
cell = ws.cell(row=row, column=1, value=note)
cell.font = Font(size=11, color=COLORS['black'])
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=4)
row += 1
# Save workbook
wb.save(output_path)
print(f"✅ Balance sheet created: {output_path}")
print(f" 📊 {len(param_rows)} parameters exported")
print(f" 📐 12 balance formulas included")
print(f" 📈 Includes economy/trading behavior settings")
print(f" 🎨 Clown colors applied! 🤡")
def main():
parser = argparse.ArgumentParser(description="Convert config to Excel balance sheet")
parser.add_argument("--output", "-o", default="balance_sheet.xlsx",
help="Output Excel file path")
parser.add_argument("--config", "-c", default=None,
help="Input JSON config file (optional, uses defaults if not provided)")
args = parser.parse_args()
# Load config
if args.config:
config = SimulationConfig.load(args.config)
print(f"📂 Loaded config from: {args.config}")
else:
config = get_config()
print("📂 Using default configuration")
create_balance_sheet(config, args.output)
if __name__ == "__main__":
main()