[new] web-based frontend
This commit is contained in:
parent
1423fc0dc9
commit
67dc007283
@ -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 ==============
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
279
web_frontend/index.html
Normal 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
132
web_frontend/src/api.js
Normal 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;
|
||||||
|
|
||||||
71
web_frontend/src/constants.js
Normal file
71
web_frontend/src/constants.js
Normal 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
73
web_frontend/src/main.js
Normal 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 };
|
||||||
|
|
||||||
141
web_frontend/src/scenes/BootScene.js
Normal file
141
web_frontend/src/scenes/BootScene.js
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1633
web_frontend/src/scenes/GameScene.js
Normal file
1633
web_frontend/src/scenes/GameScene.js
Normal file
File diff suppressed because it is too large
Load Diff
7
web_frontend/src/scenes/index.js
Normal file
7
web_frontend/src/scenes/index.js
Normal 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
1053
web_frontend/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user