Skip to content

Add Order and Chaos agent types with code review improvements#577

Merged
csmangum merged 7 commits intomainfrom
dev
Apr 5, 2026
Merged

Add Order and Chaos agent types with code review improvements#577
csmangum merged 7 commits intomainfrom
dev

Conversation

@csmangum
Copy link
Copy Markdown
Contributor

@csmangum csmangum commented Apr 5, 2026

This pull request introduces two new agent types, "Order" and "Chaos," to the agent-based modeling framework. These agents have distinct behavioral profiles and are fully integrated into the simulation engine, configuration, data pipeline, and visualization tools. The documentation is updated with detailed descriptions and a new example scenario to illustrate their use.

Agent Types and Configuration

  • Added OrderAgent and ChaosAgent to PopulationConfig, SimulationConfig, and their respective default parameters, ratios, and characteristics. [1] [2]
  • Updated agent type mappings and creation logic to support these new types throughout the simulation code, including initial agent creation and agent factory logic. [1] [2] [3] [4] [5]

Visualization and Metrics

  • Extended visualization configuration and charting to display "Order" and "Chaos" agent counts and trends, including color assignments and stats panels. [1] [2] [3] [4] [5] [6] [7]

Data Structures and Analysis

  • Updated database and data types (SimulationState, AgentDistribution) to track "Order" and "Chaos" agent counts, ensuring backward compatibility. [1] [2] [3] [4] [5]
  • Modified data retrieval and analysis utilities to handle the new agent types in queries and reporting.

Documentation

  • Added detailed descriptions of "Order" and "Chaos" agents and their behaviors to the documentation.
  • Introduced a new example scenario ("Order vs. Chaos") with code, demonstrating the new agents in practice. [1] [2]

Configuration and Usability Enhancements

  • Updated configuration flattening/nesting logic to support the new agent population fields.
  • Improved docstrings and parameter descriptions for better clarity and usability.

These changes enable richer experimental scenarios, allow for the study of stability and disruption dynamics, and provide comprehensive support for the new agent types across the simulation stack.


Note

Medium Risk
Medium risk because it changes core simulation initialization and expands metrics/database/visualization schemas to handle new agent types, which can impact backwards compatibility and downstream analytics.

Overview
Introduces two new agent typesorder and chaos—with default population fields, ratios, visualization colors, and per-type behavioral parameters (notably share_weight/attack_weight) added to SimulationConfig.

Updates the simulation runtime to create and track these agents (including action-weight customization), extends GUI charts/stat panels to plot their counts, and expands database/data-layer types/utilities/validation to propagate order_agents/chaos_agents counts for retrieval and reporting.

Adds an Order vs. Chaos documentation example and new tests covering agent creation, config defaults, and parameter expectations.

Reviewed by Cursor Bugbot for commit 1334be1. Configure here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds two new agent archetypes (“Order” and “Chaos”) to the AgentFarm simulation stack, wiring them through configuration, initialization, persistence/analysis data structures, and UI metrics so they can be run and compared in experiments.

Changes:

  • Introduces order / chaos agent counts + parameter presets in SimulationConfig / PopulationConfig, and updates agent-type weight customization.
  • Extends DB-facing data types + normalization utilities to carry order_agents / chaos_agents metrics (backward-compatible defaults).
  • Updates visualization metrics (stats panel + time-series plots) and adds tests + documentation example scenario.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_order_chaos_agents.py New tests covering creation + config defaults/parameters for Order/Chaos.
tests/config/test_config_classes.py Updates expected default visualization colors/metric colors to include new types.
farm/database/validation.py Extends population consistency validation to include order/chaos counts.
farm/database/utils.py Normalizes simulation_steps DataFrame with order_agents / chaos_agents columns from JSON.
farm/database/models.py Adds order_agents / chaos_agents to serialized step dict output.
farm/database/data_types.py Extends core dataclasses with optional/defaulted order/chaos fields for compatibility.
farm/database/data_retrieval.py Populates AgentDistribution with order/chaos ratios.
farm/core/visualization.py Adds stats + chart lines for order/chaos metrics.
farm/core/simulation.py Creates initial order/chaos agents and includes them in initial-population logging.
farm/core/agent/core.py Maps order / chaos agent types to config keys for action-weight customization.
farm/config/config.py Adds population fields, visualization colors, agent parameter presets, and flat->nested conversion support.
docs/features/agent_based_modeling_analysis.md Documents new agent types and adds an “Order vs. Chaos” example scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread farm/core/visualization.py
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: New agent types rendered with wrong color
    • _draw_agents now resolves color via agent_colors.get(agent_type) with ControlAgent as fallback so OrderAgent and ChaosAgent use their configured colors.
  • ✅ Fixed: Historical data query omits new agent types
    • get_historical_data now includes order_agents and chaos_agents from agent_type_counts JSON keys order and chaos.
  • ✅ Fixed: Backward-compat path skips adding new agent columns
    • The legacy three-column branch now adds order_agents and chaos_agents as zero when missing before returning.
