604 lines
27 KiB
Python
604 lines
27 KiB
Python
#!/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()
|
||
|