What You'll Build
In this exercise, you will build an autonomous cricket match orchestration system using agentic workflows. The system implements multiple specialized agents that operate independently yet coordinate to simulate and analyze a complete cricket match, spanning toss decisions through final scorecard generation.
The architecture demonstrates hierarchical agent delegation, in which a master orchestrator agent manages player selection agents, match simulation agents, and analytics agents. Each agent has clearly defined responsibilities, reasoning capabilities, and state persistence.
The system uses tool-calling patterns to handle complex cricket mechanics—such as run calculations, wicket logic, and powerplay constraints—and leverages multi-turn reasoning to adapt decisions based on match state. This hands-on project illustrates why decomposing complex workflows into autonomous agents, rather than monolithic pipelines, enables flexibility, maintainability, and emergent problem-solving in production AI systems.
Prerequisites
- Solid understanding of agent architecture: roles, state management, and action spaces from earlier agentic workflow lessons.
- Experience implementing tool-calling patterns where agents invoke deterministic functions (e.g., calculate_runs, apply_boundary_rules).
- Familiarity with cricket terminology: ODI format, powerplay rules, wicket types, run calculations, economy rate, and match-winning conditions.
- Proficiency in Python async/await patterns and message-passing systems for coordinating multiple agent instances.
- Knowledge of state machines to track cricket match phases (pre-match, innings 1, innings 2, post-match) and agent lifecycle.
Setup & Project Structure
Begin by initializing a Python project with a modular structure that supports multiple agent classes, shared cricket domain models, and tool libraries. Create directories for agents (orchestrator, player selection, match simulation, and analytics), cricket logic (scorecard, delivery mechanics, and rules), and configuration.
Install the required dependencies, including an LLM client library such as the Anthropic Claude SDK for agent reasoning, Pydantic for type-safe data models, and pytest for testing. The project layout is designed to enforce separation of concerns: agents contain reasoning loops, models contain cricket domain objects, tools contain deterministic cricket mechanics, and tests validate agent behavior both in isolation and during integration.
# Initialize project structure for cricket agentic workflows
mkdir cricket-agentic-orchestrator
cd cricket-agentic-orchestrator
# Create directory hierarchy
mkdir -p agents/{orchestrator,selection,simulation,analytics}
mkdir -p models/{cricket_domain,match_state}
mkdir -p tools/{cricket_mechanics,scorecard}
mkdir -p tests
mkdir -p config
# Initialize Python project
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Create requirements.txt with dependencies
cat > requirements.txt << 'EOF'
anthropics==0.32.0
pydantic==2.5.0
pydantic-settings==2.1.0
python-dotenv==1.0.0
pytest==7.4.3
pytest-asyncio==0.21.1
aiohttp==3.9.1
typing-extensions==4.9.0
EOF
# Install dependencies
pip install -r requirements.txt
# Create main entry point and config
touch main.py
touch config/agent_config.py
touch .env
echo "Project structure initialized. Run: python main.py to execute match orchestration."Step 1 — Foundation
Step 1 establishes the cricket domain model and match state infrastructure. You will define immutable Pydantic models representing cricketers, teams, innings, deliveries, and match context. The CricketMatch state object tracks the current inning, overs, runs, wickets, and match phase.
This foundational layer is critical because agents will reason about these objects and pass them between tool calls and state updates. Implementing it first ensures type safety, since Pydantic validates structure at every step and prevents agents from accidentally corrupting match state.
The models also serve as the shared semantic layer across all agents. Every agent interprets a Player object identically, understands Delivery events in the same way, and agrees on scorecard calculations. This consistency prevents the distributed intelligence problem, in which agents hallucinate inconsistent match states.
# models/cricket_domain.py
from pydantic import BaseModel, Field
from enum import Enum
from datetime import datetime
from typing import List, Optional
class CountryCode(str, Enum):
INDIA = "IND"
AUSTRALIA = "AUS"
PAKISTAN = "PAK"
ENGLAND = "ENG"
NEWZEALAND = "NZ"
class BattingStyle(str, Enum):
LEFT_HAND = "left"
RIGHT_HAND = "right"
class BowlingStyle(str, Enum):
FAST = "fast"
SPIN = "spin"
MEDIUM = "medium"
class CricketPlayer(BaseModel):
"""Represents an individual cricketer with stats and role."""
player_id: str
name: str
country: CountryCode
batting_style: BattingStyle
bowling_style: Optional[BowlingStyle] = None
average_strike_rate: float = Field(ge=70, le=150) # Validation: realistic range
average_boundary_percentage: float = Field(ge=0, le=100)
wicket_taking_rate: Optional[float] = None # Balls per wicket
economy_rate: Optional[float] = None # Runs per over
role: str = Field(default="batsman") # batsman, bowler, all-rounder
jersey_number: int = Field(ge=1, le=15)
match_fitness: float = Field(ge=0, le=1, default=1.0) # 0=injured, 1=fully fit
class Config:
use_enum_values = True
class CricketTeam(BaseModel):
"""Represents a cricket team with players and captain."""
team_id: str
team_name: str
country: CountryCode
squad: List[CricketPlayer]
captain_id: str
vice_captain_id: str
home_ground: str
odi_ranking: int = Field(ge=1, le=12)
class Delivery(BaseModel):
"""Represents a single delivery (ball) bowled in cricket."""
delivery_id: str
over_number: int = Field(ge=0, le=50)
ball_number: int = Field(ge=1, le=6)
bowler_name: str
batter_name: str
runs_scored: int = Field(ge=0, le=6)
is_boundary: bool
is_wicket: bool
wicket_type: Optional[str] = None # bowled, lbw, caught, etc.
bowler_economy_this_over: float = Field(ge=0, le=100)
timestamp: datetime
class Innings(BaseModel):
"""Represents one team's batting innings."""
innings_id: str
batting_team: CricketTeam
bowling_team: CricketTeam
total_runs: int = Field(ge=0, le=500)
total_wickets: int = Field(ge=0, le=10)
overs_completed: float = Field(ge=0, le=50) # e.g., 20.3 = 20 overs, 3 balls
deliveries: List[Delivery] = []
run_rate: float = Field(ge=0, le=20) # Current runs per over
powerplay_runs: int = Field(ge=0, le=150)
death_overs_runs: int = Field(ge=0, le=200)
highest_individual_score: int = Field(ge=0, le=150)
boundaries_hit: int = Field(ge=0, le=80)
class CricketMatch(BaseModel):
"""Represents a complete ODI cricket match."""
match_id: str
match_date: datetime
venue: str
toss_won_by: str # team name
toss_decision: str = Field(pattern="^(bat|bowl)$") # Toss winner's choice
team1: CricketTeam
team2: CricketTeam
innings1: Optional[Innings] = None
innings2: Optional[Innings] = None
match_phase: str = Field(default="pre_match") # pre_match, innings1, innings2, completed
winner: Optional[str] = None
winning_margin: Optional[str] = None # e.g., "by 25 runs" or "by 5 wickets"
man_of_match: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
class AgentThought(BaseModel):
"""Captures an agent's reasoning step."""
agent_name: str
timestamp: datetime = Field(default_factory=datetime.utcnow)
thought: str
tool_called: Optional[str] = None
tool_input: dict = {}
tool_result: Optional[str] = None
confidence: float = Field(ge=0, le=1)Step 2 — Core Logic
Step 2 implements the cricket mechanics tools and the orchestrator agent framework. Cricket mechanics are expressed as deterministic functions: calculate_run_rate takes a delivery log and returns runs per over, apply_powerplay_multiplier adjusts probabilities during overs 1 through 6, and determine_wicket_type applies logic for bowled versus caught outcomes based on delivery characteristics. These tools serve as the authoritative source of cricket rules—agents can invoke them with parameters but cannot override them.
The orchestrator agent acts as the master coordinator for the entire system. It receives a match setup consisting of two teams and a venue, reasons about strategy including toss analysis and opening pair selection, delegates tasks to subordinate agents such as the player-selection agent for XI composition and the simulation agent for delivery execution, and maintains the authoritative match state throughout.
This separation between business logic and reasoning logic is architecturally significant. By keeping cricket rules within deterministic tools and agent decision-making within the reasoning layer, both components remain independently testable and auditable.
# tools/cricket_mechanics.py
import random
from typing import Tuple, Dict
from models.cricket_domain import Delivery, Innings, CricketPlayer
from datetime import datetime
class CricketMechanicsEngine:
"""Deterministic cricket rule engine. Source of truth for match outcomes."""
@staticmethod
def simulate_delivery(
bowler: CricketPlayer,
batter: CricketPlayer,
over_number: int,
is_powerplay: bool,
bowler_state: Dict,
) -> Tuple[int, bool, str, float]:
"""
Simulate a single delivery outcome.
Returns: (runs_scored, is_wicket, wicket_type, bowler_economy)
"""
# Wicket probability increases if bowler is good, decreases if batter is in form
base_wicket_prob = 0.03 # 3% base wicket probability per delivery
if bowler.role == "bowler" and bowler.wicket_taking_rate:
base_wicket_prob += (6 - bowler.wicket_taking_rate) * 0.001
if batter.average_strike_rate > 120:
base_wicket_prob *= 0.7 # Batters in form less likely to get out
# Powerplay has different boundary probabilities
boundary_multiplier = 1.5 if is_powerplay else 1.0
boundary_prob = (batter.average_boundary_percentage / 100) * boundary_multiplier * 0.15
# Decision tree
rand = random.random()
if rand < base_wicket_prob:
wicket_types = ["bowled", "lbw", "caught", "leg_before_wicket"]
wicket = random.choice(wicket_types)
return (0, True, wicket, bowler.economy_rate or 5.0)
elif rand < base_wicket_prob + boundary_prob:
return (4, False, "", bowler.economy_rate or 5.0)
elif rand < base_wicket_prob + boundary_prob + (boundary_prob * 0.3):
return (6, False, "", (bowler.economy_rate or 5.0) + 1.0)
else:
# Dot ball or single
if random.random() < 0.6:
return (0, False, "", bowler.economy_rate or 5.0) # Dot ball
else:
return (1, False, "", bowler.economy_rate or 5.0) # Single
@staticmethod
def calculate_run_rate(innings: Innings) -> float:
"""Calculate current run rate (runs per over)."""
if innings.overs_completed == 0:
return 0.0
return round(innings.total_runs / innings.overs_completed, 2)
@staticmethod
def apply_powerplay_rules(over_number: int) -> bool:
"""Check if current over is in powerplay (first 6 overs in ODI)."""
return 0 <= over_number < 6
@staticmethod
def calculate_economy_rate(bowler_runs: int, overs_bowled: float) -> float:
"""Calculate bowler's economy (runs conceded per over)."""
if overs_bowled == 0:
return 0.0
return round(bowler_runs / overs_bowled, 2)
@staticmethod
def determine_match_winner(
innings1_runs: int,
innings2_runs: int,
innings1_team: str,
innings2_team: str,
) -> Tuple[str, str]:
"""Determine match winner and margin."""
if innings2_runs > innings1_runs:
margin = f"by {innings2_runs - innings1_runs} runs"
return (innings2_team, margin)
else:
margin = f"by {innings1_runs - innings2_runs} runs"
return (innings1_team, margin)
@staticmethod
def validate_match_state(innings: Innings) -> Tuple[bool, str]:
"""Validate innings state for consistency."""
if innings.total_wickets > 10:
return (False, "Wickets cannot exceed 10")
if innings.overs_completed > 50:
return (False, "Overs cannot exceed 50")
if innings.total_runs < 0:
return (False, "Runs cannot be negative")
return (True, "Valid innings state")
# agents/orchestrator/orchestrator_agent.py
from models.cricket_domain import CricketMatch, CricketTeam, Innings
from tools.cricket_mechanics import CricketMechanicsEngine
from datetime import datetime
import json
class OrchestratorAgent:
"""
Master agent that coordinates entire cricket match.
Responsibilities: toss logic, team selection, phase transitions, result determination.
"""
def __init__(self, match: CricketMatch, llm_client):
self.match = match
self.llm_client = llm_client
self.agent_id = "orchestrator-main"
self.reasoning_log = []
self.tools = {
"apply_powerplay_rules": CricketMechanicsEngine.apply_powerplay_rules,
"validate_match_state": CricketMechanicsEngine.validate_match_state,
"determine_match_winner": CricketMechanicsEngine.determine_match_winner,
}
async def execute_toss_logic(self) -> str:
"""
Reason about toss: which team won, what should they choose (bat/bowl)?
This demonstrates agent reasoning without simulation yet.
"""
prompt = f"""
Simulate a cricket toss between {self.match.team1.team_name} (#{self.match.team1.odi_ranking})
and {self.match.team2.team_name} (#{self.match.team2.odi_ranking}).
Venue: {self.match.venue}
Facts:
- {self.match.team1.team_name} home ground: {self.match.team1.home_ground}
- {self.match.team2.team_name} home ground: {self.match.team2.home_ground}
- {self.match.team1.team_name} ODI ranking: {self.match.team1.odi_ranking}
- {self.match.team2.team_name} ODI ranking: {self.match.team2.odi_ranking}
Decide:
1. Which team wins the toss (reason about form, recent results, captain experience)?
2. Should the toss-winning captain choose to bat or bowl first (consider ground, weather patterns)?
Respond with JSON: {{"toss_winner": "team_name", "choice": "bat|bowl", "reasoning": "explanation"}}
"""
# In production, call self.llm_client with prompt
# For now, return simulated decision
return json.dumps({
"toss_winner": self.match.team1.team_name,
"choice": "bat",
"reasoning": f"{self.match.team1.team_name} won toss, chose to bat first. Higher-ranked team prefers batting in favorable conditions."
})
def transition_phase(self, new_phase: str) -> None:
"""Safely transition match to next phase (pre_match -> innings1 -> innings2 -> completed)."""
valid_transitions = {
"pre_match": ["innings1"],
"innings1": ["innings2"],
"innings2": ["completed"],
}
if new_phase not in valid_transitions.get(self.match.match_phase, []):
raise ValueError(f"Invalid phase transition: {self.match.match_phase} -> {new_phase}")
self.match.match_phase = new_phase
self.reasoning_log.append({
"timestamp": datetime.utcnow().isoformat(),
"action": "phase_transition",
"from": self.match.match_phase,
"to": new_phase,
})Step 3 — Integration & Enhancement
Step 3 builds the player selection agent and the simulation agent, both of which work in tandem with the orchestrator. The selection agent receives team rosters and match context—including venue, toss decision, and opposition—and uses LLM reasoning to construct optimal playing XIs. It reasons about player form, role balance across openers, middle order, and tail, as well as match-ups against opposition bowlers.
The simulation agent executes the match delivery by delivery. It retrieves the current state from the orchestrator, invokes mechanics tools to simulate each delivery, updates innings state, detects match-ending conditions, and feeds the updated state back to the orchestrator. This pattern of passing state immutably and invoking shared tools is fundamental to production agentic workflows.
An enhancement layer adds analytics agents that compute live metrics such as run rate progression, partnership analysis, and momentum indicators. These metrics are fed back to the orchestrator to inform decisions about player substitutions or strategy changes as the match unfolds.
# agents/selection/player_selection_agent.py
from models.cricket_domain import CricketTeam, CricketPlayer
from typing import List
import json
class PlayerSelectionAgent:
"""
Autonomous agent responsible for constructing playing XI from squad.
Reasons about role balance, form, and match conditions.
"""
def __init__(self, llm_client):
self.agent_id = "selection-agent"
self.llm_client = llm_client
self.selection_log = []
async def select_playing_xi(
self,
squad: CricketTeam,
opposition: CricketTeam,
venue: str,
toss_decision: str,
) -> List[CricketPlayer]:
"""
Select 11 players from squad, reasoning about:
- Opposition strengths/weaknesses
- Venue conditions (home vs away)
- Player fitness and form
- Role balance (openers, middle order, bowlers)
"""
squad_summary = json.dumps([
{
"name": p.name,
"role": p.role,
"avg_strike_rate": p.average_strike_rate,
"fitness": p.match_fitness,
"economy_rate": p.economy_rate,
}
for p in squad.squad
])
prompt = f"""
You are selecting a cricket XI for {squad.team_name} against {opposition.team_name}.
Squad available:
{squad_summary}
Context:
- Venue: {venue}
- Toss decision: {toss_decision} (your team will {'bat' if toss_decision == 'bat' else 'bowl'} first)
- Opposition ranking: #{opposition.odi_ranking}
- Opposition strengths: {opposition.team_name} is a strong {'bowling' if opposition.odi_ranking < 5 else 'all-round'} team
Select exactly 11 players. Consider:
1. Role balance: 3 openers, 4 middle-order, 1 wicket-keeper, 3 bowlers
2. Player fitness: avoid any player with fitness < 0.8 unless critical
3. Home advantage if playing at home
Respond with JSON: {{
"selected_xi": ["player_name_1", "player_name_2", ...],
"captain": "captain_name",
"vice_captain": "vice_captain_name",
"reasoning": "brief explanation of selection strategy"
}}
"""
# In production: response = await self.llm_client.call(prompt)
# For demonstration, return hardcoded selection
selected_names = [p.name for p in squad.squad[:11]]
return [p for p in squad.squad if p.name in selected_names]
# agents/simulation/match_simulation_agent.py
from models.cricket_domain import (
CricketMatch, Delivery, Innings, CricketPlayer
)
from tools.cricket_mechanics import CricketMechanicsEngine
from datetime import datetime
import random
class MatchSimulationAgent:
"""
Executes match simulation delivery-by-delivery.
Maintains innings state, invokes mechanics tools, handles wicket logic.
"""
def __init__(self, match: CricketMatch):
self.match = match
self.agent_id = "simulation-agent"
self.mechanics = CricketMechanicsEngine()
self.delivery_count = 0
self.bowler_state = {} # Track bowler fatigue, economy by bowler
async def execute_delivery(
self,
innings: Innings,
bowler: CricketPlayer,
batter: CricketPlayer,
over_number: int,
ball_number: int,
) -> Delivery:
"""
Simulate one delivery: invoke mechanics tool, update state, return Delivery object.
"""
is_powerplay = self.mechanics.apply_powerplay_rules(over_number)
# Invoke deterministic tool
runs, is_wicket, wicket_type, economy = self.mechanics.simulate_delivery(
bowler=bowler,
batter=batter,
over_number=over_number,
is_powerplay=is_powerplay,
bowler_state=self.bowler_state,
)
# Create Delivery record
delivery = Delivery(
delivery_id=f"del-{innings.innings_id}-{self.delivery_count}",
over_number=over_number,
ball_number=ball_number,
bowler_name=bowler.name,
batter_name=batter.name,
runs_scored=runs,
is_boundary=(runs >= 4),
is_wicket=is_wicket,
wicket_type=wicket_type if is_wicket else None,
bowler_economy_this_over=economy,
timestamp=datetime.utcnow(),
)
# Update innings state
innings.total_runs += runs
if is_wicket:
innings.total_wickets += 1
if ball_number == 6:
innings.overs_completed += 1
if is_powerplay:
innings.powerplay_runs += runs
if over_number >= 44:
innings.death_overs_runs += runs
if runs >= 4:
innings.boundaries_hit += 1
innings.deliveries.append(delivery)
self.delivery_count += 1
# Update run rate
innings.run_rate = self.mechanics.calculate_run_rate(innings)
return delivery
def is_innings_complete(self, innings: Innings) -> bool:
"""Check if innings has ended (all out or 50 overs completed)."""
return innings.total_wickets == 10 or innings.overs_completed >= 50
# agents/analytics/analytics_agent.py
from models.cricket_domain import Innings
from typing import Dict
import statistics
class AnalyticsAgent:
"""
Computes live match analytics and insights.
Feeds metrics to orchestrator for decision-making.
"""
def __init__(self):
self.agent_id = "analytics-agent"
def compute_partnership_analysis(self, innings: Innings) -> Dict:
"""
Analyze current batting partnership.
"""
if len(innings.deliveries) < 5:
return {"status": "insufficient_data", "balls_faced": len(innings.deliveries)}
last_ten_deliveries = innings.deliveries[-10:]
runs_last_ten = sum(d.runs_scored for d in last_ten_deliveries)
balls_last_ten = len(last_ten_deliveries)
strike_rate = (runs_last_ten / balls_last_ten * 100) if balls_last_ten > 0 else 0
return {
"runs_last_10_balls": runs_last_ten,
"strike_rate_last_10": round(strike_rate, 2),
"momentum": "ascending" if strike_rate > 120 else "steady" if strike_rate > 80 else "under_pressure",
}
def compute_match_projection(self, innings: Innings) -> Dict:
"""
Project final score based on current run rate and overs remaining.
"""
overs_remaining = 50 - innings.overs_completed
if overs_remaining <= 0:
return {"projected_total": innings.total_runs, "status": "complete"}
projected_total = innings.total_runs + (innings.run_rate * overs_remaining)
return {
"current_runs": innings.total_runs,
"overs_remaining": overs_remaining,
"projected_total": round(projected_total, 0),
"required_run_rate": round(9 / overs_remaining, 2) if overs_remaining > 0 else 0,
}
def recommend_strategy_change(self, innings: Innings, opposition_total: int) -> str:
"""
Recommend strategy adjustments based on match state.
"""
projection = self.compute_match_projection(innings)
if projection["projected_total"] < opposition_total * 0.9:
return "ACCELERATE: Increase scoring to match opposition total"
elif projection["projected_total"] > opposition_total * 1.1:
return "CONSOLIDATE: Maintain current strategy, target achieved"
else:
return "STEADY: Continue current run rate, match progressing on track"Step 4 — Testing & Verification
With all components in place, run the integrated orchestrator system end-to-end using realistic cricket data. Execute a complete match simulation between two international teams and verify that the orchestrator transitions match phases correctly, agents coordinate without race conditions, and match state remains consistent across all 100 deliveries.
Validate that the final scorecard accurately reflects all deliveries, that wickets are tracked properly, and that the match winner is determined correctly. Use pytest to run both unit tests on individual agents and integration tests on multi-agent workflows to confirm the system behaves as expected at every level.
# commands/run_match_simulation.sh
#!/bin/bash
echo "=== Cricket Agentic Orchestrator - Match Simulation ==="
echo ""
# Set environment
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
# Run main match orchestration
echo "[1] Initializing match between India vs Australia..."
python3 -c "
import asyncio
from datetime import datetime
from models.cricket_domain import (
CricketMatch, CricketTeam, CricketPlayer, Innings,
CountryCode, BattingStyle, BowlingStyle
)
from agents.orchestrator.orchestrator_agent import OrchestratorAgent
from agents.simulation.match_simulation_agent import MatchSimulationAgent
from agents.analytics.analytics_agent import AnalyticsAgent
# Create sample teams with real player names
india_players = [
CricketPlayer(player_id='IND001', name='Rohit Sharma', country=CountryCode.INDIA,
batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=95.2,
average_boundary_percentage=35.0, role='batsman', jersey_number=45, match_fitness=1.0),
CricketPlayer(player_id='IND002', name='Shubman Gill', country=CountryCode.INDIA,
batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=92.1,
average_boundary_percentage=28.0, role='batsman', jersey_number=77, match_fitness=0.95),
CricketPlayer(player_id='IND003', name='Virat Kohli', country=CountryCode.INDIA,
batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=92.5,
average_boundary_percentage=32.0, role='batsman', jersey_number=18, match_fitness=1.0),
CricketPlayer(player_id='IND004', name='Suryakumar Yadav', country=CountryCode.INDIA,
batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=139.0,
average_boundary_percentage=45.0, role='batsman', jersey_number=63, match_fitness=0.9),
CricketPlayer(player_id='IND005', name='Rishabh Pant', country=CountryCode.INDIA,
batting_style=BattingStyle.LEFT_HAND, average_strike_rate=115.0,
average_boundary_percentage=38.0, role='batsman', jersey_number=17, match_fitness=0.85),
CricketPlayer(player_id='IND006', name='Jasprit Bumrah', country=CountryCode.INDIA,
bowling_style=BowlingStyle.FAST, average_strike_rate=0, average_boundary_percentage=0,
wicket_taking_rate=23.5, economy_rate=5.3, role='bowler', jersey_number=93, match_fitness=1.0),
CricketPlayer(player_id='IND007', name='Mohammed Shami', country=CountryCode.INDIA,
bowling_style=BowlingStyle.FAST, average_strike_rate=0, average_boundary_percentage=0,
wicket_taking_rate=25.0, economy_rate=5.8, role='bowler', jersey_number=11, match_fitness=0.9),
]
aus_players = [
CricketPlayer(player_id='AUS001', name='David Warner', country=CountryCode.AUSTRALIA,
batting_style=BattingStyle.LEFT_HAND, average_strike_rate=98.0,
average_boundary_percentage=40.0, role='batsman', jersey_number=31, match_fitness=1.0),
CricketPlayer(player_id='AUS002', name='Steve Smith', country=CountryCode.AUSTRALIA,
batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=85.0,
average_boundary_percentage=25.0, role='batsman', jersey_number=49, match_fitness=1.0),
CricketPlayer(player_id='AUS003', name='Glenn Maxwell', country=CountryCode.AUSTRALIA,
batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=135.0,
average_boundary_percentage=42.0, role='batsman', jersey_number=32, match_fitness=0.88),
CricketPlayer(player_id='AUS004', name='Mitchell Starc', country=CountryCode.AUSTRALIA,
bowling_style=BowlingStyle.FAST, average_strike_rate=0, average_boundary_percentage=0,
wicket_taking_rate=24.0, economy_rate=5.5, role='bowler', jersey_number=62, match_fitness=1.0),
]
# Create teams
india = CricketTeam(
team_id='IND', team_name='India', country=CountryCode.INDIA, squad=india_players,
captain_id='IND001', vice_captain_id='IND003', home_ground='Delhi',
odi_ranking=1
)
aus = CricketTeam(
team_id='AUS', team_name='Australia', country=CountryCode.AUSTRALIA, squad=aus_players,
captain_id='AUS001', vice_captain_id='AUS002', home_ground='Melbourne',
odi_ranking=2
)
# Create match
match = CricketMatch(
match_id='MATCH-IND-AUS-001',
match_date=datetime.utcnow(),
venue='Melbourne Cricket Ground',
toss_won_by='India',
toss_decision='bat',
team1=india,
team2=aus
)
print(f'[✓] Match created: {india.team_name} vs {aus.team_name}')
print(f'[✓] Venue: {match.venue}')
print(f'[✓] Toss: {match.toss_won_by} chose to {match.toss_decision}')
print()
# Orchestrator setup
orchestrator = OrchestratorAgent(match=match, llm_client=None)
print('[✓] Orchestrator agent initialized')
print(f' Agent ID: {orchestrator.agent_id}')
print()
# Simulation setup
simulator = MatchSimulationAgent(match=match)
analytics = AnalyticsAgent()
print('[✓] Simulation and analytics agents initialized')
print()
print('Match is ready for simulation. Run: python main.py')
"
echo ""
echo "[2] Running unit tests on cricket mechanics..."
python3 -m pytest tests/test_mechanics.py -v --tb=short 2>/dev/null || echo " [Note: Test file not yet created]"
echo ""
echo "[3] Expected output from full match simulation:"
echo " - 50 overs simulated with ~300 deliveries"
echo " - India 1st innings: 280-6 (50 overs)"
echo " - Australia 2nd innings: 275 all out (48.3 overs)"
echo " - Winner: India by 5 runs"
echo " - Man of Match: Jasprit Bumrah (3 wickets, economy 5.1)"
echo ""
echo "=== Simulation Ready ==="Warning: A common error is agents creating inconsistent match state—e.g., a simulation agent updating total_runs without calling the orchestrator's validation tool, leading to scorecard corruption. Always enforce that agents invoke CricketMechanicsEngine.validate_match_state() after state mutations and that only the orchestrator commits state changes to the authoritative match object. Use immutable patterns: agents should create new Innings objects rather than mutating existing ones. Implement a transaction-like pattern where state changes are validated before being accepted into the match record.
Extension Challenge: Enhance the system with a decision-making agent that makes in-match strategy calls: after every 10 overs, it evaluates innings.run_rate vs. projected_total and recommends field placements or bowling changes to the orchestrator. Implement DLS (Duckworth-Lewis-Stern) calculations so if rain interrupts, the target for the chasing team is recalculated dynamically and agents adapt strategy. Add a predictive agent that uses historical player vs. bowler match-ups to forecast probability of wickets in next over.
- Agentic decomposition separates business logic (cricket rules in tools) from reasoning logic (agents making decisions), enabling parallel development and testing of each layer independently.
- State immutability and validation are critical: agents pass state through tools that enforce cricket law constraints, preventing hallucinated impossible scenarios like 8-run deliveries.
- The orchestrator pattern works as a coordinator managing subsidiary agents: it maintains authoritative match state, sequences phase transitions, and resolves conflicts between agents' competing recommendations.
- Tool-calling is the synchronization mechanism: agents don't directly invoke other agents; instead, they invoke deterministic tools that apply domain rules, ensuring consistent outcomes regardless of agent reasoning drift.
- Multi-turn reasoning in agents enables them to reason about match context, justify decisions (toss strategy, XI selection), and adapt to feedback from tools before committing to actions.