#!/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()