Compare commits

...

1 Commits

Author SHA1 Message Date
Снесарев Максим
67dc007283 [new] web-based frontend 2026-01-19 19:44:38 +03:00
12 changed files with 3712 additions and 170 deletions

View File

@ -121,6 +121,12 @@ class StatisticsSchema(BaseModel):
total_agents_died: int total_agents_died: int
total_money_in_circulation: int total_money_in_circulation: int
professions: dict[str, int] professions: dict[str, int]
# Wealth inequality metrics
avg_money: float = 0.0
median_money: int = 0
richest_agent: int = 0
poorest_agent: int = 0
gini_coefficient: float = 0.0
class ActionLogSchema(BaseModel): class ActionLogSchema(BaseModel):
@ -137,6 +143,18 @@ class TurnLogSchema(BaseModel):
agent_actions: list[ActionLogSchema] agent_actions: list[ActionLogSchema]
deaths: list[str] deaths: list[str]
trades: list[dict] trades: list[dict]
resources_produced: dict[str, int] = {}
resources_consumed: dict[str, int] = {}
resources_spoiled: dict[str, int] = {}
class ResourceStatsSchema(BaseModel):
"""Schema for resource statistics."""
produced: dict[str, int] = {}
consumed: dict[str, int] = {}
spoiled: dict[str, int] = {}
in_inventory: dict[str, int] = {}
in_market: dict[str, int] = {}
class WorldStateResponse(BaseModel): class WorldStateResponse(BaseModel):
@ -152,6 +170,7 @@ class WorldStateResponse(BaseModel):
mode: str mode: str
is_running: bool is_running: bool
recent_logs: list[TurnLogSchema] recent_logs: list[TurnLogSchema]
resource_stats: ResourceStatsSchema = ResourceStatsSchema()
# ============== Control Schemas ============== # ============== Control Schemas ==============

View File