Preview (97f17bd194)
diff --git a/docs/features/agent_based_modeling_analysis.md b/docs/features/agent_based_modeling_analysis.md
--- a/docs/features/agent_based_modeling_analysis.md
+++ b/docs/features/agent_based_modeling_analysis.md
@@ -30,6 +30,7 @@
 3. [Practical Examples](#practical-examples)
    - [Example 1: Basic agent-based simulation](#example-1-basic-agent-based-simulation)
    - [Examples 2–4 (replaced)](#examples-24-replaced)
+   - [Example 2: Order vs. Chaos scenario](#example-2-order-vs-chaos-scenario)
 4. [Advanced features](#advanced-features)
 5. [Performance optimization](#performance-optimization)
    - [Efficient spatial queries](#efficient-spatial-queries)
@@ -83,9 +84,11 @@
 
 AgentFarm supports multiple agent types, each with distinct behaviors:
 
-- **System Agents**: Cooperative agents that prioritize collective goals and resource sharing
-- **Independent Agents**: Self-oriented agents focused on individual survival and resource acquisition
-- **Control Agents**: Baseline agents for experimental comparison
+- **System Agents** (`agent_type="system"`): Cooperative agents that prioritize collective goals and resource sharing. They have high sharing weights and low attack weights, making them well-suited for studying cooperative emergence.
+- **Independent Agents** (`agent_type="independent"`): Self-oriented agents focused on individual survival and resource acquisition. They gather resources aggressively but rarely share, making them useful for studying competitive dynamics.
+- **Control Agents** (`agent_type="control"`): Baseline agents with balanced parameters for experimental comparison. They serve as a neutral reference point in mixed-type experiments.
+- **Order Agents** (`agent_type="order"`): Structure-seeking agents that favor stability, predictable resource gathering, and cautious behavior. They maintain high resource reserves, share moderately with neighbors, and avoid combat. Use them to study the emergence of organized, rule-following societies.
+- **Chaos Agents** (`agent_type="chaos"`): Disruption-oriented agents that act recklessly, attack frequently, and ignore cooperative norms. They keep minimal resource reserves and rarely share. Use them to study instability, adversarial dynamics, and the breakdown of cooperative strategies.
 - **Custom Agents**: Define your own agent types with specialized behaviors
 
 #### Agent Capabilities
@@ -562,6 +565,59 @@
 
 Longer tutorials for cooperation studies, multi-run comparisons, and sweeps belong in **[Usage examples](../usage_examples.md)** and the **`tests/`** suite. For multiple runs with a single driver, use **`ExperimentRunner`** (`farm.runners.experiment_runner`) and read `_create_iteration_config` for how variation dicts map to `SimulationConfig`. For comparing SQLite outputs, use **`farm.database.simulation_comparison`** (session-based helpers) or **`farm.analysis.comparative_analysis.compare_simulations`**, depending on your workflow.
 
+### Example 2: Order vs. Chaos scenario
+
+```python
+from farm.config import SimulationConfig
+from farm.core.simulation import run_simulation
+from farm.core.analysis import SimulationAnalyzer
+
+
+def run_order_vs_chaos():
+    config = SimulationConfig.from_centralized_config(environment="development")
+    config.environment.width = 50
+    config.environment.height = 50
+
+    # Mix of all agent types
+    config.population.system_agents = 5
+    config.population.independent_agents = 5
+    config.population.control_agents = 5
+    config.population.order_agents = 10   # Structure-seeking, cooperative
+    config.population.chaos_agents = 10   # Disruptive, aggressive
+
+    config.resources.initial_resources = 300
+    config.resources.resource_regen_rate = 0.03
+    config.max_steps = 500
+    config.seed = 42
+
+    env = run_simulation(
+        num_steps=config.max_steps,
+        config=config,
+        path="simulations",
+        save_config=True,
+    )
+
+    analyzer = SimulationAnalyzer(env.db.db_path, simulation_id=env.simulation_id)
+    survival = analyzer.calculate_survival_rates()
+    print(survival.head())
+
+
+if __name__ == "__main__":
+    run_order_vs_chaos()
+```
+
+**Order Agent characteristics** (`agent_type="order"`):
+- High minimum resource threshold (0.3) — maintains stable reserves
+- Moderate sharing weight (0.25) — cooperative with neighbors
+- Very low attack weight (0.02) — avoids conflict
+- Moderate gather efficiency (0.6) — consistent and reliable
+
+**Chaos Agent characteristics** (`agent_type="chaos"`):
+- Very low minimum resource threshold (0.03) — reckless resource management
+- Very low sharing weight (0.02) — non-cooperative
+- Very high attack weight (0.45) — aggressive and disruptive
+- Moderate gather efficiency (0.5) — unpredictable gathering
+
 ---
 
 ## Advanced features

diff --git a/farm/config/config.py b/farm/config/config.py
--- a/farm/config/config.py
+++ b/farm/config/config.py
@@ -160,12 +160,16 @@
     system_agents: int = 10
     independent_agents: int = 10
     control_agents: int = 10
+    order_agents: int = 0
+    chaos_agents: int = 0
     max_population: int = 3000
     agent_type_ratios: Dict[str, float] = field(
         default_factory=lambda: {
             "SystemAgent": 0.33,
             "IndependentAgent": 0.33,
             "ControlAgent": 0.34,
+            "OrderAgent": 0.0,
+            "ChaosAgent": 0.0,
         }
     )
 
@@ -460,7 +464,13 @@
     birth_radius_scale: int = 4
     death_mark_scale: float = 1.5
     agent_colors: Dict[str, str] = field(
-        default_factory=lambda: {"SystemAgent": "blue", "IndependentAgent": "red", "ControlAgent": "#DAA520"}
+        default_factory=lambda: {
+            "SystemAgent": "blue",
+            "IndependentAgent": "red",
+            "ControlAgent": "#DAA520",
+            "OrderAgent": "purple",
+            "ChaosAgent": "orange",
+        }
     )
     min_font_size: int = 10
     font_scale_factor: int = 40
@@ -473,6 +483,8 @@
             "system_agents": "#50c878",
             "independent_agents": "#e74c3c",
             "control_agents": "#DAA520",
+            "order_agents": "#9932CC",
+            "chaos_agents": "#FF8C00",
             "total_resources": "#f39c12",
             "average_agent_resources": "#9b59b6",
         }
@@ -704,6 +716,20 @@
                 "share_weight": 0.15,
                 "attack_weight": 0.15,
             },
+            "OrderAgent": {
+                "gather_efficiency_multiplier": 0.6,
+                "gather_cost_multiplier": 0.35,
+                "min_resource_threshold": 0.3,
+                "share_weight": 0.25,
+                "attack_weight": 0.02,
+            },
+            "ChaosAgent": {
+                "gather_efficiency_multiplier": 0.5,
+                "gather_cost_multiplier": 0.15,
+                "min_resource_threshold": 0.03,
+                "share_weight": 0.02,
+                "attack_weight": 0.45,
+            },
         }
     )
 
@@ -1030,6 +1056,8 @@
                     "system_agents",
                     "independent_agents",
                     "control_agents",
+                    "order_agents",
+                    "chaos_agents",
                     "max_population",
                     "agent_type_ratios",
                 ],

diff --git a/farm/core/agent/core.py b/farm/core/agent/core.py
--- a/farm/core/agent/core.py
+++ b/farm/core/agent/core.py
@@ -151,6 +151,8 @@
             "system": "SystemAgent",
             "independent": "IndependentAgent",
             "control": "ControlAgent",
+            "order": "OrderAgent",
+            "chaos": "ChaosAgent",
         }
         config_key = agent_type_map.get(agent_type.lower())
         

diff --git a/farm/core/simulation.py b/farm/core/simulation.py
--- a/farm/core/simulation.py
+++ b/farm/core/simulation.py
@@ -88,6 +88,8 @@
     num_system_agents: int,
     num_independent_agents: int,
     num_control_agents: int,
+    num_order_agents: int = 0,
+    num_chaos_agents: int = 0,
 ) -> List[Tuple[float, float]]:
     """
     Create initial population of agents.
@@ -100,6 +102,12 @@
         Number of system agents to create
     num_independent_agents : int
         Number of independent agents to create
+    num_control_agents : int
+        Number of control agents to create
+    num_order_agents : int, optional
+        Number of order agents to create (default: 0)
+    num_chaos_agents : int, optional
+        Number of chaos agents to create (default: 0)
 
     Returns
     -------
@@ -112,6 +120,8 @@
         num_system_agents=num_system_agents,
         num_independent_agents=num_independent_agents,
         num_control_agents=num_control_agents,
+        num_order_agents=num_order_agents,
+        num_chaos_agents=num_chaos_agents,
     )
 
     # Create services from environment
@@ -186,6 +196,34 @@
         environment.add_agent(agent, flush_immediately=True)
         positions.append(position)
 
+    # Create order agents with learning behavior
+    for _ in range(num_order_agents):
+        position = get_random_position()
+        agent = factory.create_learning_agent(
+            agent_id=environment.get_next_agent_id(),
+            position=position,
+            initial_resources=int(initial_resource_level),
+            config=agent_config,
+            environment=environment,
+            agent_type="order",
+        )
+        environment.add_agent(agent, flush_immediately=True)
+        positions.append(position)
+
+    # Create chaos agents with learning behavior
+    for _ in range(num_chaos_agents):
+        position = get_random_position()
+        agent = factory.create_learning_agent(
+            agent_id=environment.get_next_agent_id(),
+            position=position,
+            initial_resources=int(initial_resource_level),
+            config=agent_config,
+            environment=environment,
+            agent_type="chaos",
+        )
+        environment.add_agent(agent, flush_immediately=True)
+        positions.append(position)
+
     logger.info("initial_agents_complete", total_agents=len(environment.agents))
 
     return positions
@@ -415,6 +453,8 @@
             config.population.system_agents,
             config.population.independent_agents,
             config.population.control_agents,
+            config.population.order_agents,
+            config.population.chaos_agents,
         )
 
         # Ensure all initial agents are committed to database before simulation starts
@@ -639,7 +679,13 @@
         """Calculate the initial population from configuration."""
         if config is None:
             return 0
-        return config.population.system_agents + config.population.independent_agents
+        return (
+            config.population.system_agents
+            + config.population.independent_agents
+            + config.population.control_agents
+            + config.population.order_agents
+            + config.population.chaos_agents
+        )
 
     logger.info(
         "simulation_completed",

diff --git a/farm/core/visualization.py b/farm/core/visualization.py
--- a/farm/core/visualization.py
+++ b/farm/core/visualization.py
@@ -186,6 +186,18 @@
                 "color": "#DAA520",  # Changed to goldenrod
                 "column": right_column,
             },
+            "order_agents": {
+                "var": tk.StringVar(value="0"),
+                "label": "Order Agents",
+                "color": "#9932CC",  # Dark orchid
+                "column": left_column,
+            },
+            "chaos_agents": {
+                "var": tk.StringVar(value="0"),
+                "label": "Chaos Agents",
+                "color": "#FF8C00",  # Dark orange
+                "column": right_column,
+            },
             "total_resources": {
                 "var": tk.StringVar(value="0"),
                 "label": "Total Resources",
@@ -270,12 +282,20 @@
             )[
                 0
             ],  # Changed to soft yellow
+            "order_agents": self.ax1.plot(
+                [], [], color="#9932CC", label="Order Agents"
+            )[0],
+            "chaos_agents": self.ax1.plot(
+                [], [], color="#FF8C00", label="Chaos Agents"
+            )[0],
             "resources": self.ax2.plot([], [], "g-", label="Resources")[0],
             "system_agents_future": self.ax1.plot([], [], "b-", alpha=0.3)[0],
             "independent_agents_future": self.ax1.plot([], [], "r-", alpha=0.3)[0],
             "control_agents_future": self.ax1.plot([], [], color="#FFD700", alpha=0.3)[
                 0
             ],  # Changed to soft yellow
+            "order_agents_future": self.ax1.plot([], [], color="#9932CC", alpha=0.3)[0],
+            "chaos_agents_future": self.ax1.plot([], [], color="#FF8C00", alpha=0.3)[0],
             "resources_future": self.ax2.plot([], [], "g-", alpha=0.3)[0],
             "current_step": self.ax1.axvline(
                 x=0, color="gray", linestyle="--", alpha=0.5
@@ -628,11 +648,9 @@
         """Draw agents as colored circles."""
         for agent in agent_states:
             x, y = self._transform_coords(agent[2], agent[3], params)
-            if agent[1] == "SystemAgent":
-                color = self.config.agent_colors["SystemAgent"]
-            elif agent[1] == "IndependentAgent":
-                color = self.config.agent_colors["IndependentAgent"]
-            else:  # ControlAgent
+            agent_type = agent[1]
+            color = self.config.agent_colors.get(agent_type)
+            if color is None:
                 color = self.config.agent_colors["ControlAgent"]
 
             radius = max(1, int(self.config.agent_radius_scale * params["scale"]))
@@ -716,15 +734,19 @@
 
             # Convert to numpy arrays for better performance
             steps = np.array(history["steps"])
-            system_agents = np.array(history["metrics"]["system_agents"])
-            independent_agents = np.array(history["metrics"]["independent_agents"])
-            control_agents = np.array(history["metrics"]["control_agents"])
+            system_agents = np.array(history["metrics"].get("system_agents", [0] * len(steps)))
+            independent_agents = np.array(history["metrics"].get("independent_agents", [0] * len(steps)))
+            control_agents = np.array(history["metrics"].get("control_agents", [0] * len(steps)))
+            order_agents = np.array(history["metrics"].get("order_agents", [0] * len(steps)))
+            chaos_agents = np.array(history["metrics"].get("chaos_agents", [0] * len(steps)))
             total_resources = np.array(history["metrics"]["total_resources"])
 
             # Update all lines with full data
             self.lines["system_agents"].set_data(steps, system_agents)
             self.lines["independent_agents"].set_data(steps, independent_agents)
             self.lines["control_agents"].set_data(steps, control_agents)
+            self.lines["order_agents"].set_data(steps, order_agents)
+            self.lines["chaos_agents"].set_data(steps, chaos_agents)
             self.lines["resources"].set_data(steps, total_resources)
 
             # Update current step line
@@ -733,6 +755,8 @@
                 max(system_agents),
                 max(independent_agents),
                 max(control_agents),
+                max(order_agents),
+                max(chaos_agents),
                 max(total_resources),
             )
             self.lines["current_step"].set_ydata([0, max_y])
@@ -741,7 +765,13 @@
             self.ax1.set_xlim(0, max(steps) + 10)
             self.ax1.set_ylim(
                 0,
-                max(max(system_agents), max(independent_agents), max(control_agents))
+                max(
+                    max(system_agents),
+                    max(independent_agents),
+                    max(control_agents),
+                    max(order_agents),
+                    max(chaos_agents),
+                )
                 * 1.1,
             )
             self.ax2.set_ylim(0, max(total_resources) * 1.1)

diff --git a/farm/database/data_retrieval.py b/farm/database/data_retrieval.py
--- a/farm/database/data_retrieval.py
+++ b/farm/database/data_retrieval.py
@@ -531,6 +531,8 @@
                     system_agents=type_ratios.get("system", 0.0),
                     independent_agents=type_ratios.get("independent", 0.0),
                     control_agents=type_ratios.get("control", 0.0),
+                    order_agents=type_ratios.get("order", 0.0),
+                    chaos_agents=type_ratios.get("chaos", 0.0),
                 ),
             )
 

diff --git a/farm/database/data_types.py b/farm/database/data_types.py
--- a/farm/database/data_types.py
+++ b/farm/database/data_types.py
@@ -74,6 +74,10 @@
         Number of independent agents
     control_agents : int
         Number of control group agents
+    order_agents : int
+        Number of order agents
+    chaos_agents : int
+        Number of chaos agents
     total_resources : float
         Total resources available in the environment
     average_agent_resources : float
@@ -112,6 +116,10 @@
     system_agents: int
     independent_agents: int
     control_agents: int
+    # Note: order_agents and chaos_agents have defaults to maintain backward compatibility
+    # with code that constructs SimulationState without these fields. They are logically
+    # grouped with the other agent-count fields above, but Python dataclass rules require
+    # fields with defaults to follow all fields without defaults.
     total_resources: float
     average_agent_resources: float
     births: int
@@ -128,6 +136,8 @@
     genetic_diversity: float
     dominant_genome_ratio: float
     resources_consumed: float
+    order_agents: int = 0
+    chaos_agents: int = 0
 
 
 @dataclass
@@ -231,11 +241,17 @@
         Average number of independent agents
     control_agents : float
         Average number of control agents
+    order_agents : float
+        Average number of order agents
+    chaos_agents : float
+        Average number of chaos agents
     """
 
     system_agents: float
     independent_agents: float
     control_agents: float
