This document describes the function and methods of agent-model simulation server (currently named combined_server.py). It servers as an intermediate between simulation code (simulation.py, imported as module) and a client accessing the simulation state via the network (http, websocket).
The simulation is loaded as a module named World:
from simulation import WorldThe following methods must exists in World (even if just as a dummy method returning null if not applicable)
###create() (classmethod)
@classmethod
World.create(**kwargs) -> WorldPrimary entry point for creating a simulation world.
Accepts an opaque config dict from the server. Only this method knows what parameters are valid and what they mean. The server passes the config through without inspection.
Advance simulation one step.
Clear dirty tracking (call after sending delta to clients).
Get all entities (agents and map cells) within a viewport region.
Server passes array without unpacking:
counts = world.count_agents()
return {'counts': list(counts), 'total': sum(counts)}Sent by HTTP /stats endpoint and in periodic status broatcasts in stream.
Generic simulation state statistis (not interpreted by server)
Returns simulation parameters for external reporting.
Used by: HTTP /params endpoint. Simulation decides what to expose.
Set a simulation parameter at runtime. Returns dict:
{'status': 'ok', 'name': 'prey.move_cost', 'old_value': 0.1, 'new_value': 0.5}
{'status': 'error', 'message': 'Unknown parameter: foo', 'valid_params': [...]}
{'status': 'error', 'message': 'Invalid value for prey.move_cost: ...'}Used by: HTTP /set_param endpoint.
Send description of full simulation state (for clients connecting to running simulation).
Get IDs of changed agents since last mark_clean(). Preferred for streaming.
{
'tick': int,
'spawned_ids': List[int], # New agents
'updated_ids': List[int], # Modified agents (excluding spawned)
'despawned_ids': List[int] # Removed agents
}Used by: Streaming server delta distribution
Clear dirty tracking. Call after processing delta.
Complete world snapshot for initial sync.
{
'tick': int,
'width': int,
'height': int,
'agents': List[dict], # to_display_dict() for each
'cells': List[dict], # Food cells (type=-1)
'total_entities': int
}Used by: WebSocket subscribe (initial snapshot)
Get full state of a specific agent.
Used by: HTTP /inspect/<id>, WebSocket inspect command
Check if a position is within a viewport region.
World.is_in_viewport((5, 5), (0, 0), 10) # True - within radius
World.is_in_viewport((15, 5), (0, 0), 10) # False - outside radiusServer uses this for viewport filtering without knowing topology details.
Used by: Streaming buffer, HTTP viewport handler
Get all agents within a viewport region.
{
'tick': int,
'center': List[float], # Center position as list
'radius': float,
'agents': List[dict], # to_display_dict() for each agent in viewport
'count': int # Number of agents
}This is the simulation's implementation of viewport queries. The server calls this without knowing topology details.
Used by: HTTP /viewport endpoint
Get static map data for initial transmission to client.
###World parameters world.tick Current simulation time.
world.halted, world.halt_reason Flag to report if simulation has halted, string with reason for halting.
world.width, world.height These are there for convenience, so the client can request them. Not needed for server functionality, can also return null.
World contains an agents array that can be accessed by agent id:
agent = world.agents.get(aid)
agent.id Unique integer to identify specific agent.
agent.position Reports agent "position". Server is agnostic as to format or content, can also return null if agents don't hvae "positions".
agent.to_display_dict() "Phenotype" dictionary of agents streamed over websocket. Server is agnostic as to format or content of this data.
Simulation should also have standalone CLI entry point for testing. Not used by server.
run_simulation(
width: int = 100,
height: int = 100,
initial_prey: int = 1000,
initial_predators: int = 15,
max_ticks: int = 500,
seed: Optional[int] = None
)The server is completely simulation-agnostic. It:
- Only imports
World(notAgentTypeor any constants) - Does not inspect config parameters - passes them as opaque dict
- Treats agent counts as opaque arrays
- Reads topology agent.position, world.width, world.height only to transmit to client, does not "look inside".
- Delegates viewport logic to
World.is_in_viewport()andWorld.get_viewport()
HTTP Endpoints (port 5000): POST /init - Initialize world POST /start - Start simulation POST /stop - Stop simulation POST /step - Single step(s) POST /set_param - Set simulation parameter GET /stats - Get statistics GET /params - Get parameters GET /viewport - Get agents in viewport (center, r) GET /inspect/ - Get full agent state
Viewport URL format: /viewport?center=50_50&r=20 ("center" is a single string to be parsed by get_viewport method) /viewport?cx=50&cy=50&r=20 (legacy with explicit 2d position format)
WebSocket (port 8765): -> subscribe - Start receiving streaming updates -> unsubscribe - Stop receiving updates -> viewport - Set viewport filter for WS stream -> viewport_clear - Clear viewport filter <- snapshot - Full state (on subscribe) <- delta - Streaming updates (constant rate) <- stats - Periodic statistics
WS Streaming: {ws.DEFAULT_AGENTS_PER_MESSAGE} agents every {ws.DEFAULT_STREAM_INTERVAL_MS}ms = {1000/ws.DEFAULT_STREAM_INTERVAL_MS * ws.DEFAULT_AGENTS_PER_MESSAGE:.0f} agent updates/sec
world.agentsis a Python dict - individualget()calls are thread-safe- Iteration (
for agent in agents.values()) is NOT safe during modification - Server uses
list(agents.keys())snapshot pattern for safe iteration step()modifies agents dict - must not be called concurrently with reads