@ -31,31 +31,38 @@ class TurnLog:
agent_actions: list[dict] = field(default_factory=list) agent_actions: list[dict] = field(default_factory=list)
deaths: list[str] = field(default_factory=list) deaths: list[str] = field(default_factory=list)
trades: list[dict] = field(default_factory=list) trades: list[dict] = field(default_factory=list)
# Resource tracking for this turn
resources_produced: dict = field(default_factory=dict)
resources_consumed: dict = field(default_factory=dict)
resources_spoiled: dict = field(default_factory=dict)
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
"turn": self.turn, "turn": self.turn,
"agent_actions": self.agent_actions, "agent_actions": self.agent_actions,
"deaths": self.deaths, "deaths": self.deaths,
"trades": self.trades, "trades": self.trades,
"resources_produced": self.resources_produced,
"resources_consumed": self.resources_consumed,
"resources_spoiled": self.resources_spoiled,
} }
class GameEngine: class GameEngine:
"""Main game engine singleton.""" """Main game engine singleton."""
_instance: Optional["GameEngine"] = None _instance: Optional["GameEngine"] = None
def __new__(cls): def __new__(cls):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance._initialized = False cls._instance._initialized = False
return cls._instance return cls._instance
def __init__(self): def __init__(self):
if self._initialized: if self._initialized:
return return
self.world = World() self.world = World()
self.market = OrderBook() self.market = OrderBook()
self.mode = SimulationMode.MANUAL self.mode = SimulationMode.MANUAL
@ -66,55 +73,76 @@ class GameEngine:
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.turn_logs: list[TurnLog] = [] self.turn_logs: list[TurnLog] = []
self.logger = get_simulation_logger() self.logger = get_simulation_logger()
# Resource statistics tracking (cumulative)
self.resource_stats = {
"produced": {}, # Total resources produced
"consumed": {}, # Total resources consumed
"spoiled": {}, # Total resources spoiled
"traded": {}, # Total resources traded (bought/sold)
"in_market": {}, # Currently in market
"in_inventory": {}, # Currently in all inventories
}
self._initialized = True self._initialized = True
def reset(self, config: Optional[WorldConfig] = None) -> None: def reset(self, config: Optional[WorldConfig] = None) -> None:
"""Reset the simulation to initial state.""" """Reset the simulation to initial state."""
# Stop auto mode if running # Stop auto mode if running
self._stop_auto_mode() self._stop_auto_mode()
if config: if config:
self.world = World(config=config) self.world = World(config=config)
else: else:
self.world = World() self.world = World()
self.market = OrderBook() self.market = OrderBook()
self.turn_logs = [] self.turn_logs = []
# Reset resource statistics
self.resource_stats = {
"produced": {},
"consumed": {},
"spoiled": {},
"traded": {},
"in_market": {},
"in_inventory": {},
}
# Reset and start new logging session # Reset and start new logging session
self.logger = reset_simulation_logger() self.logger = reset_simulation_logger()
sim_config = get_config() sim_config = get_config()
self.logger.start_session(sim_config.to_dict()) self.logger.start_session(sim_config.to_dict())
self.world.initialize() self.world.initialize()
self.is_running = True self.is_running = True
def initialize(self, num_agents: Optional[int] = None) -> None: def initialize(self, num_agents: Optional[int] = None) -> None:
"""Initialize the simulation with agents. """Initialize the simulation with agents.
Args: Args:
num_agents: Number of agents to spawn. If None, uses config.json value. num_agents: Number of agents to spawn. If None, uses config.json value.
""" """
if num_agents is not None: if num_agents is not None:
self.world.config.initial_agents = num_agents self.world.config.initial_agents = num_agents
# Otherwise use the value already loaded from config.json # Otherwise use the value already loaded from config.json
self.world.initialize() self.world.initialize()
# Start logging session # Start logging session
self.logger = reset_simulation_logger() self.logger = reset_simulation_logger()
sim_config = get_config() sim_config = get_config()
self.logger.start_session(sim_config.to_dict()) self.logger.start_session(sim_config.to_dict())
self.is_running = True self.is_running = True
def next_step(self) -> TurnLog: def next_step(self) -> TurnLog:
"""Advance the simulation by one step.""" """Advance the simulation by one step."""
if not self.is_running: if not self.is_running:
return TurnLog(turn=-1) return TurnLog(turn=-1)
turn_log = TurnLog(turn=self.world.current_turn + 1) turn_log = TurnLog(turn=self.world.current_turn + 1)
current_turn = self.world.current_turn + 1 current_turn = self.world.current_turn + 1
# Start logging this turn # Start logging this turn
self.logger.start_turn( self.logger.start_turn(
turn=current_turn, turn=current_turn,
@ -122,13 +150,13 @@ class GameEngine:
step_in_day=self.world.step_in_day + 1, step_in_day=self.world.step_in_day + 1,
time_of_day=self.world.time_of_day.value, time_of_day=self.world.time_of_day.value,
) )
# Log market state before # Log market state before
market_orders_before = [o.to_dict() for o in self.market.get_active_orders()] market_orders_before = [o.to_dict() for o in self.market.get_active_orders()]
# 0. Remove corpses from previous turn (agents who died last turn) # 0. Remove corpses from previous turn (agents who died last turn)
self._remove_old_corpses(current_turn) self._remove_old_corpses(current_turn)
# 1. Collect AI decisions for all living agents (not corpses) # 1. Collect AI decisions for all living agents (not corpses)
decisions: list[tuple[Agent, AIDecision]] = [] decisions: list[tuple[Agent, AIDecision]] = []
for agent in self.world.get_living_agents(): for agent in self.world.get_living_agents():
@ -142,7 +170,7 @@ class GameEngine:
inventory=[r.to_dict() for r in agent.inventory], inventory=[r.to_dict() for r in agent.inventory],
money=agent.money, money=agent.money,
) )
if self.world.is_night(): if self.world.is_night():
# Force sleep at night # Force sleep at night
decision = AIDecision( decision = AIDecision(
@ -152,18 +180,18 @@ class GameEngine:
else: else:
# Pass time info so AI can prepare for night # Pass time info so AI can prepare for night
decision = get_ai_decision( decision = get_ai_decision(
agent, agent,
self.market, self.market,
step_in_day=self.world.step_in_day, step_in_day=self.world.step_in_day,
day_steps=self.world.config.day_steps, day_steps=self.world.config.day_steps,
current_turn=current_turn, current_turn=current_turn,
) )
decisions.append((agent, decision)) decisions.append((agent, decision))
# Log decision # Log decision
self.logger.log_agent_decision(agent.id, decision.to_dict()) self.logger.log_agent_decision(agent.id, decision.to_dict())
# 2. Calculate movement targets and move agents # 2. Calculate movement targets and move agents
for agent, decision in decisions: for agent, decision in decisions:
action_name = decision.action.value action_name = decision.action.value
@ -175,22 +203,41 @@ class GameEngine:
target_resource=decision.target_resource.value if decision.target_resource else None, target_resource=decision.target_resource.value if decision.target_resource else None,
) )
agent.update_movement() agent.update_movement()
# 3. Execute all actions and update action indicators with results # 3. Execute all actions and update action indicators with results
for agent, decision in decisions: for agent, decision in decisions:
result = self._execute_action(agent, decision) result = self._execute_action(agent, decision)
# Complete agent action with result - this updates the indicator to show what was done # Complete agent action with result - this updates the indicator to show what was done
if result: if result:
agent.complete_action(result.success, result.message) agent.complete_action(result.success, result.message)
# Log to agent's personal history
agent.log_action(
turn=current_turn,
action_type=decision.action.value,
result=result.message,
success=result.success,
)
# Track resources produced
for res in result.resources_gained:
res_type = res.type.value
turn_log.resources_produced[res_type] = turn_log.resources_produced.get(res_type, 0) + res.quantity
self.resource_stats["produced"][res_type] = self.resource_stats["produced"].get(res_type, 0) + res.quantity
# Track resources consumed
for res in result.resources_consumed:
res_type = res.type.value
turn_log.resources_consumed[res_type] = turn_log.resources_consumed.get(res_type, 0) + res.quantity
self.resource_stats["consumed"][res_type] = self.resource_stats["consumed"].get(res_type, 0) + res.quantity
turn_log.agent_actions.append({ turn_log.agent_actions.append({
"agent_id": agent.id, "agent_id": agent.id,
"agent_name": agent.name, "agent_name": agent.name,
"decision": decision.to_dict(), "decision": decision.to_dict(),
"result": result.to_dict() if result else None, "result": result.to_dict() if result else None,
}) })
# Log agent state after action # Log agent state after action
self.logger.log_agent_after( self.logger.log_agent_after(
agent_id=agent.id, agent_id=agent.id,
@ -200,22 +247,27 @@ class GameEngine:
position=agent.position.to_dict(), position=agent.position.to_dict(),
action_result=result.to_dict() if result else {}, action_result=result.to_dict() if result else {},
) )
# 4. Resolve pending market orders (price updates) # 4. Resolve pending market orders (price updates)
self.market.update_prices(current_turn) self.market.update_prices(current_turn)
# Log market state after # Log market state after
market_orders_after = [o.to_dict() for o in self.market.get_active_orders()] market_orders_after = [o.to_dict() for o in self.market.get_active_orders()]
self.logger.log_market_state(market_orders_before, market_orders_after) self.logger.log_market_state(market_orders_before, market_orders_after)
# 5. Apply passive decay to all living agents # 5. Apply passive decay to all living agents
for agent in self.world.get_living_agents(): for agent in self.world.get_living_agents():
agent.apply_passive_decay() agent.apply_passive_decay()
# 6. Decay resources in inventories # 6. Decay resources in inventories
for agent in self.world.get_living_agents(): for agent in self.world.get_living_agents():
expired = agent.decay_inventory(current_turn) expired = agent.decay_inventory(current_turn)
# Track spoiled resources
for res in expired:
res_type = res.type.value
turn_log.resources_spoiled[res_type] = turn_log.resources_spoiled.get(res_type, 0) + res.quantity
self.resource_stats["spoiled"][res_type] = self.resource_stats["spoiled"].get(res_type, 0) + res.quantity
# 7. Mark newly dead agents as corpses (don't remove yet for visualization) # 7. Mark newly dead agents as corpses (don't remove yet for visualization)
newly_dead = self._mark_dead_agents(current_turn) newly_dead = self._mark_dead_agents(current_turn)
for dead_agent in newly_dead: for dead_agent in newly_dead:
@ -224,24 +276,24 @@ class GameEngine:
# Cancel their market orders immediately # Cancel their market orders immediately
self.market.cancel_seller_orders(dead_agent.id) self.market.cancel_seller_orders(dead_agent.id)
turn_log.deaths = [a.name for a in newly_dead] turn_log.deaths = [a.name for a in newly_dead]
# Log statistics # Log statistics
self.logger.log_statistics(self.world.get_statistics()) self.logger.log_statistics(self.world.get_statistics())
# End turn logging # End turn logging
self.logger.end_turn() self.logger.end_turn()
# 8. Advance time # 8. Advance time
self.world.advance_time() self.world.advance_time()
# 9. Check win/lose conditions (count only truly living agents, not corpses) # 9. Check win/lose conditions (count only truly living agents, not corpses)
if len(self.world.get_living_agents()) == 0: if len(self.world.get_living_agents()) == 0:
self.is_running = False self.is_running = False
self.logger.close() self.logger.close()
self.turn_logs.append(turn_log) self.turn_logs.append(turn_log)
return turn_log return turn_log
def _mark_dead_agents(self, current_turn: int) -> list[Agent]: def _mark_dead_agents(self, current_turn: int) -> list[Agent]:
"""Mark agents who just died as corpses. Returns list of newly dead agents.""" """Mark agents who just died as corpses. Returns list of newly dead agents."""
newly_dead = [] newly_dead = []
@ -255,7 +307,7 @@ class GameEngine:
agent.current_action.message = f"Died: {cause}" agent.current_action.message = f"Died: {cause}"
newly_dead.append(agent) newly_dead.append(agent)
return newly_dead return newly_dead
def _remove_old_corpses(self, current_turn: int) -> list[Agent]: def _remove_old_corpses(self, current_turn: int) -> list[Agent]:
"""Remove corpses that have been visible for one turn.""" """Remove corpses that have been visible for one turn."""
to_remove = [] to_remove = []
@ -263,18 +315,18 @@ class GameEngine:
if agent.is_corpse() and agent.death_turn < current_turn: if agent.is_corpse() and agent.death_turn < current_turn:
# Corpse has been visible for one turn, remove it # Corpse has been visible for one turn, remove it
to_remove.append(agent) to_remove.append(agent)
for agent in to_remove: for agent in to_remove:
self.world.agents.remove(agent) self.world.agents.remove(agent)
self.world.total_agents_died += 1 self.world.total_agents_died += 1
return to_remove return to_remove
def _execute_action(self, agent: Agent, decision: AIDecision) -> Optional[ActionResult]: def _execute_action(self, agent: Agent, decision: AIDecision) -> Optional[ActionResult]:
"""Execute an action for an agent.""" """Execute an action for an agent."""
action = decision.action action = decision.action
config = ACTION_CONFIG[action] config = ACTION_CONFIG[action]
# Handle different action types # Handle different action types
if action == ActionType.SLEEP: if action == ActionType.SLEEP:
agent.restore_energy(config.energy_cost) agent.restore_energy(config.energy_cost)
@ -284,7 +336,7 @@ class GameEngine:
energy_spent=-config.energy_cost, energy_spent=-config.energy_cost,
message="Sleeping soundly", message="Sleeping soundly",
) )
elif action == ActionType.REST: elif action == ActionType.REST:
agent.restore_energy(config.energy_cost) agent.restore_energy(config.energy_cost)
return ActionResult( return ActionResult(
@ -293,17 +345,25 @@ class GameEngine:
energy_spent=-config.energy_cost, energy_spent=-config.energy_cost,
message="Resting", message="Resting",
) )
elif action == ActionType.CONSUME: elif action == ActionType.CONSUME:
if decision.target_resource: if decision.target_resource:
success = agent.consume(decision.target_resource) success = agent.consume(decision.target_resource)
consumed_list = []
if success:
consumed_list.append(Resource(
type=decision.target_resource,
quantity=1,
created_turn=self.world.current_turn,
))
return ActionResult( return ActionResult(
action_type=action, action_type=action,
success=success, success=success,
resources_consumed=consumed_list,
message=f"Consumed {decision.target_resource.value}" if success else "Nothing to consume", message=f"Consumed {decision.target_resource.value}" if success else "Nothing to consume",
) )
return ActionResult(action_type=action, success=False, message="No resource specified") return ActionResult(action_type=action, success=False, message="No resource specified")
elif action == ActionType.BUILD_FIRE: elif action == ActionType.BUILD_FIRE:
if agent.has_resource(ResourceType.WOOD): if agent.has_resource(ResourceType.WOOD):
agent.remove_from_inventory(ResourceType.WOOD, 1) agent.remove_from_inventory(ResourceType.WOOD, 1)
@ -318,22 +378,23 @@ class GameEngine:
success=True, success=True,
energy_spent=abs(config.energy_cost), energy_spent=abs(config.energy_cost),
heat_gained=fire_heat, heat_gained=fire_heat,
resources_consumed=[Resource(type=ResourceType.WOOD, quantity=1, created_turn=self.world.current_turn)],
message="Built a warm fire", message="Built a warm fire",
) )
return ActionResult(action_type=action, success=False, message="No wood for fire") return ActionResult(action_type=action, success=False, message="No wood for fire")
elif action == ActionType.TRADE: elif action == ActionType.TRADE:
return self._execute_trade(agent, decision) return self._execute_trade(agent, decision)
elif action in [ActionType.HUNT, ActionType.GATHER, ActionType.CHOP_WOOD, elif action in [ActionType.HUNT, ActionType.GATHER, ActionType.CHOP_WOOD,
ActionType.GET_WATER, ActionType.WEAVE]: ActionType.GET_WATER, ActionType.WEAVE]:
return self._execute_work(agent, action, config) return self._execute_work(agent, action, config)
return ActionResult(action_type=action, success=False, message="Unknown action") return ActionResult(action_type=action, success=False, message="Unknown action")
def _execute_work(self, agent: Agent, action: ActionType, config) -> ActionResult: def _execute_work(self, agent: Agent, action: ActionType, config) -> ActionResult:
"""Execute a work action (hunting, gathering, etc.). """Execute a work action (hunting, gathering, etc.).
Skills now affect outcomes: Skills now affect outcomes:
- Hunting skill affects hunt success rate - Hunting skill affects hunt success rate
- Gathering skill affects gather output - Gathering skill affects gather output
@ -348,8 +409,9 @@ class GameEngine:
success=False, success=False,
message="Not enough energy", message="Not enough energy",
) )
# Check required materials # Check required materials
resources_consumed = []
if config.requires_resource: if config.requires_resource:
if not agent.has_resource(config.requires_resource, config.requires_quantity): if not agent.has_resource(config.requires_resource, config.requires_quantity):
agent.restore_energy(energy_cost) # Refund energy agent.restore_energy(energy_cost) # Refund energy
@ -359,12 +421,17 @@ class GameEngine:
message=f"Missing required {config.requires_resource.value}", message=f"Missing required {config.requires_resource.value}",
) )
agent.remove_from_inventory(config.requires_resource, config.requires_quantity) agent.remove_from_inventory(config.requires_resource, config.requires_quantity)
resources_consumed.append(Resource(
type=config.requires_resource,
quantity=config.requires_quantity,
created_turn=self.world.current_turn,
))
# Get relevant skill for this action # Get relevant skill for this action
skill_name = self._get_skill_for_action(action) skill_name = self._get_skill_for_action(action)
skill_value = getattr(agent.skills, skill_name, 1.0) if skill_name else 1.0 skill_value = getattr(agent.skills, skill_name, 1.0) if skill_name else 1.0
skill_modifier = get_action_skill_modifier(skill_value) skill_modifier = get_action_skill_modifier(skill_value)
# Check success chance (modified by skill) # Check success chance (modified by skill)
# Higher skill = higher effective success chance # Higher skill = higher effective success chance
effective_success_chance = min(0.98, config.success_chance * skill_modifier) effective_success_chance = min(0.98, config.success_chance * skill_modifier)
@ -379,15 +446,15 @@ class GameEngine:
energy_spent=energy_cost, energy_spent=energy_cost,
message="Action failed", message="Action failed",
) )
# Generate output (modified by skill for quantity) # Generate output (modified by skill for quantity)
resources_gained = [] resources_gained = []
if config.output_resource: if config.output_resource:
# Skill affects output quantity # Skill affects output quantity
base_quantity = random.randint(config.min_output, config.max_output) base_quantity = random.randint(config.min_output, config.max_output)
quantity = max(config.min_output, int(base_quantity * skill_modifier)) quantity = max(config.min_output, int(base_quantity * skill_modifier))
if quantity > 0: if quantity > 0:
resource = Resource( resource = Resource(
type=config.output_resource, type=config.output_resource,
@ -401,7 +468,7 @@ class GameEngine:
quantity=added, quantity=added,
created_turn=self.world.current_turn, created_turn=self.world.current_turn,
)) ))
# Secondary output (e.g., hide from hunting) - also affected by skill # Secondary output (e.g., hide from hunting) - also affected by skill
if config.secondary_output: if config.secondary_output:
base_quantity = random.randint(config.secondary_min, config.secondary_max) base_quantity = random.randint(config.secondary_min, config.secondary_max)
@ -419,24 +486,25 @@ class GameEngine:
quantity=added, quantity=added,
created_turn=self.world.current_turn, created_turn=self.world.current_turn,
)) ))
# Record action and improve skill # Record action and improve skill
agent.record_action(action.value) agent.record_action(action.value)
if skill_name: if skill_name:
agent.skills.improve(skill_name, 0.015) # Skill improves with successful use agent.skills.improve(skill_name, 0.015) # Skill improves with successful use
# Build success message with details # Build success message with details
gained_str = ", ".join(f"+{r.quantity} {r.type.value}" for r in resources_gained) gained_str = ", ".join(f"+{r.quantity} {r.type.value}" for r in resources_gained)
message = f"{action.value}: {gained_str}" if gained_str else f"{action.value} (nothing gained)" message = f"{action.value}: {gained_str}" if gained_str else f"{action.value} (nothing gained)"
return ActionResult( return ActionResult(
action_type=action, action_type=action,
success=True, success=True,
energy_spent=energy_cost, energy_spent=energy_cost,
resources_gained=resources_gained, resources_gained=resources_gained,
resources_consumed=resources_consumed,
message=message, message=message,
) )
def _get_skill_for_action(self, action: ActionType) -> Optional[str]: def _get_skill_for_action(self, action: ActionType) -> Optional[str]:
"""Get the skill name that affects a given action.""" """Get the skill name that affects a given action."""
skill_map = { skill_map = {
@ -446,22 +514,22 @@ class GameEngine:
ActionType.WEAVE: "crafting", ActionType.WEAVE: "crafting",
} }
return skill_map.get(action) return skill_map.get(action)
def _execute_trade(self, agent: Agent, decision: AIDecision) -> ActionResult: def _execute_trade(self, agent: Agent, decision: AIDecision) -> ActionResult:
"""Execute a trade action (buy, sell, or price adjustment). Supports multi-item trades. """Execute a trade action (buy, sell, or price adjustment). Supports multi-item trades.
Trading skill improves with successful trades and affects prices slightly. Trading skill improves with successful trades and affects prices slightly.
""" """
config = ACTION_CONFIG[ActionType.TRADE] config = ACTION_CONFIG[ActionType.TRADE]
# Handle price adjustments (no energy cost) # Handle price adjustments (no energy cost)
if decision.adjust_order_id and decision.new_price is not None: if decision.adjust_order_id and decision.new_price is not None:
return self._execute_price_adjustment(agent, decision) return self._execute_price_adjustment(agent, decision)
# Handle multi-item trades # Handle multi-item trades
if decision.trade_items: if decision.trade_items:
return self._execute_multi_buy(agent, decision) return self._execute_multi_buy(agent, decision)
if decision.order_id: if decision.order_id:
# Buying single item from market # Buying single item from market
result = self.market.execute_buy( result = self.market.execute_buy(
@ -470,11 +538,11 @@ class GameEngine:
quantity=decision.quantity, quantity=decision.quantity,
buyer_money=agent.money, buyer_money=agent.money,
) )
if result.success: if result.success:
# Log the trade # Log the trade
self.logger.log_trade(result.to_dict()) self.logger.log_trade(result.to_dict())
# Record sale for price history tracking # Record sale for price history tracking
self.market._record_sale( self.market._record_sale(
result.resource_type, result.resource_type,
@ -482,10 +550,14 @@ class GameEngine:
result.quantity, result.quantity,
self.world.current_turn, self.world.current_turn,
) )
# Track traded resources
res_type = result.resource_type.value
self.resource_stats["traded"][res_type] = self.resource_stats["traded"].get(res_type, 0) + result.quantity
# Deduct money from buyer # Deduct money from buyer
agent.money -= result.total_paid agent.money -= result.total_paid
# Add resources to buyer # Add resources to buyer
resource = Resource( resource = Resource(
type=result.resource_type, type=result.resource_type,
@ -493,20 +565,20 @@ class GameEngine:
created_turn=self.world.current_turn, created_turn=self.world.current_turn,
) )
agent.add_to_inventory(resource) agent.add_to_inventory(resource)
# Add money to seller and record their trade # Add money to seller and record their trade
seller = self.world.get_agent(result.seller_id) seller = self.world.get_agent(result.seller_id)
if seller: if seller:
seller.money += result.total_paid seller.money += result.total_paid
seller.record_trade(result.total_paid) seller.record_trade(result.total_paid)
seller.skills.improve("trading", 0.02) # Seller skill improves seller.skills.improve("trading", 0.02) # Seller skill improves
agent.spend_energy(abs(config.energy_cost)) agent.spend_energy(abs(config.energy_cost))
# Record buyer's trade and improve skill # Record buyer's trade and improve skill
agent.record_action("trade") agent.record_action("trade")
agent.skills.improve("trading", 0.01) # Buyer skill improves less agent.skills.improve("trading", 0.01) # Buyer skill improves less
return ActionResult( return ActionResult(
action_type=ActionType.TRADE, action_type=ActionType.TRADE,
success=True, success=True,
@ -520,12 +592,12 @@ class GameEngine:
success=False, success=False,
message=result.message, message=result.message,
) )
elif decision.target_resource and decision.quantity > 0: elif decision.target_resource and decision.quantity > 0:
# Selling to market (listing) # Selling to market (listing)
if agent.has_resource(decision.target_resource, decision.quantity): if agent.has_resource(decision.target_resource, decision.quantity):
agent.remove_from_inventory(decision.target_resource, decision.quantity) agent.remove_from_inventory(decision.target_resource, decision.quantity)
order = self.market.place_order( order = self.market.place_order(
seller_id=agent.id, seller_id=agent.id,
resource_type=decision.target_resource, resource_type=decision.target_resource,
@ -533,10 +605,10 @@ class GameEngine:
price_per_unit=decision.price, price_per_unit=decision.price,
current_turn=self.world.current_turn, current_turn=self.world.current_turn,
) )
agent.spend_energy(abs(config.energy_cost)) agent.spend_energy(abs(config.energy_cost))
agent.record_action("trade") # Track listing action agent.record_action("trade") # Track listing action
return ActionResult( return ActionResult(
action_type=ActionType.TRADE, action_type=ActionType.TRADE,
success=True, success=True,
@ -549,13 +621,13 @@ class GameEngine:
success=False, success=False,
message="Not enough resources to sell", message="Not enough resources to sell",
) )
return ActionResult( return ActionResult(
action_type=ActionType.TRADE, action_type=ActionType.TRADE,
success=False, success=False,
message="Invalid trade parameters", message="Invalid trade parameters",
) )
def _execute_price_adjustment(self, agent: Agent, decision: AIDecision) -> ActionResult: def _execute_price_adjustment(self, agent: Agent, decision: AIDecision) -> ActionResult:
"""Execute a price adjustment on an existing order (no energy cost).""" """Execute a price adjustment on an existing order (no energy cost)."""
success = self.market.adjust_order_price( success = self.market.adjust_order_price(
@ -564,7 +636,7 @@ class GameEngine:
new_price=decision.new_price, new_price=decision.new_price,
current_turn=self.world.current_turn, current_turn=self.world.current_turn,
) )
if success: if success:
return ActionResult( return ActionResult(
action_type=ActionType.TRADE, action_type=ActionType.TRADE,
@ -578,40 +650,44 @@ class GameEngine:
success=False, success=False,
message="Failed to adjust price", message="Failed to adjust price",
) )
def _execute_multi_buy(self, agent: Agent, decision: AIDecision) -> ActionResult: def _execute_multi_buy(self, agent: Agent, decision: AIDecision) -> ActionResult:
"""Execute a multi-item buy trade.""" """Execute a multi-item buy trade."""
config = ACTION_CONFIG[ActionType.TRADE] config = ACTION_CONFIG[ActionType.TRADE]
# Build list of purchases # Build list of purchases
purchases = [(item.order_id, item.quantity) for item in decision.trade_items] purchases = [(item.order_id, item.quantity) for item in decision.trade_items]
# Execute all purchases # Execute all purchases
results = self.market.execute_multi_buy( results = self.market.execute_multi_buy(
buyer_id=agent.id, buyer_id=agent.id,
purchases=purchases, purchases=purchases,
buyer_money=agent.money, buyer_money=agent.money,
) )
# Process results # Process results
total_paid = 0 total_paid = 0
resources_gained = [] resources_gained = []
items_bought = [] items_bought = []
for result in results: for result in results:
if result.success: if result.success:
self.logger.log_trade(result.to_dict()) self.logger.log_trade(result.to_dict())
agent.money -= result.total_paid agent.money -= result.total_paid
total_paid += result.total_paid total_paid += result.total_paid
# Record sale for price history # Record sale for price history
self.market._record_sale( self.market._record_sale(
result.resource_type, result.resource_type,
result.total_paid // result.quantity, result.total_paid // result.quantity,
result.quantity, result.quantity,
self.world.current_turn self.world.current_turn
) )
# Track traded resources
res_type = result.resource_type.value
self.resource_stats["traded"][res_type] = self.resource_stats["traded"].get(res_type, 0) + result.quantity
resource = Resource( resource = Resource(
type=result.resource_type, type=result.resource_type,
quantity=result.quantity, quantity=result.quantity,
@ -620,12 +696,12 @@ class GameEngine:
agent.add_to_inventory(resource) agent.add_to_inventory(resource)
resources_gained.append(resource) resources_gained.append(resource)
items_bought.append(f"{result.quantity} {result.resource_type.value}") items_bought.append(f"{result.quantity} {result.resource_type.value}")
# Add money to seller # Add money to seller
seller = self.world.get_agent(result.seller_id) seller = self.world.get_agent(result.seller_id)
if seller: if seller:
seller.money += result.total_paid seller.money += result.total_paid
if resources_gained: if resources_gained:
agent.spend_energy(abs(config.energy_cost)) agent.spend_energy(abs(config.energy_cost))
message = f"Bought {', '.join(items_bought)} for {total_paid}c" message = f"Bought {', '.join(items_bought)} for {total_paid}c"
@ -642,38 +718,38 @@ class GameEngine:
success=False, success=False,
message="Failed to buy any items", message="Failed to buy any items",
) )
def set_mode(self, mode: SimulationMode) -> None: def set_mode(self, mode: SimulationMode) -> None:
"""Set the simulation mode.""" """Set the simulation mode."""
if mode == self.mode: if mode == self.mode:
return return
if mode == SimulationMode.AUTO: if mode == SimulationMode.AUTO:
self._start_auto_mode() self._start_auto_mode()
else: else:
self._stop_auto_mode() self._stop_auto_mode()
self.mode = mode self.mode = mode
def _start_auto_mode(self) -> None: def _start_auto_mode(self) -> None:
"""Start automatic step advancement.""" """Start automatic step advancement."""
self._stop_event.clear() self._stop_event.clear()
def auto_step(): def auto_step():
while not self._stop_event.is_set() and self.is_running: while not self._stop_event.is_set() and self.is_running:
self.next_step() self.next_step()
time.sleep(self.auto_step_interval) time.sleep(self.auto_step_interval)
self._auto_thread = threading.Thread(target=auto_step, daemon=True) self._auto_thread = threading.Thread(target=auto_step, daemon=True)
self._auto_thread.start() self._auto_thread.start()
def _stop_auto_mode(self) -> None: def _stop_auto_mode(self) -> None:
"""Stop automatic step advancement.""" """Stop automatic step advancement."""
self._stop_event.set() self._stop_event.set()
if self._auto_thread: if self._auto_thread:
self._auto_thread.join(timeout=2.0) self._auto_thread.join(timeout=2.0)
self._auto_thread = None self._auto_thread = None
def get_state(self) -> dict: def get_state(self) -> dict:
"""Get the full simulation state for API.""" """Get the full simulation state for API."""
return { return {
@ -684,6 +760,31 @@ class GameEngine:
"recent_logs": [ "recent_logs": [
log.to_dict() for log in self.turn_logs[-5:] log.to_dict() for log in self.turn_logs[-5:]
], ],
"resource_stats": self._get_resource_stats(),
}
def _get_resource_stats(self) -> dict:
"""Get comprehensive resource statistics."""
# Calculate current inventory totals
in_inventory = {}
for agent in self.world.get_living_agents():
for res in agent.inventory:
res_type = res.type.value
in_inventory[res_type] = in_inventory.get(res_type, 0) + res.quantity
# Calculate current market totals
in_market = {}
for order in self.market.get_active_orders():
res_type = order.resource_type.value
in_market[res_type] = in_market.get(res_type, 0) + order.quantity
return {
"produced": self.resource_stats["produced"].copy(),
"consumed": self.resource_stats["consumed"].copy(),
"spoiled": self.resource_stats["spoiled"].copy(),
"traded": self.resource_stats["traded"].copy(),
"in_inventory": in_inventory,
"in_market": in_market,
} }