+    order_agents: float = 0.0
+    chaos_agents: float = 0.0
 
 
 @dataclass
@@ -846,12 +862,18 @@
         Number of independent agents
     control_agents : int
         Number of control agents
+    order_agents : int
+        Number of order agents
+    chaos_agents : int
+        Number of chaos agents
     """
 
     total_agents: int
     system_agents: int
     independent_agents: int
     control_agents: int
+    order_agents: int = 0
+    chaos_agents: int = 0
 
 
 @dataclass
@@ -1243,6 +1265,10 @@
         Number of independent agents at this step (optional)
     control_agents : Optional[int]
         Number of control agents at this step (optional)
+    order_agents : Optional[int]
+        Number of order agents at this step (optional)
+    chaos_agents : Optional[int]
+        Number of chaos agents at this step (optional)
     avg_resources : Optional[float]
         Average resources per agent at this step (optional)
     """
@@ -1254,6 +1280,8 @@
     system_agents: Optional[int] = None
     independent_agents: Optional[int] = None
     control_agents: Optional[int] = None
+    order_agents: Optional[int] = None
+    chaos_agents: Optional[int] = None
     avg_resources: Optional[float] = None
 
 

diff --git a/farm/database/models.py b/farm/database/models.py
--- a/farm/database/models.py
+++ b/farm/database/models.py
@@ -379,6 +379,8 @@
             "system_agents": agent_counts.get("system", 0),
             "independent_agents": agent_counts.get("independent", 0),
             "control_agents": agent_counts.get("control", 0),
