[upd] rebalance professions + add game controls to the stats page
This commit is contained in:
parent
308f738c37
commit
25bd13e001
@ -202,10 +202,28 @@ def create_gather_action(
|
||||
if success_chance < 1.0:
|
||||
base_cost *= 1.0 + (1.0 - success_chance) * 0.3
|
||||
|
||||
# Mild personality adjustments (shouldn't dominate the cost)
|
||||
# STRONG profession specialization effect for gathering
|
||||
if action_type == ActionType.GATHER:
|
||||
# Cautious agents slightly prefer gathering
|
||||
base_cost *= (0.9 + state.risk_tolerance * 0.2)
|
||||
# Compare gather_preference to other preferences
|
||||
# Specialists get big discounts, generalists pay penalty
|
||||
other_prefs = (state.hunt_preference + state.trade_preference) / 2
|
||||
relative_strength = state.gather_preference / max(0.1, other_prefs)
|
||||
|
||||
# relative_strength > 1.0 means gathering is your specialty
|
||||
# relative_strength < 1.0 means you're NOT a gatherer
|
||||
if relative_strength >= 1.0:
|
||||
# Specialist discount: up to 50% off
|
||||
preference_modifier = 1.0 / relative_strength
|
||||
else:
|
||||
# Non-specialist penalty: up to 3x cost
|
||||
preference_modifier = 1.0 + (1.0 - relative_strength) * 2.0
|
||||
|
||||
base_cost *= preference_modifier
|
||||
|
||||
# Skill reduces cost further (experienced = efficient)
|
||||
# skill 0: no bonus, skill 1.0: 40% discount
|
||||
skill_modifier = 1.0 - state.gathering_skill * 0.4
|
||||
base_cost *= skill_modifier
|
||||
|
||||
return base_cost
|
||||
|
||||
@ -280,8 +298,28 @@ def create_buy_action(resource_type: ResourceType) -> GOAPAction:
|
||||
# Trading cost is low (1 energy)
|
||||
base_cost = 0.5
|
||||
|
||||
# Market-oriented agents prefer buying
|
||||
base_cost *= (1.5 - state.market_affinity)
|
||||
# MILD profession effect for trading (everyone should be able to trade)
|
||||
# Traders get a bonus, but non-traders shouldn't be heavily penalized
|
||||
# (trading benefits the whole economy)
|
||||
other_prefs = (state.hunt_preference + state.gather_preference) / 2
|
||||
relative_strength = state.trade_preference / max(0.1, other_prefs)
|
||||
|
||||
if relative_strength >= 1.0:
|
||||
# Specialist discount: up to 40% off for dedicated traders
|
||||
preference_modifier = max(0.6, 1.0 / relative_strength)
|
||||
else:
|
||||
# Mild non-specialist penalty: up to 50% cost increase
|
||||
preference_modifier = 1.0 + (1.0 - relative_strength) * 0.5
|
||||
|
||||
base_cost *= preference_modifier
|
||||
|
||||
# Skill reduces cost (experienced traders are efficient)
|
||||
# skill 0: no bonus, skill 1.0: 40% discount
|
||||
skill_modifier = 1.0 - state.trading_skill * 0.4
|
||||
base_cost *= skill_modifier
|
||||
|
||||
# Market affinity still has mild effect
|
||||
base_cost *= (1.2 - state.market_affinity * 0.4)
|
||||
|
||||
# Check if it's a good deal
|
||||
if resource_type == ResourceType.MEAT:
|
||||
|
||||
@ -159,9 +159,28 @@ def _create_hunt() -> GOAPAction:
|
||||
if config.success_chance < 1.0:
|
||||
base_cost *= 1.0 + (1.0 - config.success_chance) * 0.2
|
||||
|
||||
# Risk-tolerant agents prefer hunting
|
||||
# Range: 0.85 (high risk tolerance) to 1.15 (low risk tolerance)
|
||||
risk_modifier = 1.0 + (0.5 - state.risk_tolerance) * 0.3
|
||||
# STRONG profession specialization effect for hunting
|
||||
# Compare hunt_preference to other preferences
|
||||
other_prefs = (state.gather_preference + state.trade_preference) / 2
|
||||
relative_strength = state.hunt_preference / max(0.1, other_prefs)
|
||||
|
||||
# relative_strength > 1.0 means hunting is your specialty
|
||||
if relative_strength >= 1.0:
|
||||
# Specialist discount: up to 50% off
|
||||
preference_modifier = 1.0 / relative_strength
|
||||
else:
|
||||
# Non-specialist penalty: up to 3x cost
|
||||
preference_modifier = 1.0 + (1.0 - relative_strength) * 2.0
|
||||
|
||||
base_cost *= preference_modifier
|
||||
|
||||
# Skill reduces cost further (experienced hunters are efficient)
|
||||
# skill 0: no bonus, skill 1.0: 40% discount
|
||||
skill_modifier = 1.0 - state.hunting_skill * 0.4
|
||||
base_cost *= skill_modifier
|
||||
|
||||
# Risk tolerance still has mild effect
|
||||
risk_modifier = 1.0 + (0.5 - state.risk_tolerance) * 0.15
|
||||
base_cost *= risk_modifier
|
||||
|
||||
# Big bonus if we have no meat - prioritize getting some
|
||||
@ -295,11 +314,29 @@ def _create_sell_action(resource_type: ResourceType, min_keep: int = 1) -> GOAPA
|
||||
return result
|
||||
|
||||
def cost(state: WorldState) -> float:
|
||||
# Selling has low cost
|
||||
# Selling has low cost - everyone should be able to sell excess
|
||||
base_cost = 1.0
|
||||
|
||||
# Hoarders reluctant to sell
|
||||
base_cost *= (0.5 + state.hoarding_rate)
|
||||
# MILD profession effect for selling (everyone should be able to trade)
|
||||
other_prefs = (state.hunt_preference + state.gather_preference) / 2
|
||||
relative_strength = state.trade_preference / max(0.1, other_prefs)
|
||||
|
||||
if relative_strength >= 1.0:
|
||||
# Specialist discount: up to 40% off for dedicated traders
|
||||
preference_modifier = max(0.6, 1.0 / relative_strength)
|
||||
else:
|
||||
# Mild non-specialist penalty: up to 50% cost increase
|
||||
preference_modifier = 1.0 + (1.0 - relative_strength) * 0.5
|
||||
|
||||
base_cost *= preference_modifier
|
||||
|
||||
# Skill reduces cost (experienced traders know the market)
|
||||
# skill 0: no bonus, skill 1.0: 40% discount
|
||||
skill_modifier = 1.0 - state.trading_skill * 0.4
|
||||
base_cost *= skill_modifier
|
||||
|
||||
# Hoarders reluctant to sell (mild effect)
|
||||
base_cost *= (0.8 + state.hoarding_rate * 0.4)
|
||||
|
||||
return base_cost
|
||||
|
||||
|
||||
@ -73,6 +73,16 @@ class WorldState:
|
||||
market_affinity: float = 0.5
|
||||
is_trader: bool = False
|
||||
|
||||
# Profession preferences (0.5-1.5 range, higher = more preferred)
|
||||
gather_preference: float = 1.0
|
||||
hunt_preference: float = 1.0
|
||||
trade_preference: float = 1.0
|
||||
|
||||
# Skill levels (0.0-1.0, higher = more skilled)
|
||||
hunting_skill: float = 0.0
|
||||
gathering_skill: float = 0.0
|
||||
trading_skill: float = 0.0
|
||||
|
||||
# Critical thresholds (from config)
|
||||
critical_threshold: float = 0.25
|
||||
low_threshold: float = 0.45
|
||||
@ -297,6 +307,12 @@ def create_world_state(
|
||||
risk_tolerance=agent.personality.risk_tolerance,
|
||||
market_affinity=agent.personality.market_affinity,
|
||||
is_trader=is_trader,
|
||||
gather_preference=agent.personality.gather_preference,
|
||||
hunt_preference=agent.personality.hunt_preference,
|
||||
trade_preference=agent.personality.trade_preference,
|
||||
hunting_skill=agent.skills.hunting,
|
||||
gathering_skill=agent.skills.gathering,
|
||||
trading_skill=agent.skills.trading,
|
||||
critical_threshold=agent_config.critical_threshold,
|
||||
low_threshold=0.45, # Could also be in config
|
||||
)
|
||||
|
||||
@ -28,7 +28,7 @@ class ChartColors:
|
||||
GRID = '#2f3545'
|
||||
TEXT = '#e0e0e8'
|
||||
TEXT_DIM = '#7a7e8c'
|
||||
|
||||
|
||||
# Neon accents for data series
|
||||
CYAN = '#00d4ff'
|
||||
MAGENTA = '#ff0099'
|
||||
@ -38,7 +38,7 @@ class ChartColors:
|
||||
YELLOW = '#ffcc00'
|
||||
TEAL = '#00ffa3'
|
||||
PINK = '#ff1493'
|
||||
|
||||
|
||||
# Series colors for different resources/categories
|
||||
SERIES = [CYAN, MAGENTA, LIME, ORANGE, PURPLE, YELLOW, TEAL, PINK]
|
||||
|
||||
@ -60,26 +60,26 @@ class UIColors:
|
||||
class HistoryData:
|
||||
"""Stores historical simulation data for charting."""
|
||||
max_history: int = 200
|
||||
|
||||
|
||||
# Time series data
|
||||
turns: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
population: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
deaths_cumulative: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
|
||||
|
||||
# Money/Wealth data
|
||||
total_money: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
avg_wealth: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
gini_coefficient: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
|
||||
|
||||
# Price history per resource
|
||||
prices: dict = field(default_factory=dict) # resource -> deque of prices
|
||||
|
||||
|
||||
# Trade statistics
|
||||
trade_volume: deque = field(default_factory=lambda: deque(maxlen=200))
|
||||
|
||||
|
||||
# Profession counts over time
|
||||
professions: dict = field(default_factory=dict) # profession -> deque of counts
|
||||
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all history data."""
|
||||
self.turns.clear()
|
||||
@ -91,51 +91,51 @@ class HistoryData:
|
||||
self.prices.clear()
|
||||
self.trade_volume.clear()
|
||||
self.professions.clear()
|
||||
|
||||
|
||||
def update(self, state: "SimulationState") -> None:
|
||||
"""Update history with new state data."""
|
||||
turn = state.turn
|
||||
|
||||
|
||||
# Avoid duplicate entries for the same turn
|
||||
if self.turns and self.turns[-1] == turn:
|
||||
return
|
||||
|
||||
|
||||
self.turns.append(turn)
|
||||
|
||||
|
||||
# Population
|
||||
living = len([a for a in state.agents if a.get("is_alive", False)])
|
||||
self.population.append(living)
|
||||
self.deaths_cumulative.append(state.statistics.get("total_agents_died", 0))
|
||||
|
||||
|
||||
# Wealth data
|
||||
stats = state.statistics
|
||||
self.total_money.append(stats.get("total_money_in_circulation", 0))
|
||||
self.avg_wealth.append(stats.get("avg_money", 0))
|
||||
self.gini_coefficient.append(stats.get("gini_coefficient", 0))
|
||||
|
||||
|
||||
# Price history from market
|
||||
for resource, data in state.market_prices.items():
|
||||
if resource not in self.prices:
|
||||
self.prices[resource] = deque(maxlen=self.max_history)
|
||||
|
||||
|
||||
# Track lowest price (current market rate)
|
||||
lowest = data.get("lowest_price")
|
||||
avg = data.get("avg_sale_price")
|
||||
# Use lowest price if available, else avg sale price
|
||||
price = lowest if lowest is not None else avg
|
||||
self.prices[resource].append(price)
|
||||
|
||||
|
||||
# Trade volume (from recent trades in market orders)
|
||||
trades = len(state.market_orders) # Active orders as proxy
|
||||
self.trade_volume.append(trades)
|
||||
|
||||
|
||||
# Profession distribution
|
||||
professions = stats.get("professions", {})
|
||||
for prof, count in professions.items():
|
||||
if prof not in self.professions:
|
||||
self.professions[prof] = deque(maxlen=self.max_history)
|
||||
self.professions[prof].append(count)
|
||||
|
||||
|
||||
# Pad missing professions with 0
|
||||
for prof in self.professions:
|
||||
if prof not in professions:
|
||||
@ -144,12 +144,12 @@ class HistoryData:
|
||||
|
||||
class ChartRenderer:
|
||||
"""Renders matplotlib charts to pygame surfaces."""
|
||||
|
||||
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.dpi = 100
|
||||
|
||||
|
||||
# Configure matplotlib style
|
||||
plt.style.use('dark_background')
|
||||
plt.rcParams.update({
|
||||
@ -168,27 +168,27 @@ class ChartRenderer:
|
||||
'axes.titlesize': 11,
|
||||
'axes.titleweight': 'bold',
|
||||
})
|
||||
|
||||
|
||||
def _fig_to_surface(self, fig: Figure) -> pygame.Surface:
|
||||
"""Convert a matplotlib figure to a pygame surface."""
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format='png', dpi=self.dpi,
|
||||
fig.savefig(buf, format='png', dpi=self.dpi,
|
||||
facecolor=ChartColors.BG, edgecolor='none',
|
||||
bbox_inches='tight', pad_inches=0.1)
|
||||
buf.seek(0)
|
||||
|
||||
|
||||
surface = pygame.image.load(buf, 'png')
|
||||
buf.close()
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
return surface
|
||||
|
||||
|
||||
def render_price_history(self, history: HistoryData, width: int, height: int) -> pygame.Surface:
|
||||
"""Render price history chart for all resources."""
|
||||
fig, ax = plt.subplots(figsize=(width/self.dpi, height/self.dpi), dpi=self.dpi)
|
||||
|
||||
|
||||
turns = list(history.turns) if history.turns else [0]
|
||||
|
||||
|
||||
has_data = False
|
||||
for i, (resource, prices) in enumerate(history.prices.items()):
|
||||
if prices and any(p is not None for p in prices):
|
||||
@ -197,46 +197,46 @@ class ChartRenderer:
|
||||
valid_prices = [p if p is not None else 0 for p in prices]
|
||||
# Align with turns
|
||||
min_len = min(len(turns), len(valid_prices))
|
||||
ax.plot(list(turns)[-min_len:], valid_prices[-min_len:],
|
||||
ax.plot(list(turns)[-min_len:], valid_prices[-min_len:],
|
||||
color=color, linewidth=1.5, label=resource.capitalize(), alpha=0.9)
|
||||
has_data = True
|
||||
|
||||
|
||||
ax.set_title('Market Prices', color=ChartColors.CYAN)
|
||||
ax.set_xlabel('Turn')
|
||||
ax.set_ylabel('Price (coins)')
|
||||
ax.grid(True, alpha=0.2)
|
||||
|
||||
|
||||
if has_data:
|
||||
ax.legend(loc='upper left', fontsize=8, framealpha=0.8)
|
||||
|
||||
|
||||
ax.set_ylim(bottom=0)
|
||||
ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
|
||||
|
||||
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
def render_population(self, history: HistoryData, width: int, height: int) -> pygame.Surface:
|
||||
"""Render population over time chart."""
|
||||
fig, ax = plt.subplots(figsize=(width/self.dpi, height/self.dpi), dpi=self.dpi)
|
||||
|
||||
|
||||
turns = list(history.turns) if history.turns else [0]
|
||||
population = list(history.population) if history.population else [0]
|
||||
deaths = list(history.deaths_cumulative) if history.deaths_cumulative else [0]
|
||||
|
||||
|
||||
min_len = min(len(turns), len(population))
|
||||
|
||||
|
||||
# Population line
|
||||
ax.fill_between(turns[-min_len:], population[-min_len:],
|
||||
ax.fill_between(turns[-min_len:], population[-min_len:],
|
||||
alpha=0.3, color=ChartColors.CYAN)
|
||||
ax.plot(turns[-min_len:], population[-min_len:],
|
||||
ax.plot(turns[-min_len:], population[-min_len:],
|
||||
color=ChartColors.CYAN, linewidth=2, label='Living')
|
||||
|
||||
|
||||
# Deaths line
|
||||
if deaths:
|
||||
ax.plot(turns[-min_len:], deaths[-min_len:],
|
||||
color=ChartColors.MAGENTA, linewidth=1.5, linestyle='--',
|
||||
ax.plot(turns[-min_len:], deaths[-min_len:],
|
||||
color=ChartColors.MAGENTA, linewidth=1.5, linestyle='--',
|
||||
label='Total Deaths', alpha=0.8)
|
||||
|
||||
|
||||
ax.set_title('Population Over Time', color=ChartColors.LIME)
|
||||
ax.set_xlabel('Turn')
|
||||
ax.set_ylabel('Count')
|
||||
@ -244,28 +244,28 @@ class ChartRenderer:
|
||||
ax.legend(loc='upper right', fontsize=8)
|
||||
ax.set_ylim(bottom=0)
|
||||
ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
|
||||
|
||||
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
def render_wealth_distribution(self, state: "SimulationState", width: int, height: int) -> pygame.Surface:
|
||||
"""Render current wealth distribution as a bar chart."""
|
||||
fig, ax = plt.subplots(figsize=(width/self.dpi, height/self.dpi), dpi=self.dpi)
|
||||
|
||||
|
||||
# Get agent wealth data
|
||||
agents = [a for a in state.agents if a.get("is_alive", False)]
|
||||
if not agents:
|
||||
ax.text(0.5, 0.5, 'No living agents', ha='center', va='center',
|
||||
ax.text(0.5, 0.5, 'No living agents', ha='center', va='center',
|
||||
color=ChartColors.TEXT_DIM, fontsize=12)
|
||||
ax.set_title('Wealth Distribution', color=ChartColors.ORANGE)
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
# Sort by wealth
|
||||
agents_sorted = sorted(agents, key=lambda a: a.get("money", 0), reverse=True)
|
||||
names = [a.get("name", "?")[:8] for a in agents_sorted]
|
||||
wealth = [a.get("money", 0) for a in agents_sorted]
|
||||
|
||||
|
||||
# Create gradient colors based on wealth ranking
|
||||
colors = []
|
||||
for i in range(len(agents_sorted)):
|
||||
@ -275,86 +275,86 @@ class ChartRenderer:
|
||||
g = int(212 - ratio * 212)
|
||||
b = int(255 - ratio * 102)
|
||||
colors.append(f'#{r:02x}{g:02x}{b:02x}')
|
||||
|
||||
|
||||
bars = ax.barh(range(len(agents_sorted)), wealth, color=colors, alpha=0.85)
|
||||
ax.set_yticks(range(len(agents_sorted)))
|
||||
ax.set_yticklabels(names, fontsize=7)
|
||||
ax.invert_yaxis() # Rich at top
|
||||
|
||||
|
||||
# Add value labels
|
||||
for bar, val in zip(bars, wealth):
|
||||
ax.text(bar.get_width() + 1, bar.get_y() + bar.get_height()/2,
|
||||
f'{val}', va='center', fontsize=7, color=ChartColors.TEXT_DIM)
|
||||
|
||||
|
||||
ax.set_title('Wealth Distribution', color=ChartColors.ORANGE)
|
||||
ax.set_xlabel('Coins')
|
||||
ax.grid(True, alpha=0.2, axis='x')
|
||||
|
||||
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
def render_wealth_over_time(self, history: HistoryData, width: int, height: int) -> pygame.Surface:
|
||||
"""Render wealth metrics over time (total money, avg, gini)."""
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(width/self.dpi, height/self.dpi),
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(width/self.dpi, height/self.dpi),
|
||||
dpi=self.dpi, height_ratios=[2, 1])
|
||||
|
||||
|
||||
turns = list(history.turns) if history.turns else [0]
|
||||
total = list(history.total_money) if history.total_money else [0]
|
||||
avg = list(history.avg_wealth) if history.avg_wealth else [0]
|
||||
gini = list(history.gini_coefficient) if history.gini_coefficient else [0]
|
||||
|
||||
|
||||
min_len = min(len(turns), len(total), len(avg))
|
||||
|
||||
|
||||
# Total and average wealth
|
||||
ax1.plot(turns[-min_len:], total[-min_len:],
|
||||
ax1.plot(turns[-min_len:], total[-min_len:],
|
||||
color=ChartColors.CYAN, linewidth=2, label='Total Money')
|
||||
ax1.fill_between(turns[-min_len:], total[-min_len:],
|
||||
ax1.fill_between(turns[-min_len:], total[-min_len:],
|
||||
alpha=0.2, color=ChartColors.CYAN)
|
||||
|
||||
|
||||
ax1_twin = ax1.twinx()
|
||||
ax1_twin.plot(turns[-min_len:], avg[-min_len:],
|
||||
ax1_twin.plot(turns[-min_len:], avg[-min_len:],
|
||||
color=ChartColors.LIME, linewidth=1.5, linestyle='--', label='Avg Wealth')
|
||||
ax1_twin.set_ylabel('Avg Wealth', color=ChartColors.LIME)
|
||||
ax1_twin.tick_params(axis='y', labelcolor=ChartColors.LIME)
|
||||
|
||||
|
||||
ax1.set_title('Money in Circulation', color=ChartColors.YELLOW)
|
||||
ax1.set_ylabel('Total Money', color=ChartColors.CYAN)
|
||||
ax1.tick_params(axis='y', labelcolor=ChartColors.CYAN)
|
||||
ax1.grid(True, alpha=0.2)
|
||||
ax1.set_ylim(bottom=0)
|
||||
|
||||
|
||||
# Gini coefficient (inequality)
|
||||
min_len_gini = min(len(turns), len(gini))
|
||||
ax2.fill_between(turns[-min_len_gini:], gini[-min_len_gini:],
|
||||
ax2.fill_between(turns[-min_len_gini:], gini[-min_len_gini:],
|
||||
alpha=0.4, color=ChartColors.MAGENTA)
|
||||
ax2.plot(turns[-min_len_gini:], gini[-min_len_gini:],
|
||||
ax2.plot(turns[-min_len_gini:], gini[-min_len_gini:],
|
||||
color=ChartColors.MAGENTA, linewidth=1.5)
|
||||
ax2.set_xlabel('Turn')
|
||||
ax2.set_ylabel('Gini')
|
||||
ax2.set_title('Inequality Index', color=ChartColors.MAGENTA, fontsize=9)
|
||||
ax2.set_ylim(0, 1)
|
||||
ax2.grid(True, alpha=0.2)
|
||||
|
||||
|
||||
# Add reference lines for gini
|
||||
ax2.axhline(y=0.4, color=ChartColors.YELLOW, linestyle=':', alpha=0.5, linewidth=1)
|
||||
ax2.text(turns[-1] if turns else 0, 0.42, 'Moderate', fontsize=7,
|
||||
ax2.text(turns[-1] if turns else 0, 0.42, 'Moderate', fontsize=7,
|
||||
color=ChartColors.YELLOW, alpha=0.7)
|
||||
|
||||
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
def render_professions(self, state: "SimulationState", history: HistoryData,
|
||||
|
||||
def render_professions(self, state: "SimulationState", history: HistoryData,
|
||||
width: int, height: int) -> pygame.Surface:
|
||||
"""Render profession distribution as pie chart and area chart."""
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(width/self.dpi, height/self.dpi), dpi=self.dpi)
|
||||
|
||||
|
||||
# Current profession pie chart
|
||||
professions = state.statistics.get("professions", {})
|
||||
if professions:
|
||||
labels = list(professions.keys())
|
||||
sizes = list(professions.values())
|
||||
colors = [ChartColors.SERIES[i % len(ChartColors.SERIES)] for i in range(len(labels))]
|
||||
|
||||
|
||||
wedges, texts, autotexts = ax1.pie(
|
||||
sizes, labels=labels, colors=colors, autopct='%1.0f%%',
|
||||
startangle=90, pctdistance=0.75,
|
||||
@ -367,7 +367,7 @@ class ChartRenderer:
|
||||
else:
|
||||
ax1.text(0.5, 0.5, 'No data', ha='center', va='center', color=ChartColors.TEXT_DIM)
|
||||
ax1.set_title('Current Distribution', color=ChartColors.PURPLE)
|
||||
|
||||
|
||||
# Profession history as stacked area
|
||||
turns = list(history.turns) if history.turns else [0]
|
||||
if history.professions and turns:
|
||||
@ -379,37 +379,37 @@ class ChartRenderer:
|
||||
while len(prof_data) < len(turns):
|
||||
prof_data.insert(0, 0)
|
||||
data.append(prof_data[-len(turns):])
|
||||
|
||||
|
||||
colors = [ChartColors.SERIES[i % len(ChartColors.SERIES)] for i in range(len(profs_list))]
|
||||
ax2.stackplot(turns, *data, labels=profs_list, colors=colors, alpha=0.8)
|
||||
ax2.legend(loc='upper left', fontsize=7, framealpha=0.8)
|
||||
ax2.set_xlabel('Turn')
|
||||
ax2.set_ylabel('Count')
|
||||
|
||||
|
||||
ax2.set_title('Over Time', color=ChartColors.PURPLE, fontsize=10)
|
||||
ax2.grid(True, alpha=0.2)
|
||||
|
||||
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
def render_market_activity(self, state: "SimulationState", history: HistoryData,
|
||||
width: int, height: int) -> pygame.Surface:
|
||||
"""Render market activity - orders by resource, supply/demand."""
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(width/self.dpi, height/self.dpi), dpi=self.dpi)
|
||||
|
||||
|
||||
# Current market orders by resource type
|
||||
prices = state.market_prices
|
||||
resources = []
|
||||
quantities = []
|
||||
colors = []
|
||||
|
||||
|
||||
for i, (resource, data) in enumerate(prices.items()):
|
||||
qty = data.get("total_available", 0)
|
||||
if qty > 0:
|
||||
resources.append(resource.capitalize())
|
||||
quantities.append(qty)
|
||||
colors.append(ChartColors.SERIES[i % len(ChartColors.SERIES)])
|
||||
|
||||
|
||||
if resources:
|
||||
bars = ax1.bar(resources, quantities, color=colors, alpha=0.85)
|
||||
ax1.set_ylabel('Available')
|
||||
@ -418,93 +418,93 @@ class ChartRenderer:
|
||||
str(val), ha='center', fontsize=8, color=ChartColors.TEXT)
|
||||
else:
|
||||
ax1.text(0.5, 0.5, 'No orders', ha='center', va='center', color=ChartColors.TEXT_DIM)
|
||||
|
||||
|
||||
ax1.set_title('Market Supply', color=ChartColors.TEAL, fontsize=10)
|
||||
ax1.tick_params(axis='x', rotation=45, labelsize=7)
|
||||
ax1.grid(True, alpha=0.2, axis='y')
|
||||
|
||||
|
||||
# Supply/Demand scores
|
||||
resources_sd = []
|
||||
supply_scores = []
|
||||
demand_scores = []
|
||||
|
||||
|
||||
for resource, data in prices.items():
|
||||
resources_sd.append(resource[:6])
|
||||
supply_scores.append(data.get("supply_score", 0.5))
|
||||
demand_scores.append(data.get("demand_score", 0.5))
|
||||
|
||||
|
||||
if resources_sd:
|
||||
x = np.arange(len(resources_sd))
|
||||
width_bar = 0.35
|
||||
|
||||
ax2.bar(x - width_bar/2, supply_scores, width_bar, label='Supply',
|
||||
|
||||
ax2.bar(x - width_bar/2, supply_scores, width_bar, label='Supply',
|
||||
color=ChartColors.CYAN, alpha=0.8)
|
||||
ax2.bar(x + width_bar/2, demand_scores, width_bar, label='Demand',
|
||||
ax2.bar(x + width_bar/2, demand_scores, width_bar, label='Demand',
|
||||
color=ChartColors.MAGENTA, alpha=0.8)
|
||||
|
||||
|
||||
ax2.set_xticks(x)
|
||||
ax2.set_xticklabels(resources_sd, fontsize=7, rotation=45)
|
||||
ax2.set_ylabel('Score')
|
||||
ax2.legend(fontsize=7)
|
||||
ax2.set_ylim(0, 1.2)
|
||||
|
||||
|
||||
ax2.set_title('Supply/Demand', color=ChartColors.TEAL, fontsize=10)
|
||||
ax2.grid(True, alpha=0.2, axis='y')
|
||||
|
||||
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
def render_agent_stats(self, state: "SimulationState", width: int, height: int) -> pygame.Surface:
|
||||
"""Render aggregate agent statistics - energy, hunger, thirst distributions."""
|
||||
fig, axes = plt.subplots(2, 2, figsize=(width/self.dpi, height/self.dpi), dpi=self.dpi)
|
||||
|
||||
|
||||
agents = [a for a in state.agents if a.get("is_alive", False)]
|
||||
|
||||
|
||||
if not agents:
|
||||
for ax in axes.flat:
|
||||
ax.text(0.5, 0.5, 'No agents', ha='center', va='center', color=ChartColors.TEXT_DIM)
|
||||
fig.suptitle('Agent Statistics', color=ChartColors.CYAN)
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
|
||||
|
||||
# Extract stats
|
||||
energies = [a.get("stats", {}).get("energy", 0) for a in agents]
|
||||
hungers = [a.get("stats", {}).get("hunger", 0) for a in agents]
|
||||
thirsts = [a.get("stats", {}).get("thirst", 0) for a in agents]
|
||||
heats = [a.get("stats", {}).get("heat", 0) for a in agents]
|
||||
|
||||
|
||||
max_energy = agents[0].get("stats", {}).get("max_energy", 100)
|
||||
max_hunger = agents[0].get("stats", {}).get("max_hunger", 100)
|
||||
max_thirst = agents[0].get("stats", {}).get("max_thirst", 100)
|
||||
max_heat = agents[0].get("stats", {}).get("max_heat", 100)
|
||||
|
||||
|
||||
stats_data = [
|
||||
(energies, max_energy, 'Energy', ChartColors.LIME),
|
||||
(hungers, max_hunger, 'Hunger', ChartColors.ORANGE),
|
||||
(thirsts, max_thirst, 'Thirst', ChartColors.CYAN),
|
||||
(heats, max_heat, 'Heat', ChartColors.MAGENTA),
|
||||
]
|
||||
|
||||
|
||||
for ax, (values, max_val, name, color) in zip(axes.flat, stats_data):
|
||||
# Histogram
|
||||
bins = np.linspace(0, max_val, 11)
|
||||
ax.hist(values, bins=bins, color=color, alpha=0.7, edgecolor=ChartColors.PANEL)
|
||||
|
||||
|
||||
# Mean line
|
||||
mean_val = np.mean(values)
|
||||
ax.axvline(x=mean_val, color=ChartColors.TEXT, linestyle='--',
|
||||
ax.axvline(x=mean_val, color=ChartColors.TEXT, linestyle='--',
|
||||
linewidth=1.5, label=f'Avg: {mean_val:.0f}')
|
||||
|
||||
|
||||
# Critical threshold
|
||||
critical = max_val * 0.25
|
||||
ax.axvline(x=critical, color=ChartColors.MAGENTA, linestyle=':',
|
||||
ax.axvline(x=critical, color=ChartColors.MAGENTA, linestyle=':',
|
||||
linewidth=1, alpha=0.7)
|
||||
|
||||
|
||||
ax.set_title(name, color=color, fontsize=9)
|
||||
ax.set_xlim(0, max_val)
|
||||
ax.legend(fontsize=7, loc='upper right')
|
||||
ax.grid(True, alpha=0.2)
|
||||
|
||||
|
||||
fig.suptitle('Agent Statistics Distribution', color=ChartColors.CYAN, fontsize=11)
|
||||
fig.tight_layout()
|
||||
return self._fig_to_surface(fig)
|
||||
@ -512,7 +512,7 @@ class ChartRenderer:
|
||||
|
||||
class StatsRenderer:
|
||||
"""Main statistics panel with tabs and charts."""
|
||||
|
||||
|
||||
TABS = [
|
||||
("Prices", "price_history"),
|
||||
("Wealth", "wealth"),
|
||||
@ -521,35 +521,35 @@ class StatsRenderer:
|
||||
("Market", "market"),
|
||||
("Agent Stats", "agent_stats"),
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, screen: pygame.Surface):
|
||||
self.screen = screen
|
||||
self.visible = False
|
||||
|
||||
|
||||
self.font = pygame.font.Font(None, 24)
|
||||
self.small_font = pygame.font.Font(None, 18)
|
||||
self.title_font = pygame.font.Font(None, 32)
|
||||
|
||||
|
||||
self.current_tab = 0
|
||||
self.tab_hovered = -1
|
||||
|
||||
|
||||
# History data
|
||||
self.history = HistoryData()
|
||||
|
||||
|
||||
# Chart renderer
|
||||
self.chart_renderer: Optional[ChartRenderer] = None
|
||||
|
||||
|
||||
# Cached chart surfaces
|
||||
self._chart_cache: dict[str, pygame.Surface] = {}
|
||||
self._cache_turn: int = -1
|
||||
|
||||
|
||||
# Layout
|
||||
self._calculate_layout()
|
||||
|
||||
|
||||
def _calculate_layout(self) -> None:
|
||||
"""Calculate panel layout based on screen size."""
|
||||
screen_w, screen_h = self.screen.get_size()
|
||||
|
||||
|
||||
# Panel takes most of the screen with some margin
|
||||
margin = 30
|
||||
self.panel_rect = pygame.Rect(
|
||||
@ -557,7 +557,7 @@ class StatsRenderer:
|
||||
screen_w - margin * 2,
|
||||
screen_h - margin * 2
|
||||
)
|
||||
|
||||
|
||||
# Tab bar
|
||||
self.tab_height = 40
|
||||
self.tab_rect = pygame.Rect(
|
||||
@ -566,7 +566,7 @@ class StatsRenderer:
|
||||
self.panel_rect.width,
|
||||
self.tab_height
|
||||
)
|
||||
|
||||
|
||||
# Chart area
|
||||
self.chart_rect = pygame.Rect(
|
||||
self.panel_rect.x + 10,
|
||||
@ -574,52 +574,52 @@ class StatsRenderer:
|
||||
self.panel_rect.width - 20,
|
||||
self.panel_rect.height - self.tab_height - 20
|
||||
)
|
||||
|
||||
|
||||
# Initialize chart renderer with chart area size
|
||||
self.chart_renderer = ChartRenderer(
|
||||
self.chart_rect.width,
|
||||
self.chart_rect.height
|
||||
)
|
||||
|
||||
|
||||
# Calculate tab widths
|
||||
self.tab_width = self.panel_rect.width // len(self.TABS)
|
||||
|
||||
|
||||
def toggle(self) -> None:
|
||||
"""Toggle visibility of the stats panel."""
|
||||
self.visible = not self.visible
|
||||
if self.visible:
|
||||
self._invalidate_cache()
|
||||
|
||||
|
||||
def update_history(self, state: "SimulationState") -> None:
|
||||
"""Update history data with new state."""
|
||||
if state:
|
||||
self.history.update(state)
|
||||
|
||||
|
||||
def clear_history(self) -> None:
|
||||
"""Clear all history data (e.g., on simulation reset)."""
|
||||
self.history.clear()
|
||||
self._invalidate_cache()
|
||||
|
||||
|
||||
def _invalidate_cache(self) -> None:
|
||||
"""Invalidate chart cache to force re-render."""
|
||||
self._chart_cache.clear()
|
||||
self._cache_turn = -1
|
||||
|
||||
|
||||
def handle_event(self, event: pygame.event.Event) -> bool:
|
||||
"""Handle input events. Returns True if event was consumed."""
|
||||
if not self.visible:
|
||||
return False
|
||||
|
||||
|
||||
if event.type == pygame.MOUSEMOTION:
|
||||
self._handle_mouse_motion(event.pos)
|
||||
return True
|
||||
|
||||
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||
if self._handle_click(event.pos):
|
||||
return True
|
||||
# Consume clicks when visible
|
||||
return True
|
||||
|
||||
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
self.toggle()
|
||||
@ -632,19 +632,19 @@ class StatsRenderer:
|
||||
self.current_tab = (self.current_tab + 1) % len(self.TABS)
|
||||
self._invalidate_cache()
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _handle_mouse_motion(self, pos: tuple[int, int]) -> None:
|
||||
"""Handle mouse motion for tab hover effects."""
|
||||
self.tab_hovered = -1
|
||||
|
||||
|
||||
if self.tab_rect.collidepoint(pos):
|
||||
rel_x = pos[0] - self.tab_rect.x
|
||||
tab_idx = rel_x // self.tab_width
|
||||
if 0 <= tab_idx < len(self.TABS):
|
||||
self.tab_hovered = tab_idx
|
||||
|
||||
|
||||
def _handle_click(self, pos: tuple[int, int]) -> bool:
|
||||
"""Handle mouse click. Returns True if click was on a tab."""
|
||||
if self.tab_rect.collidepoint(pos):
|
||||
@ -655,20 +655,20 @@ class StatsRenderer:
|
||||
self._invalidate_cache()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _render_chart(self, state: "SimulationState") -> pygame.Surface:
|
||||
"""Render the current tab's chart."""
|
||||
tab_name, tab_key = self.TABS[self.current_tab]
|
||||
|
||||
|
||||
# Check cache
|
||||
current_turn = state.turn if state else 0
|
||||
if tab_key in self._chart_cache and self._cache_turn == current_turn:
|
||||
return self._chart_cache[tab_key]
|
||||
|
||||
|
||||
# Render chart based on current tab
|
||||
width = self.chart_rect.width
|
||||
height = self.chart_rect.height
|
||||
|
||||
|
||||
if tab_key == "price_history":
|
||||
surface = self.chart_renderer.render_price_history(self.history, width, height)
|
||||
elif tab_key == "wealth":
|
||||
@ -676,7 +676,7 @@ class StatsRenderer:
|
||||
half_height = height // 2
|
||||
dist_surface = self.chart_renderer.render_wealth_distribution(state, width, half_height)
|
||||
time_surface = self.chart_renderer.render_wealth_over_time(self.history, width, half_height)
|
||||
|
||||
|
||||
surface = pygame.Surface((width, height))
|
||||
surface.fill(UIColors.BG)
|
||||
surface.blit(dist_surface, (0, 0))
|
||||
@ -693,48 +693,48 @@ class StatsRenderer:
|
||||
# Fallback empty surface
|
||||
surface = pygame.Surface((width, height))
|
||||
surface.fill(UIColors.BG)
|
||||
|
||||
|
||||
# Cache the result
|
||||
self._chart_cache[tab_key] = surface
|
||||
self._cache_turn = current_turn
|
||||
|
||||
|
||||
return surface
|
||||
|
||||
|
||||
def draw(self, state: "SimulationState") -> None:
|
||||
"""Draw the statistics panel."""
|
||||
if not self.visible:
|
||||
return
|
||||
|
||||
|
||||
# Dim background
|
||||
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 220))
|
||||
self.screen.blit(overlay, (0, 0))
|
||||
|
||||
|
||||
# Panel background
|
||||
pygame.draw.rect(self.screen, UIColors.PANEL_BG, self.panel_rect, border_radius=12)
|
||||
pygame.draw.rect(self.screen, UIColors.PANEL_BORDER, self.panel_rect, 2, border_radius=12)
|
||||
|
||||
|
||||
# Draw tabs
|
||||
self._draw_tabs()
|
||||
|
||||
|
||||
# Draw chart
|
||||
if state:
|
||||
chart_surface = self._render_chart(state)
|
||||
self.screen.blit(chart_surface, self.chart_rect.topleft)
|
||||
|
||||
|
||||
# Draw close hint
|
||||
hint = self.small_font.render("Press G or ESC to close | ← → to switch tabs",
|
||||
hint = self.small_font.render("Press G or ESC to close | ← → to switch tabs",
|
||||
True, UIColors.TEXT_SECONDARY)
|
||||
hint_rect = hint.get_rect(centerx=self.panel_rect.centerx,
|
||||
hint_rect = hint.get_rect(centerx=self.panel_rect.centerx,
|
||||
y=self.panel_rect.bottom - 25)
|
||||
self.screen.blit(hint, hint_rect)
|
||||
|
||||
|
||||
def _draw_tabs(self) -> None:
|
||||
"""Draw the tab bar."""
|
||||
for i, (tab_name, _) in enumerate(self.TABS):
|
||||
tab_x = self.tab_rect.x + i * self.tab_width
|
||||
tab_rect = pygame.Rect(tab_x, self.tab_rect.y, self.tab_width, self.tab_height)
|
||||
|
||||
|
||||
# Tab background
|
||||
if i == self.current_tab:
|
||||
color = UIColors.TAB_ACTIVE
|
||||
@ -742,26 +742,26 @@ class StatsRenderer:
|
||||
color = UIColors.TAB_HOVER
|
||||
else:
|
||||
color = UIColors.TAB_INACTIVE
|
||||
|
||||
|
||||
# Draw tab with rounded top corners
|
||||
tab_surface = pygame.Surface((self.tab_width, self.tab_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(tab_surface, color, (0, 0, self.tab_width, self.tab_height),
|
||||
pygame.draw.rect(tab_surface, color, (0, 0, self.tab_width, self.tab_height),
|
||||
border_top_left_radius=8, border_top_right_radius=8)
|
||||
|
||||
|
||||
if i == self.current_tab:
|
||||
# Active tab - solid color
|
||||
tab_surface.set_alpha(255)
|
||||
else:
|
||||
tab_surface.set_alpha(180)
|
||||
|
||||
|
||||
self.screen.blit(tab_surface, (tab_x, self.tab_rect.y))
|
||||
|
||||
|
||||
# Tab text
|
||||
text_color = UIColors.BG if i == self.current_tab else UIColors.TEXT_PRIMARY
|
||||
text = self.small_font.render(tab_name, True, text_color)
|
||||
text_rect = text.get_rect(center=tab_rect.center)
|
||||
self.screen.blit(text, text_rect)
|
||||
|
||||
|
||||
# Tab border
|
||||
if i != self.current_tab:
|
||||
pygame.draw.line(self.screen, UIColors.PANEL_BORDER,
|
||||
|
||||
@ -278,6 +278,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-footer">
|
||||
<div class="controls">
|
||||
<button id="btn-initialize-stats" class="btn btn-secondary" title="Reset Simulation">
|
||||
<span class="btn-icon">⟳</span> Reset
|
||||
</button>
|
||||
<button id="btn-step-stats" class="btn btn-primary" title="Advance one turn">
|
||||
<span class="btn-icon">▶</span> Step
|
||||
</button>
|
||||
<button id="btn-auto-stats" class="btn btn-toggle" title="Toggle auto mode">
|
||||
<span class="btn-icon">⏯</span> Auto
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats-summary-bar">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Turn</span>
|
||||
@ -304,6 +315,11 @@
|
||||
<span class="summary-value" id="stats-gini">0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="speed-control">
|
||||
<label for="speed-slider-stats">Speed</label>
|
||||
<input type="range" id="speed-slider-stats" min="50" max="1000" value="150" step="50">
|
||||
<span id="speed-display-stats">150ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -132,6 +132,12 @@ export default class GameScene extends Phaser.Scene {
|
||||
goapPlanView: document.getElementById('goap-plan-view'),
|
||||
goapActionsList: document.getElementById('goap-actions-list'),
|
||||
chartGoapGoals: document.getElementById('chart-goap-goals'),
|
||||
// Stats screen controls (duplicated for stats page)
|
||||
btnStepStats: document.getElementById('btn-step-stats'),
|
||||
btnAutoStats: document.getElementById('btn-auto-stats'),
|
||||
btnInitializeStats: document.getElementById('btn-initialize-stats'),
|
||||
speedSliderStats: document.getElementById('speed-slider-stats'),
|
||||
speedDisplayStats: document.getElementById('speed-display-stats'),
|
||||
};
|
||||
|
||||
// GOAP state
|
||||
@ -172,6 +178,21 @@ export default class GameScene extends Phaser.Scene {
|
||||
btnCloseStats.removeEventListener('click', this.boundHandlers.closeStats);
|
||||
}
|
||||
|
||||
// Stats screen controls cleanup
|
||||
const { btnStepStats, btnAutoStats, btnInitializeStats, speedSliderStats } = this.domCache;
|
||||
if (btnStepStats && this.boundHandlers.step) {
|
||||
btnStepStats.removeEventListener('click', this.boundHandlers.step);
|
||||
}
|
||||
if (btnAutoStats && this.boundHandlers.auto) {
|
||||
btnAutoStats.removeEventListener('click', this.boundHandlers.auto);
|
||||
}
|
||||
if (btnInitializeStats && this.boundHandlers.init) {
|
||||
btnInitializeStats.removeEventListener('click', this.boundHandlers.init);
|
||||
}
|
||||
if (speedSliderStats && this.boundHandlers.speedStats) {
|
||||
speedSliderStats.removeEventListener('input', this.boundHandlers.speedStats);
|
||||
}
|
||||
|
||||
// Destroy charts
|
||||
Object.values(this.charts).forEach(chart => chart?.destroy());
|
||||
this.charts = {};
|
||||
@ -286,19 +307,34 @@ export default class GameScene extends Phaser.Scene {
|
||||
|
||||
setupUIControls() {
|
||||
const { btnStep, btnAuto, btnInitialize, btnStats, btnCloseStats, speedSlider, speedDisplay, tabButtons } = this.domCache;
|
||||
const { btnStepStats, btnAutoStats, btnInitializeStats, speedSliderStats, speedDisplayStats } = this.domCache;
|
||||
|
||||
// Create bound handlers for later cleanup
|
||||
this.boundHandlers.step = () => this.handleStep();
|
||||
this.boundHandlers.auto = () => this.toggleAutoMode();
|
||||
this.boundHandlers.init = () => this.handleInitialize();
|
||||
|
||||
// Speed handler that syncs both sliders
|
||||
this.boundHandlers.speed = (e) => {
|
||||
this.autoSpeed = parseInt(e.target.value);
|
||||
if (speedDisplay) speedDisplay.textContent = `${this.autoSpeed}ms`;
|
||||
if (speedDisplayStats) speedDisplayStats.textContent = `${this.autoSpeed}ms`;
|
||||
if (speedSliderStats) speedSliderStats.value = this.autoSpeed;
|
||||
if (this.isAutoMode) this.restartAutoMode();
|
||||
};
|
||||
|
||||
this.boundHandlers.speedStats = (e) => {
|
||||
this.autoSpeed = parseInt(e.target.value);
|
||||
if (speedDisplay) speedDisplay.textContent = `${this.autoSpeed}ms`;
|
||||
if (speedDisplayStats) speedDisplayStats.textContent = `${this.autoSpeed}ms`;
|
||||
if (speedSlider) speedSlider.value = this.autoSpeed;
|
||||
if (this.isAutoMode) this.restartAutoMode();
|
||||
};
|
||||
|
||||
this.boundHandlers.openStats = () => this.showStatsScreen();
|
||||
this.boundHandlers.closeStats = () => this.hideStatsScreen();
|
||||
|
||||
// Main controls
|
||||
if (btnStep) btnStep.addEventListener('click', this.boundHandlers.step);
|
||||
if (btnAuto) btnAuto.addEventListener('click', this.boundHandlers.auto);
|
||||
if (btnInitialize) btnInitialize.addEventListener('click', this.boundHandlers.init);
|
||||
@ -306,6 +342,12 @@ export default class GameScene extends Phaser.Scene {
|
||||
if (btnStats) btnStats.addEventListener('click', this.boundHandlers.openStats);
|
||||
if (btnCloseStats) btnCloseStats.addEventListener('click', this.boundHandlers.closeStats);
|
||||
|
||||
// Stats screen controls (same handlers)
|
||||
if (btnStepStats) btnStepStats.addEventListener('click', this.boundHandlers.step);
|
||||
if (btnAutoStats) btnAutoStats.addEventListener('click', this.boundHandlers.auto);
|
||||
if (btnInitializeStats) btnInitializeStats.addEventListener('click', this.boundHandlers.init);
|
||||
if (speedSliderStats) speedSliderStats.addEventListener('input', this.boundHandlers.speedStats);
|
||||
|
||||
// Tab switching
|
||||
tabButtons?.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
|
||||
@ -380,15 +422,19 @@ export default class GameScene extends Phaser.Scene {
|
||||
|
||||
toggleAutoMode() {
|
||||
this.isAutoMode = !this.isAutoMode;
|
||||
const { btnAuto, btnStep } = this.domCache;
|
||||
const { btnAuto, btnStep, btnAutoStats, btnStepStats } = this.domCache;
|
||||
|
||||
if (this.isAutoMode) {
|
||||
btnAuto?.classList.add('active');
|
||||
btnAutoStats?.classList.add('active');
|
||||
btnStep?.setAttribute('disabled', 'true');
|
||||
btnStepStats?.setAttribute('disabled', 'true');
|
||||
this.startAutoMode();
|
||||
} else {
|
||||
btnAuto?.classList.remove('active');
|
||||
btnAutoStats?.classList.remove('active');
|
||||
btnStep?.removeAttribute('disabled');
|
||||
btnStepStats?.removeAttribute('disabled');
|
||||
this.stopAutoMode();
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,7 +613,8 @@ body {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#speed-slider {
|
||||
#speed-slider,
|
||||
#speed-slider-stats {
|
||||
width: 120px;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
@ -623,7 +624,8 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#speed-slider::-webkit-slider-thumb {
|
||||
#speed-slider::-webkit-slider-thumb,
|
||||
#speed-slider-stats::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
@ -633,7 +635,8 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#speed-display {
|
||||
#speed-display,
|
||||
#speed-display-stats {
|
||||
font-family: var(--font-mono);
|
||||
min-width: 50px;
|
||||
}
|
||||
@ -939,16 +942,21 @@ body {
|
||||
|
||||
/* Stats Footer */
|
||||
.stats-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.stats-summary-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-xl);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
@ -1049,6 +1057,28 @@ body {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.stats-footer {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.stats-footer .controls {
|
||||
order: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.stats-footer .stats-summary-bar {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-footer .speed-control {
|
||||
order: 3;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* =================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user