View File

@ -40,11 +40,11 @@ class Position:
"""2D position on the map (floating point for smooth movement).""" """2D position on the map (floating point for smooth movement)."""
x: float = 0.0 x: float = 0.0
y: float = 0.0 y: float = 0.0
def distance_to(self, other: "Position") -> float: def distance_to(self, other: "Position") -> float:
"""Calculate distance to another position.""" """Calculate distance to another position."""
return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
def move_towards(self, target: "Position", speed: float = 0.5) -> bool: def move_towards(self, target: "Position", speed: float = 0.5) -> bool:
"""Move towards target position. Returns True if reached.""" """Move towards target position. Returns True if reached."""
dist = self.distance_to(target) dist = self.distance_to(target)
@ -52,19 +52,19 @@ class Position:
self.x = target.x self.x = target.x
self.y = target.y self.y = target.y
return True return True
# Calculate direction # Calculate direction
dx = target.x - self.x dx = target.x - self.x
dy = target.y - self.y dy = target.y - self.y
# Normalize and apply speed # Normalize and apply speed
self.x += (dx / dist) * speed self.x += (dx / dist) * speed
self.y += (dy / dist) * speed self.y += (dy / dist) * speed
return False return False
def to_dict(self) -> dict: def to_dict(self) -> dict:
return {"x": round(self.x, 2), "y": round(self.y, 2)} return {"x": round(self.x, 2), "y": round(self.y, 2)}
def copy(self) -> "Position": def copy(self) -> "Position":
return Position(self.x, self.y) return Position(self.x, self.y)
@ -72,7 +72,7 @@ class Position:
@dataclass @dataclass
class AgentStats: class AgentStats:
"""Vital statistics for an agent. """Vital statistics for an agent.
Values are loaded from config.json. Default values are used as fallback. Values are loaded from config.json. Default values are used as fallback.
""" """
# Current values - defaults will be overwritten by factory function # Current values - defaults will be overwritten by factory function
@ -80,32 +80,32 @@ class AgentStats:
hunger: int = field(default=80) hunger: int = field(default=80)
thirst: int = field(default=70) thirst: int = field(default=70)
heat: int = field(default=100) heat: int = field(default=100)
# Maximum values - loaded from config # Maximum values - loaded from config
MAX_ENERGY: int = field(default=50) MAX_ENERGY: int = field(default=50)
MAX_HUNGER: int = field(default=100) MAX_HUNGER: int = field(default=100)
MAX_THIRST: int = field(default=100) MAX_THIRST: int = field(default=100)
MAX_HEAT: int = field(default=100) MAX_HEAT: int = field(default=100)
# Passive decay rates per turn - loaded from config # Passive decay rates per turn - loaded from config
ENERGY_DECAY: int = field(default=1) ENERGY_DECAY: int = field(default=1)
HUNGER_DECAY: int = field(default=2) HUNGER_DECAY: int = field(default=2)
THIRST_DECAY: int = field(default=3) THIRST_DECAY: int = field(default=3)
HEAT_DECAY: int = field(default=2) HEAT_DECAY: int = field(default=2)
# Critical threshold - loaded from config # Critical threshold - loaded from config
CRITICAL_THRESHOLD: float = field(default=0.25) CRITICAL_THRESHOLD: float = field(default=0.25)
def apply_passive_decay(self, has_clothes: bool = False) -> None: def apply_passive_decay(self, has_clothes: bool = False) -> None:
"""Apply passive stat decay each turn.""" """Apply passive stat decay each turn."""
self.energy = max(0, self.energy - self.ENERGY_DECAY) self.energy = max(0, self.energy - self.ENERGY_DECAY)
self.hunger = max(0, self.hunger - self.HUNGER_DECAY) self.hunger = max(0, self.hunger - self.HUNGER_DECAY)
self.thirst = max(0, self.thirst - self.THIRST_DECAY) self.thirst = max(0, self.thirst - self.THIRST_DECAY)
# Clothes reduce heat loss by 50% # Clothes reduce heat loss by 50%
heat_decay = self.HEAT_DECAY // 2 if has_clothes else self.HEAT_DECAY heat_decay = self.HEAT_DECAY // 2 if has_clothes else self.HEAT_DECAY
self.heat = max(0, self.heat - heat_decay) self.heat = max(0, self.heat - heat_decay)
def is_critical(self) -> bool: def is_critical(self) -> bool:
"""Check if any vital stat is below critical threshold.""" """Check if any vital stat is below critical threshold."""
threshold_hunger = int(self.MAX_HUNGER * self.CRITICAL_THRESHOLD) threshold_hunger = int(self.MAX_HUNGER * self.CRITICAL_THRESHOLD)
@ -116,13 +116,13 @@ class AgentStats:
self.thirst < threshold_thirst or self.thirst < threshold_thirst or
self.heat < threshold_heat self.heat < threshold_heat
) )
def get_critical_stat(self) -> Optional[str]: def get_critical_stat(self) -> Optional[str]:
"""Get the name of the most critical stat, if any.""" """Get the name of the most critical stat, if any."""
threshold_thirst = int(self.MAX_THIRST * self.CRITICAL_THRESHOLD) threshold_thirst = int(self.MAX_THIRST * self.CRITICAL_THRESHOLD)
threshold_hunger = int(self.MAX_HUNGER * self.CRITICAL_THRESHOLD) threshold_hunger = int(self.MAX_HUNGER * self.CRITICAL_THRESHOLD)
threshold_heat = int(self.MAX_HEAT * self.CRITICAL_THRESHOLD) threshold_heat = int(self.MAX_HEAT * self.CRITICAL_THRESHOLD)
if self.thirst < threshold_thirst: if self.thirst < threshold_thirst:
return "thirst" return "thirst"
if self.hunger < threshold_hunger: if self.hunger < threshold_hunger:
@ -130,11 +130,11 @@ class AgentStats:
if self.heat < threshold_heat: if self.heat < threshold_heat:
return "heat" return "heat"
return None return None
def can_work(self, energy_required: int) -> bool: def can_work(self, energy_required: int) -> bool:
"""Check if agent has enough energy to perform an action.""" """Check if agent has enough energy to perform an action."""
return self.energy >= abs(energy_required) return self.energy >= abs(energy_required)
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
"energy": self.energy, "energy": self.energy,
@ -177,7 +177,7 @@ class AgentAction:
progress: float = 0.0 # 0.0 to 1.0 progress: float = 0.0 # 0.0 to 1.0
is_moving: bool = False is_moving: bool = False
message: str = "" message: str = ""
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
"action_type": self.action_type, "action_type": self.action_type,
@ -213,7 +213,7 @@ def _get_world_config():
@dataclass @dataclass
class Agent: class Agent:
"""An agent in the village simulation. """An agent in the village simulation.
Stats, inventory slots, and starting money are loaded from config.json. Stats, inventory slots, and starting money are loaded from config.json.
Each agent now has unique personality traits and skills that create Each agent now has unique personality traits and skills that create
emergent behaviors and professions. emergent behaviors and professions.
@ -225,47 +225,51 @@ class Agent:
stats: AgentStats = field(default_factory=create_agent_stats) stats: AgentStats = field(default_factory=create_agent_stats)
inventory: list[Resource] = field(default_factory=list) inventory: list[Resource] = field(default_factory=list)
money: int = field(default=-1) # -1 signals to use config value money: int = field(default=-1) # -1 signals to use config value
# Personality and skills - create agent diversity # Personality and skills - create agent diversity
personality: PersonalityTraits = field(default_factory=PersonalityTraits) personality: PersonalityTraits = field(default_factory=PersonalityTraits)
skills: Skills = field(default_factory=Skills) skills: Skills = field(default_factory=Skills)
# Movement and action tracking # Movement and action tracking
home_position: Position = field(default_factory=Position) home_position: Position = field(default_factory=Position)
current_action: AgentAction = field(default_factory=AgentAction) current_action: AgentAction = field(default_factory=AgentAction)
last_action_result: str = "" last_action_result: str = ""
# Death tracking for corpse visualization # Death tracking for corpse visualization
death_turn: int = -1 # Turn when agent died, -1 if alive death_turn: int = -1 # Turn when agent died, -1 if alive
death_reason: str = "" # Cause of death death_reason: str = "" # Cause of death
# Statistics tracking for profession determination # Statistics tracking for profession determination
actions_performed: dict = field(default_factory=lambda: { actions_performed: dict = field(default_factory=lambda: {
"hunt": 0, "gather": 0, "chop_wood": 0, "trade": 0, "craft": 0 "hunt": 0, "gather": 0, "chop_wood": 0, "trade": 0, "craft": 0
}) })
total_trades_completed: int = 0 total_trades_completed: int = 0
total_money_earned: int = 0 total_money_earned: int = 0
# Personal action log (recent actions with results)
action_history: list = field(default_factory=list)
MAX_HISTORY_SIZE: int = 20
# Configuration - loaded from config # Configuration - loaded from config
INVENTORY_SLOTS: int = field(default=-1) # -1 signals to use config value INVENTORY_SLOTS: int = field(default=-1) # -1 signals to use config value
MOVE_SPEED: float = 0.8 # Grid cells per turn MOVE_SPEED: float = 0.8 # Grid cells per turn
def __post_init__(self): def __post_init__(self):
if not self.name: if not self.name:
self.name = f"Agent_{self.id}" self.name = f"Agent_{self.id}"
# Set home position to initial position # Set home position to initial position
self.home_position = self.position.copy() self.home_position = self.position.copy()
# Load config values if defaults were used # Load config values if defaults were used
config = _get_world_config() config = _get_world_config()
if self.money == -1: if self.money == -1:
self.money = config.starting_money self.money = config.starting_money
if self.INVENTORY_SLOTS == -1: if self.INVENTORY_SLOTS == -1:
self.INVENTORY_SLOTS = config.inventory_slots self.INVENTORY_SLOTS = config.inventory_slots
# Update profession based on personality and skills # Update profession based on personality and skills
self._update_profession() self._update_profession()
def _update_profession(self) -> None: def _update_profession(self) -> None:
"""Update profession based on personality and skills.""" """Update profession based on personality and skills."""
prof_type = determine_profession(self.personality, self.skills) prof_type = determine_profession(self.personality, self.skills)
@ -277,18 +281,31 @@ class Agent:
ProfessionType.GENERALIST: Profession.VILLAGER, ProfessionType.GENERALIST: Profession.VILLAGER,
} }
self.profession = profession_map.get(prof_type, Profession.VILLAGER) self.profession = profession_map.get(prof_type, Profession.VILLAGER)
def record_action(self, action_type: str) -> None: def record_action(self, action_type: str) -> None:
"""Record an action for profession tracking.""" """Record an action for profession tracking."""
if action_type in self.actions_performed: if action_type in self.actions_performed:
self.actions_performed[action_type] += 1 self.actions_performed[action_type] += 1
def log_action(self, turn: int, action_type: str, result: str, success: bool = True) -> None:
"""Add an action to the agent's personal history log."""
entry = {
"turn": turn,
"action": action_type,
"result": result,
"success": success,
}
self.action_history.append(entry)
# Keep only recent history
if len(self.action_history) > self.MAX_HISTORY_SIZE:
self.action_history = self.action_history[-self.MAX_HISTORY_SIZE:]
def record_trade(self, money_earned: int) -> None: def record_trade(self, money_earned: int) -> None:
"""Record a completed trade for statistics.""" """Record a completed trade for statistics."""
self.total_trades_completed += 1 self.total_trades_completed += 1
if money_earned > 0: if money_earned > 0:
self.total_money_earned += money_earned self.total_money_earned += money_earned
def is_alive(self) -> bool: def is_alive(self) -> bool:
"""Check if the agent is still alive.""" """Check if the agent is still alive."""
return ( return (
@ -296,28 +313,28 @@ class Agent:
self.stats.thirst > 0 and self.stats.thirst > 0 and
self.stats.heat > 0 self.stats.heat > 0
) )
def is_corpse(self) -> bool: def is_corpse(self) -> bool:
"""Check if this agent is a corpse (died but still visible).""" """Check if this agent is a corpse (died but still visible)."""
return self.death_turn >= 0 return self.death_turn >= 0
def can_act(self) -> bool: def can_act(self) -> bool:
"""Check if agent can perform active actions.""" """Check if agent can perform active actions."""
return self.is_alive() and self.stats.energy > 0 return self.is_alive() and self.stats.energy > 0
def has_clothes(self) -> bool: def has_clothes(self) -> bool:
"""Check if agent has clothes equipped.""" """Check if agent has clothes equipped."""
return any(r.type == ResourceType.CLOTHES for r in self.inventory) return any(r.type == ResourceType.CLOTHES for r in self.inventory)
def inventory_space(self) -> int: def inventory_space(self) -> int:
"""Get remaining inventory slots.""" """Get remaining inventory slots."""
total_items = sum(r.quantity for r in self.inventory) total_items = sum(r.quantity for r in self.inventory)
return max(0, self.INVENTORY_SLOTS - total_items) return max(0, self.INVENTORY_SLOTS - total_items)
def inventory_full(self) -> bool: def inventory_full(self) -> bool:
"""Check if inventory is full.""" """Check if inventory is full."""
return self.inventory_space() <= 0 return self.inventory_space() <= 0
def set_action( def set_action(
self, self,
action_type: str, action_type: str,
@ -328,7 +345,7 @@ class Agent:
) -> None: ) -> None:
"""Set the current action and calculate target position.""" """Set the current action and calculate target position."""
location = ACTION_LOCATIONS.get(action_type, {"zone": "current", "offset_range": (0, 0)}) location = ACTION_LOCATIONS.get(action_type, {"zone": "current", "offset_range": (0, 0)})
if location["zone"] == "current": if location["zone"] == "current":
# Stay in place # Stay in place
target = self.position.copy() target = self.position.copy()
@ -339,14 +356,14 @@ class Agent:
offset_min = float(offset_range[0]) offset_min = float(offset_range[0])
offset_max = float(offset_range[1]) offset_max = float(offset_range[1])
target_x = world_width * random.uniform(offset_min, offset_max) target_x = world_width * random.uniform(offset_min, offset_max)
# Keep y position somewhat consistent but allow some variation # Keep y position somewhat consistent but allow some variation
target_y = self.home_position.y + random.uniform(-2, 2) target_y = self.home_position.y + random.uniform(-2, 2)
target_y = max(0.5, min(world_height - 0.5, target_y)) target_y = max(0.5, min(world_height - 0.5, target_y))
target = Position(target_x, target_y) target = Position(target_x, target_y)
is_moving = self.position.distance_to(target) > 0.5 is_moving = self.position.distance_to(target) > 0.5
self.current_action = AgentAction( self.current_action = AgentAction(
action_type=action_type, action_type=action_type,
target_position=target, target_position=target,
@ -355,7 +372,7 @@ class Agent:
is_moving=is_moving, is_moving=is_moving,
message=message, message=message,
) )
def update_movement(self) -> None: def update_movement(self) -> None:
"""Update agent position moving towards target.""" """Update agent position moving towards target."""
if self.current_action.target_position and self.current_action.is_moving: if self.current_action.target_position and self.current_action.is_moving:
@ -366,28 +383,28 @@ class Agent:
if reached: if reached:
self.current_action.is_moving = False self.current_action.is_moving = False
self.current_action.progress = 0.5 # At location, doing action self.current_action.progress = 0.5 # At location, doing action
def complete_action(self, success: bool, message: str) -> None: def complete_action(self, success: bool, message: str) -> None:
"""Mark current action as complete.""" """Mark current action as complete."""
self.current_action.progress = 1.0 self.current_action.progress = 1.0
self.current_action.is_moving = False self.current_action.is_moving = False
self.last_action_result = message self.last_action_result = message
self.current_action.message = message if success else f"Failed: {message}" self.current_action.message = message if success else f"Failed: {message}"
def add_to_inventory(self, resource: Resource) -> int: def add_to_inventory(self, resource: Resource) -> int:
"""Add resource to inventory, returns quantity actually added.""" """Add resource to inventory, returns quantity actually added."""
space = self.inventory_space() space = self.inventory_space()
if space <= 0: if space <= 0:
return 0 return 0
quantity_to_add = min(resource.quantity, space) quantity_to_add = min(resource.quantity, space)
# Try to stack with existing resource of same type # Try to stack with existing resource of same type
for existing in self.inventory: for existing in self.inventory:
if existing.type == resource.type: if existing.type == resource.type:
existing.quantity += quantity_to_add existing.quantity += quantity_to_add
return quantity_to_add return quantity_to_add
# Add as new stack # Add as new stack
new_resource = Resource( new_resource = Resource(
type=resource.type, type=resource.type,
@ -396,7 +413,7 @@ class Agent:
) )
self.inventory.append(new_resource) self.inventory.append(new_resource)
return quantity_to_add return quantity_to_add
def remove_from_inventory(self, resource_type: ResourceType, quantity: int = 1) -> int: def remove_from_inventory(self, resource_type: ResourceType, quantity: int = 1) -> int:
"""Remove resource from inventory, returns quantity actually removed.""" """Remove resource from inventory, returns quantity actually removed."""
removed = 0 removed = 0
@ -405,31 +422,31 @@ class Agent:
take = min(resource.quantity, quantity - removed) take = min(resource.quantity, quantity - removed)
resource.quantity -= take resource.quantity -= take
removed += take removed += take
if resource.quantity <= 0: if resource.quantity <= 0:
self.inventory.remove(resource) self.inventory.remove(resource)
if removed >= quantity: if removed >= quantity:
break break
return removed return removed
def get_resource_count(self, resource_type: ResourceType) -> int: def get_resource_count(self, resource_type: ResourceType) -> int:
"""Get total count of a resource type in inventory.""" """Get total count of a resource type in inventory."""
return sum( return sum(
r.quantity for r in self.inventory r.quantity for r in self.inventory
if r.type == resource_type if r.type == resource_type
) )
def has_resource(self, resource_type: ResourceType, quantity: int = 1) -> bool: def has_resource(self, resource_type: ResourceType, quantity: int = 1) -> bool:
"""Check if agent has at least the specified quantity of a resource.""" """Check if agent has at least the specified quantity of a resource."""
return self.get_resource_count(resource_type) >= quantity return self.get_resource_count(resource_type) >= quantity
def consume(self, resource_type: ResourceType) -> bool: def consume(self, resource_type: ResourceType) -> bool:
"""Consume a resource from inventory and apply its effects.""" """Consume a resource from inventory and apply its effects."""
if not self.has_resource(resource_type, 1): if not self.has_resource(resource_type, 1):
return False return False
effect = RESOURCE_EFFECTS[resource_type] effect = RESOURCE_EFFECTS[resource_type]
self.stats.hunger = min( self.stats.hunger = min(
self.stats.MAX_HUNGER, self.stats.MAX_HUNGER,
@ -447,25 +464,25 @@ class Agent:
self.stats.MAX_ENERGY, self.stats.MAX_ENERGY,
self.stats.energy + effect.energy self.stats.energy + effect.energy
) )
self.remove_from_inventory(resource_type, 1) self.remove_from_inventory(resource_type, 1)
return True return True
def apply_heat(self, amount: int) -> None: def apply_heat(self, amount: int) -> None:
"""Apply heat from a fire.""" """Apply heat from a fire."""
self.stats.heat = min(self.stats.MAX_HEAT, self.stats.heat + amount) self.stats.heat = min(self.stats.MAX_HEAT, self.stats.heat + amount)
def restore_energy(self, amount: int) -> None: def restore_energy(self, amount: int) -> None:
"""Restore energy (from sleep/rest).""" """Restore energy (from sleep/rest)."""
self.stats.energy = min(self.stats.MAX_ENERGY, self.stats.energy + amount) self.stats.energy = min(self.stats.MAX_ENERGY, self.stats.energy + amount)
def spend_energy(self, amount: int) -> bool: def spend_energy(self, amount: int) -> bool:
"""Spend energy on an action. Returns False if not enough energy.""" """Spend energy on an action. Returns False if not enough energy."""
if self.stats.energy < amount: if self.stats.energy < amount:
return False return False
self.stats.energy -= amount self.stats.energy -= amount
return True return True
def decay_inventory(self, current_turn: int) -> list[Resource]: def decay_inventory(self, current_turn: int) -> list[Resource]:
"""Remove expired resources from inventory. Returns list of removed resources.""" """Remove expired resources from inventory. Returns list of removed resources."""
expired = [] expired = []
@ -474,21 +491,21 @@ class Agent:
expired.append(resource) expired.append(resource)
self.inventory.remove(resource) self.inventory.remove(resource)
return expired return expired
def apply_passive_decay(self) -> None: def apply_passive_decay(self) -> None:
"""Apply passive stat decay for this turn.""" """Apply passive stat decay for this turn."""
self.stats.apply_passive_decay(has_clothes=self.has_clothes()) self.stats.apply_passive_decay(has_clothes=self.has_clothes())
def mark_dead(self, turn: int, reason: str) -> None: def mark_dead(self, turn: int, reason: str) -> None:
"""Mark this agent as dead.""" """Mark this agent as dead."""
self.death_turn = turn self.death_turn = turn
self.death_reason = reason self.death_reason = reason
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert to dictionary for API serialization.""" """Convert to dictionary for API serialization."""
# Update profession before serializing # Update profession before serializing
self._update_profession() self._update_profession()
return { return {
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
@ -511,4 +528,6 @@ class Agent:
"actions_performed": self.actions_performed.copy(), "actions_performed": self.actions_performed.copy(),
"total_trades": self.total_trades_completed, "total_trades": self.total_trades_completed,
"total_money_earned": self.total_money_earned, "total_money_earned": self.total_money_earned,
# Personal action history
"action_history": self.action_history.copy(),
} }

View File

@ -1,12 +1,17 @@
"""FastAPI entry point for the Village Simulation backend.""" """FastAPI entry point for the Village Simulation backend."""
import os
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from backend.api.routes import router from backend.api.routes import router
from backend.core.engine import get_engine from backend.core.engine import get_engine
# Path to web frontend
WEB_FRONTEND_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "web_frontend")
# Create FastAPI app # Create FastAPI app
app = FastAPI( app = FastAPI(
title="Village Simulation API", title="Village Simulation API",
@ -33,7 +38,7 @@ app.include_router(router, prefix="/api", tags=["simulation"])
async def startup_event(): async def startup_event():
"""Initialize the simulation on startup with config.json values.""" """Initialize the simulation on startup with config.json values."""
from backend.config import get_config from backend.config import get_config
config = get_config() config = get_config()
engine = get_engine() engine = get_engine()
# Use reset() which automatically loads config values # Use reset() which automatically loads config values
@ -48,6 +53,7 @@ def root():
"name": "Village Simulation API", "name": "Village Simulation API",
"version": "1.0.0", "version": "1.0.0",
"docs": "/docs", "docs": "/docs",
"web_frontend": "/web/",
"status": "running", "status": "running",
} }
@ -63,6 +69,14 @@ def health_check():
} }
# ============== Web Frontend Static Files ==============
# Mount static files for web frontend
# Access at http://localhost:8000/web/
if os.path.exists(WEB_FRONTEND_PATH):
app.mount("/web", StaticFiles(directory=WEB_FRONTEND_PATH, html=True), name="web_frontend")
def main(): def main():
"""Run the server.""" """Run the server."""
uvicorn.run( uvicorn.run(

279
web_frontend/index.html Normal file
View File

@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VillSim - Village Economy Simulation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<div id="app">
<header id="header">
<div class="header-left">
<h1 class="title">VillSim</h1>
<span class="subtitle">Village Economy Simulation</span>
</div>
<div class="header-center">
<div class="time-display">
<span id="day-display">Day 1</span>
<span class="separator">·</span>
<span id="time-display">☀️ Day</span>
<span class="separator">·</span>
<span id="turn-display">Turn 0</span>
</div>
</div>
<div class="header-right">
<div class="connection-status" id="connection-status">
<span class="status-dot disconnected"></span>
<span class="status-text">Disconnected</span>
</div>
</div>
</header>
<main id="main-content">
<aside id="left-panel" class="panel">
<div class="panel-section">
<h3 class="section-title">Population</h3>
<div class="stat-grid" id="population-stats">
<div class="stat-item">
<span class="stat-value" id="stat-alive">0</span>
<span class="stat-label">Alive</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-dead">0</span>
<span class="stat-label">Dead</span>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Professions</h3>
<div class="profession-list" id="profession-list">
<!-- Filled by JS -->
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Economy</h3>
<div class="economy-stats" id="economy-stats">
<div class="economy-item">
<span class="economy-label">Money in Circulation</span>
<span class="economy-value" id="stat-money">0</span>
</div>
</div>
</div>
</aside>
<div id="game-container">
<!-- Phaser canvas will be inserted here -->
</div>
<aside id="right-panel" class="panel">
<div class="panel-section agent-section scrollable-section">
<h3 class="section-title">Selected Agent</h3>
<div id="agent-details" class="agent-details">
<p class="no-selection">Click an agent to view details</p>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Market Prices</h3>
<div class="market-prices" id="market-prices">
<!-- Filled by JS -->
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Activity Log</h3>
<div class="activity-log" id="activity-log">
<!-- Filled by JS -->
</div>
</div>
</aside>
</main>
<footer id="footer">
<div class="controls">
<button id="btn-initialize" class="btn btn-secondary" title="Reset Simulation">
<span class="btn-icon"></span> Reset
</button>
<button id="btn-step" class="btn btn-primary" title="Advance one turn">
<span class="btn-icon"></span> Step
</button>
<button id="btn-auto" class="btn btn-toggle" title="Toggle auto mode">
<span class="btn-icon"></span> Auto
</button>
<button id="btn-stats" class="btn btn-secondary" title="View Statistics">
<span class="btn-icon">📊</span> Stats
</button>
</div>
<div class="speed-control">
<label for="speed-slider">Speed</label>
<input type="range" id="speed-slider" min="50" max="1000" value="150" step="50">
<span id="speed-display">150ms</span>
</div>
</footer>
<!-- Stats Screen (Full View) -->
<div id="stats-screen" class="stats-screen hidden">
<div class="stats-header">
<div class="stats-header-left">
<h2>📊 Simulation Statistics</h2>
<span class="stats-subtitle">Real-time metrics and charts</span>
</div>
<div class="stats-header-center">
<div class="stats-tabs">
<button class="tab-btn active" data-tab="prices">Prices</button>
<button class="tab-btn" data-tab="wealth">Wealth</button>
<button class="tab-btn" data-tab="population">Population</button>
<button class="tab-btn" data-tab="professions">Professions</button>
<button class="tab-btn" data-tab="resources">Resources</button>
<button class="tab-btn" data-tab="market">Market</button>
<button class="tab-btn" data-tab="agents">Agents</button>
</div>
</div>
<div class="stats-header-right">
<button id="btn-close-stats" class="btn btn-secondary">
<span class="btn-icon"></span> Back to Game
</button>
</div>
</div>
<div class="stats-body">
<!-- Prices Tab -->
<div id="tab-prices" class="tab-panel active">
<div class="chart-wrapper">
<canvas id="chart-prices"></canvas>
</div>
</div>
<!-- Wealth Tab -->
<div id="tab-wealth" class="tab-panel">
<div class="chart-grid three-col">
<div class="chart-wrapper">
<canvas id="chart-wealth-dist"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-wealth-prof"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-wealth-time"></canvas>
</div>
</div>
</div>
<!-- Population Tab -->
<div id="tab-population" class="tab-panel">
<div class="chart-wrapper">
<canvas id="chart-population"></canvas>
</div>
</div>
<!-- Professions Tab -->
<div id="tab-professions" class="tab-panel">
<div class="chart-grid two-col">
<div class="chart-wrapper">
<canvas id="chart-prof-pie"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-prof-time"></canvas>
</div>
</div>
</div>
<!-- Resources Tab -->
<div id="tab-resources" class="tab-panel">
<div class="chart-grid four-col">
<div class="chart-wrapper">
<canvas id="chart-res-produced"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-res-consumed"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-res-spoiled"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-res-stock"></canvas>
</div>
</div>
<div class="chart-grid four-col" style="margin-top: 16px;">
<div class="chart-wrapper">
<canvas id="chart-res-cum-produced"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-res-cum-consumed"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-res-cum-spoiled"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-res-cum-traded"></canvas>
</div>
</div>
</div>
<!-- Market Tab -->
<div id="tab-market" class="tab-panel">
<div class="chart-grid two-col">
<div class="chart-wrapper">
<canvas id="chart-market-supply"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-market-activity"></canvas>
</div>
</div>
</div>
<!-- Agents Tab -->
<div id="tab-agents" class="tab-panel">
<div class="chart-grid four-col">
<div class="chart-wrapper">
<canvas id="chart-stat-energy"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-stat-hunger"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-stat-thirst"></canvas>
</div>
<div class="chart-wrapper">
<canvas id="chart-stat-heat"></canvas>
</div>
</div>
</div>
</div>
<div class="stats-footer">
<div class="stats-summary-bar">
<div class="summary-item">
<span class="summary-label">Turn</span>
<span class="summary-value" id="stats-turn">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Living</span>
<span class="summary-value highlight" id="stats-living">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Deaths</span>
<span class="summary-value danger" id="stats-deaths">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Total Gold</span>
<span class="summary-value gold" id="stats-gold">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Avg Wealth</span>
<span class="summary-value" id="stats-avg-wealth">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Gini Index</span>
<span class="summary-value" id="stats-gini">0.00</span>
</div>
</div>
</div>
</div>
</div>
<!-- Load game modules -->
<script type="module" src="src/main.js"></script>
</body>
</html>

132
web_frontend/src/api.js Normal file
View File

@ -0,0 +1,132 @@
/**
* VillSim API Client
* Handles all communication with the backend simulation server.
*/
// Auto-detect API base from current page location (same origin)
function getApiBase() {
// When served by the backend, use same origin
if (typeof window !== 'undefined') {
return window.location.origin;
}
// Fallback for development
return 'http://localhost:8000';
}
class SimulationAPI {
constructor() {
this.baseUrl = getApiBase();
this.connected = false;
this.lastState = null;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.connected = true;
return await response.json();
} catch (error) {
console.error(`API Error (${endpoint}):`, error.message);
this.connected = false;
throw error;
}
}
// Health check
async checkHealth() {
try {
const data = await this.request('/health');
this.connected = data.status === 'healthy';
return this.connected;
} catch {
this.connected = false;
return false;
}
}
// Get full simulation state
async getState() {
const data = await this.request('/api/state');
this.lastState = data;
return data;
}
// Get all agents
async getAgents() {
return await this.request('/api/agents');
}
// Get specific agent
async getAgent(agentId) {
return await this.request(`/api/agents/${agentId}`);
}
// Get market orders
async getMarketOrders() {
return await this.request('/api/market/orders');
}
// Get market prices
async getMarketPrices() {
return await this.request('/api/market/prices');
}
// Control: Initialize simulation
async initialize(numAgents = 8, worldWidth = 20, worldHeight = 20) {
return await this.request('/api/control/initialize', {
method: 'POST',
body: JSON.stringify({
num_agents: numAgents,
world_width: worldWidth,
world_height: worldHeight,
}),
});
}
// Control: Advance one step
async nextStep() {
return await this.request('/api/control/next_step', {
method: 'POST',
});
}
// Control: Set mode (manual/auto)
async setMode(mode) {
return await this.request('/api/control/mode', {
method: 'POST',
body: JSON.stringify({ mode }),
});
}
// Control: Get status
async getStatus() {
return await this.request('/api/control/status');
}
// Config: Get configuration
async getConfig() {
return await this.request('/api/config');
}
// Logs: Get recent logs
async getLogs(limit = 10) {
return await this.request(`/api/logs?limit=${limit}`);
}
}
// Export singleton instance
export const api = new SimulationAPI();
export default api;

View File

@ -0,0 +1,71 @@
/**
* VillSim Constants
* Shared constants for the Phaser game.
*/
// Profession icons and colors
export const PROFESSIONS = {
hunter: { icon: '🏹', color: 0xc45c5c, name: 'Hunter' },
gatherer: { icon: '🌿', color: 0x6bab5e, name: 'Gatherer' },
woodcutter: { icon: '🪓', color: 0xa67c52, name: 'Woodcutter' },
trader: { icon: '💰', color: 0xd4a84b, name: 'Trader' },
crafter: { icon: '🧵', color: 0x8b6fc0, name: 'Crafter' },
villager: { icon: '👤', color: 0x7a8899, name: 'Villager' },
};
// Resource icons and colors
export const RESOURCES = {
meat: { icon: '🥩', color: 0xc45c5c, name: 'Meat' },
berries: { icon: '🫐', color: 0xa855a8, name: 'Berries' },
water: { icon: '💧', color: 0x5a8cc8, name: 'Water' },
wood: { icon: '🪵', color: 0xa67c52, name: 'Wood' },
hide: { icon: '🦴', color: 0x8b7355, name: 'Hide' },
clothes: { icon: '👕', color: 0x6b6560, name: 'Clothes' },
};
// Action icons
export const ACTIONS = {
hunt: { icon: '🏹', verb: 'hunting' },
gather: { icon: '🌿', verb: 'gathering' },
chop_wood: { icon: '🪓', verb: 'chopping wood' },
get_water: { icon: '💧', verb: 'getting water' },
weave: { icon: '🧵', verb: 'weaving' },
build_fire: { icon: '🔥', verb: 'building fire' },
trade: { icon: '💰', verb: 'trading' },
rest: { icon: '💤', verb: 'resting' },
sleep: { icon: '😴', verb: 'sleeping' },
consume: { icon: '🍽️', verb: 'consuming' },
idle: { icon: '⏳', verb: 'idle' },
};
// Time of day
export const TIME_OF_DAY = {
day: { icon: '☀️', name: 'Day' },
night: { icon: '🌙', name: 'Night' },
};
// World zones (approximate x-positions as percentages)
export const WORLD_ZONES = {
river: { start: 0.0, end: 0.15, color: 0x3a6ea5, name: 'River' },
bushes: { start: 0.15, end: 0.35, color: 0x4a7c59, name: 'Berry Bushes' },
village: { start: 0.35, end: 0.65, color: 0x8b7355, name: 'Village' },
forest: { start: 0.65, end: 1.0, color: 0x2d5016, name: 'Forest' },
};
// Colors for stats
export const STAT_COLORS = {
energy: 0xd4a84b,
hunger: 0xc87f5a,
thirst: 0x5a8cc8,
heat: 0xc45c5c,
};
// Game display settings
export const DISPLAY = {
TILE_SIZE: 32,
AGENT_SIZE: 24,
MIN_ZOOM: 0.5,
MAX_ZOOM: 2.0,
DEFAULT_ZOOM: 1.0,
};

73
web_frontend/src/main.js Normal file
View File

@ -0,0 +1,73 @@
/**
* VillSim - Phaser Web Frontend
* Main entry point
*/
import BootScene from './scenes/BootScene.js';
import GameScene from './scenes/GameScene.js';
// Calculate game dimensions based on container
function getGameDimensions() {
const container = document.getElementById('game-container');
if (!container) {
return { width: 800, height: 600 };
}
const rect = container.getBoundingClientRect();
return {
width: Math.floor(rect.width),
height: Math.floor(rect.height),
};
}
// Phaser game configuration
const { width, height } = getGameDimensions();
const config = {
type: Phaser.AUTO,
parent: 'game-container',
width: width,
height: height,
backgroundColor: '#151921',
scene: [BootScene, GameScene],
scale: {
mode: Phaser.Scale.RESIZE,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
render: {
antialias: true,
pixelArt: false,
roundPixels: true,
},
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: false,
},
},
dom: {
createContainer: true,
},
};
// Initialize game when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
console.log('VillSim Web Frontend starting...');
// Create Phaser game
const game = new Phaser.Game(config);
// Handle window resize
window.addEventListener('resize', () => {
const { width, height } = getGameDimensions();
game.scale.resize(width, height);
});
// Store game reference globally for debugging
window.villsimGame = game;
});
// Export for debugging
export { config };

View File

@ -0,0 +1,141 @@
/**
* BootScene - Initial loading and setup
*/
import { api } from '../api.js';
export default class BootScene extends Phaser.Scene {
constructor() {
super({ key: 'BootScene' });
}
preload() {
// Create loading graphics
const { width, height } = this.cameras.main;
// Background
this.add.rectangle(width / 2, height / 2, width, height, 0x151921);
// Loading text
this.loadingText = this.add.text(width / 2, height / 2 - 40, 'VillSim', {
fontSize: '48px',
fontFamily: 'Crimson Pro, Georgia, serif',
color: '#d4a84b',
}).setOrigin(0.5);
this.statusText = this.add.text(width / 2, height / 2 + 20, 'Connecting to server...', {
fontSize: '18px',
fontFamily: 'Crimson Pro, Georgia, serif',
color: '#a8a095',
}).setOrigin(0.5);
// Progress bar background
const barWidth = 300;
const barHeight = 8;
this.progressBg = this.add.rectangle(
width / 2, height / 2 + 60,
barWidth, barHeight,
0x242b3d
).setOrigin(0.5);
this.progressBar = this.add.rectangle(
width / 2 - barWidth / 2, height / 2 + 60,
0, barHeight,
0xd4a84b
).setOrigin(0, 0.5);
}
async create() {
// Animate progress bar
this.tweens.add({
targets: this.progressBar,
width: 100,
duration: 500,
ease: 'Power2',
});
// Attempt to connect to server
this.statusText.setText('Connecting to server...');
let connected = false;
let retries = 0;
const maxRetries = 10;
while (!connected && retries < maxRetries) {
try {
connected = await api.checkHealth();
if (connected) {
this.statusText.setText('Connected! Loading simulation...');
this.tweens.add({
targets: this.progressBar,
width: 200,
duration: 300,
ease: 'Power2',
});
} else {
retries++;
this.statusText.setText(`Connecting... (attempt ${retries}/${maxRetries})`);
await this.delay(1000);
}
} catch (error) {
retries++;
this.statusText.setText(`Connection failed. Retrying... (${retries}/${maxRetries})`);
await this.delay(1000);
}
}
if (!connected) {
this.statusText.setText('Could not connect to server. Is the backend running?');
this.statusText.setColor('#c45c5c');
// Add retry button
const retryBtn = this.add.text(
this.cameras.main.width / 2,
this.cameras.main.height / 2 + 100,
'[ Click to Retry ]',
{
fontSize: '16px',
fontFamily: 'Crimson Pro, Georgia, serif',
color: '#d4a84b',
}
).setOrigin(0.5).setInteractive({ useHandCursor: true });
retryBtn.on('pointerup', () => {
this.scene.restart();
});
retryBtn.on('pointerover', () => retryBtn.setColor('#e8e4dc'));
retryBtn.on('pointerout', () => retryBtn.setColor('#d4a84b'));
return;
}
// Load initial state
try {
const state = await api.getState();
this.registry.set('simulationState', state);
this.tweens.add({
targets: this.progressBar,
width: 300,
duration: 300,
ease: 'Power2',
onComplete: () => {
this.statusText.setText('Starting simulation...');
this.time.delayedCall(500, () => {
this.scene.start('GameScene');
});
}
});
} catch (error) {
this.statusText.setText('Error loading simulation state');
this.statusText.setColor('#c45c5c');
console.error('Failed to load state:', error);
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
/**
* Scene exports
*/
export { default as BootScene } from './BootScene.js';
export { default as GameScene } from './GameScene.js';

1053
web_frontend/styles.css Normal file

File diff suppressed because it is too large Load Diff