+            "order_agents": agent_counts.get("order", 0),
+            "chaos_agents": agent_counts.get("chaos", 0),
             "total_resources": self.total_resources,
             "average_agent_resources": self.average_agent_resources,
             "births": self.births,

diff --git a/farm/database/repositories/gui_repository.py b/farm/database/repositories/gui_repository.py
--- a/farm/database/repositories/gui_repository.py
+++ b/farm/database/repositories/gui_repository.py
@@ -54,6 +54,8 @@
                 - system_agents
                 - independent_agents
                 - control_agents
+                - order_agents
+                - chaos_agents
                 - total_resources
                 - average_agent_resources
 
@@ -94,6 +96,8 @@
                     "system_agents": [(row[2] or {}).get("system", 0) for row in rows],
                     "independent_agents": [(row[2] or {}).get("independent", 0) for row in rows],
                     "control_agents": [(row[2] or {}).get("control", 0) for row in rows],
+                    "order_agents": [(row[2] or {}).get("order", 0) for row in rows],
+                    "chaos_agents": [(row[2] or {}).get("chaos", 0) for row in rows],
                     "total_resources": [row[3] for row in rows],
                     "average_agent_resources": [row[4] for row in rows],
                 },

diff --git a/farm/database/utils.py b/farm/database/utils.py
--- a/farm/database/utils.py
+++ b/farm/database/utils.py
@@ -37,8 +37,9 @@
 def normalize_simulation_steps_dataframe(df: pd.DataFrame) -> pd.DataFrame:
     """Normalize simulation_steps DataFrame to extract agent counts from JSON.
     
-    This function adds backwards-compatible columns (system_agents, independent_agents, control_agents)
-    by extracting them from the agent_type_counts JSON column.
+    This function adds backwards-compatible columns (system_agents, independent_agents,
+    control_agents, order_agents, chaos_agents) by extracting them from the
+    agent_type_counts JSON column.
     
     Parameters
     ----------
@@ -57,11 +58,17 @@
     if "agent_type_counts" not in df.columns:
         # If old columns exist, keep them (for backwards compatibility during migration)
         if all(col in df.columns for col in ["system_agents", "independent_agents", "control_agents"]):
+            if "order_agents" not in df.columns:
+                df["order_agents"] = 0
+            if "chaos_agents" not in df.columns:
+                df["chaos_agents"] = 0
             return df
         # Otherwise, add empty columns
         df["system_agents"] = 0
         df["independent_agents"] = 0
         df["control_agents"] = 0
+        df["order_agents"] = 0
+        df["chaos_agents"] = 0
         return df
     
     # Extract counts from JSON column
@@ -71,13 +78,15 @@
             "system_agents": counts.get("system", 0),
             "independent_agents": counts.get("independent", 0),
             "control_agents": counts.get("control", 0),
+            "order_agents": counts.get("order", 0),
+            "chaos_agents": counts.get("chaos", 0),
         })
     
     # Extract agent counts
     counts_df = df.apply(extract_counts, axis=1)
     
     # Add columns, overwriting if they already exist
-    for col in ["system_agents", "independent_agents", "control_agents"]:
+    for col in ["system_agents", "independent_agents", "control_agents", "order_agents", "chaos_agents"]:
         df[col] = counts_df[col]
     
     return df

diff --git a/farm/database/validation.py b/farm/database/validation.py
--- a/farm/database/validation.py
+++ b/farm/database/validation.py
@@ -674,6 +674,8 @@
             agent_type_counts.get("system", 0),
             agent_type_counts.get("independent", 0),
             agent_type_counts.get("control", 0),
+            agent_type_counts.get("order", 0),
+            agent_type_counts.get("chaos", 0),
         )
 
         # Get actual agent counts
@@ -708,6 +710,22 @@
             {"step": latest_step},
         ).scalar()
 
+        actual_order = self.session.execute(
+            text("""
+            SELECT COUNT(*) FROM agents 
+            WHERE agent_type = 'order' AND (death_time IS NULL OR death_time > :step)
+        """),
+            {"step": latest_step},
+        ).scalar()
+
+        actual_chaos = self.session.execute(
+            text("""
+            SELECT COUNT(*) FROM agents 
+            WHERE agent_type = 'chaos' AND (death_time IS NULL OR death_time > :step)
+        """),
+            {"step": latest_step},
+        ).scalar()
+
         violations = 0
         issues = []
 
@@ -732,6 +750,14 @@
             violations += 1
             issues.append(f"Control agents: step={step_counts[3]}, actual={actual_control}")
 
+        if abs(step_counts[4] - actual_order) > tolerance:
+            violations += 1
+            issues.append(f"Order agents: step={step_counts[4]}, actual={actual_order}")
+
+        if abs(step_counts[5] - actual_chaos) > tolerance:
+            violations += 1
+            issues.append(f"Chaos agents: step={step_counts[5]}, actual={actual_chaos}")
+
         if violations > 0:
             self.report.add_result(
                 ValidationResult(

diff --git a/tests/config/test_config_classes.py b/tests/config/test_config_classes.py
--- a/tests/config/test_config_classes.py
+++ b/tests/config/test_config_classes.py
@@ -29,7 +29,13 @@
         self.assertEqual(config.agent_radius_scale, 2)
         self.assertEqual(config.birth_radius_scale, 4)
         self.assertEqual(config.death_mark_scale, 1.5)
-        self.assertEqual(config.agent_colors, {"SystemAgent": "blue", "IndependentAgent": "red", "ControlAgent": "#DAA520"})
+        self.assertEqual(config.agent_colors, {
+            "SystemAgent": "blue",
+            "IndependentAgent": "red",
+            "ControlAgent": "#DAA520",
+            "OrderAgent": "purple",
+            "ChaosAgent": "orange",
+        })
         self.assertEqual(config.min_font_size, 10)
         self.assertEqual(config.font_scale_factor, 40)
         self.assertEqual(config.font_family, "arial")
@@ -40,6 +46,8 @@
             "system_agents": "#50c878",
             "independent_agents": "#e74c3c",
             "control_agents": "#DAA520",
+            "order_agents": "#9932CC",
+            "chaos_agents": "#FF8C00",
             "total_resources": "#f39c12",
             "average_agent_resources": "#9b59b6",
         }
@@ -82,7 +90,13 @@
         self.assertEqual(config_dict["birth_mark_color"], [200, 200, 200])
 
         # Check that nested dicts are preserved
-        self.assertEqual(config_dict["agent_colors"], {"SystemAgent": "blue", "IndependentAgent": "red", "ControlAgent": "#DAA520"})
+        self.assertEqual(config_dict["agent_colors"], {
+            "SystemAgent": "blue",
+            "IndependentAgent": "red",
+            "ControlAgent": "#DAA520",
+            "OrderAgent": "purple",
+            "ChaosAgent": "orange",
+        })
 
         # Check basic types
         self.assertEqual(config_dict["padding"], 20)

diff --git a/tests/database/test_gui_repository.py b/tests/database/test_gui_repository.py
--- a/tests/database/test_gui_repository.py
+++ b/tests/database/test_gui_repository.py
@@ -28,8 +28,20 @@
 
     def test_get_historical_data(self):
         """Test get_historical_data returns time series metrics."""
-        mock_row1 = (1, 10, {"system": 5, "independent": 3, "control": 2}, 1000.0, 100.0)
-        mock_row2 = (2, 12, {"system": 6, "independent": 4, "control": 2}, 1200.0, 100.0)
+        mock_row1 = (
+            1,
+            10,
+            {"system": 5, "independent": 3, "control": 2, "order": 1, "chaos": 0},
+            1000.0,
+            100.0,
+        )
+        mock_row2 = (
+            2,
+            12,
+            {"system": 6, "independent": 4, "control": 2, "order": 0, "chaos": 1},
+            1200.0,
+            100.0,
+        )
 
         mock_query = Mock()
         mock_query.order_by.return_value.all.return_value = [mock_row1, mock_row2]
@@ -42,6 +54,8 @@
         self.assertEqual(result["steps"], [1, 2])
         self.assertEqual(result["metrics"]["total_agents"], [10, 12])
         self.assertEqual(result["metrics"]["system_agents"], [5, 6])
+        self.assertEqual(result["metrics"]["order_agents"], [1, 0])
+        self.assertEqual(result["metrics"]["chaos_agents"], [0, 1])
 
     def test_get_historical_data_with_agent_filter(self):
         """Test get_historical_data with agent_id filter."""

diff --git a/tests/test_db_utils.py b/tests/test_db_utils.py
--- a/tests/test_db_utils.py
+++ b/tests/test_db_utils.py
@@ -90,6 +90,10 @@
         )
         result = normalize_simulation_steps_dataframe(df)
         self.assertEqual(result.iloc[0]["system_agents"], 5)
+        self.assertIn("order_agents", result.columns)
+        self.assertIn("chaos_agents", result.columns)
+        self.assertEqual(result.iloc[0]["order_agents"], 0)
+        self.assertEqual(result.iloc[0]["chaos_agents"], 0)
 
     def test_missing_agent_types_default_to_zero(self):
         df = pd.DataFrame(

diff --git a/tests/test_order_chaos_agents.py b/tests/test_order_chaos_agents.py
new file mode 100644
--- /dev/null
+++ b/tests/test_order_chaos_agents.py
@@ -1,0 +1,160 @@
+"""Tests for Order and Chaos agent types.
+
+Validates that Order and Chaos agents are correctly recognized, configured,
+and created with the expected behavioral parameters.
+"""
+
+import pytest
+from unittest.mock import Mock
+
+from farm.core.agent import AgentFactory, AgentServices
+from farm.config.config import PopulationConfig, SimulationConfig
+
+
+class TestOrderChaosAgentTypes:
+    """Test Order and Chaos agent type recognition and configuration."""
+
+    @pytest.fixture
+    def mock_services(self):
+        """Create mock services for testing."""
+        return AgentServices(
+            spatial_service=Mock(),
+            time_service=Mock(current_time=Mock(return_value=0)),
+            metrics_service=Mock(),
+            logging_service=Mock(),
+            validation_service=Mock(is_valid_position=Mock(return_value=True)),
+            lifecycle_service=Mock(),
+        )
+
+    def test_order_agent_creation(self, mock_services):
+        """Test that an order agent can be created with correct type."""
+        factory = AgentFactory(mock_services)
+        agent = factory.create_default_agent(
+            agent_id="order_001",
+            position=(10.0, 10.0),
+            initial_resources=100.0,
+            agent_type="order",
+        )
+        assert agent.agent_type == "order"
+        assert agent.alive is True
+
+    def test_chaos_agent_creation(self, mock_services):
+        """Test that a chaos agent can be created with correct type."""
+        factory = AgentFactory(mock_services)
+        agent = factory.create_default_agent(
+            agent_id="chaos_001",
+            position=(20.0, 20.0),
+            initial_resources=100.0,
+            agent_type="chaos",
+        )
+        assert agent.agent_type == "chaos"
+        assert agent.alive is True
+
+    def test_order_agent_action_weights(self, mock_services):
+        """Order agents should have very low attack weight per config."""
+        config = SimulationConfig()
+        order_params = config.agent_parameters["OrderAgent"]
+        assert order_params["attack_weight"] < order_params["share_weight"]
+
+    def test_chaos_agent_action_weights(self, mock_services):
+        """Chaos agents should have very high attack weight per config."""
+        config = SimulationConfig()
+        chaos_params = config.agent_parameters["ChaosAgent"]
+        assert chaos_params["attack_weight"] > chaos_params["share_weight"]
+
+    def test_all_agent_types_have_components(self, mock_services):
+        """All new agent types should have the standard set of components."""
+        factory = AgentFactory(mock_services)
+        for agent_type in ("order", "chaos"):
+            agent = factory.create_default_agent(
+                agent_id=f"{agent_type}_comp_test",
+                position=(0.0, 0.0),
+                agent_type=agent_type,
+            )
+            assert agent.get_component("movement") is not None
+            assert agent.get_component("resource") is not None
+            assert agent.get_component("combat") is not None
+
+
+class TestPopulationConfig:
+    """Test PopulationConfig includes order and chaos agents."""
+
+    def test_default_order_chaos_agents_zero(self):
+        """Order and Chaos agents default to 0 for backward compatibility."""
+        config = PopulationConfig()
... diff truncated: showing 800 of 876 lines

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 1334be1. Configure here.

Comment thread farm/core/visualization.py
Comment thread farm/core/visualization.py
Comment thread farm/database/utils.py
…alization

- Look up agent draw color from agent_colors by type with ControlAgent fallback
- Include order_agents and chaos_agents in get_historical_data metrics
- Add missing order/chaos columns on legacy simulation_steps DataFrames
- Extend unit tests for gui_repository and db utils

Co-authored-by: Chris Mangum <csmangum@gmail.com>
- Introduced DOMINANCE_AGENT_TYPES constant for consistent agent type references across modules.
- Updated various functions and classes to utilize the new constant, enhancing maintainability and reducing hardcoded values.
- Added support for new agent types: OrderAgent and ChaosAgent in relevant calculations and data structures.
- Improved data handling in simulation processing and metrics calculations to accommodate the expanded agent types.

Co-authored-by: csmangum <30098247+csmangum@users.noreply.github.com>
@csmangum
Copy link
Copy Markdown
Contributor Author

csmangum commented Apr 5, 2026

@cursor fix the python test failure(s)

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 5, 2026

Taking a look!

Open in Web Open in Cursor 

@csmangum csmangum merged commit 3f5ee12 into main Apr 5, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants