harshraj22/croprl-workspace / code /time_controller.py
harshraj22's picture
download
raw
4.8 kB
"""
TimeController — Slot-based turn synchronisation for the multi-agent CropRL env.
Each calendar month is divided into K action slots. Every agent starts the
month with a budget of K slots. The month only advances when **every** agent
has either exhausted their budget or explicitly called End Turn (action 0).
Design choices aligned with the implementation plan:
- Fixed rotation: the "first agent" rotates each month to avoid positional bias.
- Slot ordering within a month is fixed (agent 0, 1, … N-1 take turns), but
the first active slot is awarded to a different agent each month.
- Agents that call End Turn early simply wait (blocked) while others finish.
"""
from __future__ import annotations
from typing import Dict, Optional
class TurnOverError(Exception):
"""Raised when an agent tries to act after calling End Turn this month."""
class TimeController:
"""
Manages the shared calendar month and per-agent action budgets.
Attributes
----------
month : int
Current calendar month (1-12).
year : int
Current year (1-based).
month_count : int
Total months elapsed since episode start.
"""
def __init__(self, num_agents: int, action_slots_per_month: int) -> None:
self.num_agents = num_agents
self.action_slots_per_month = action_slots_per_month
self.month: int = 1
self.year: int = 1
self.month_count: int = 0
# Per-agent bookkeeping
self._slots_used: Dict[int, int] = {i: 0 for i in range(num_agents)}
# Rotating first-agent index (changes each month, fair rotation)
self._first_agent_offset: int = 0
# ──────────────────────────────────────────────────────────────
# Public API
# ──────────────────────────────────────────────────────────────
def reset(self) -> None:
"""Reset the controller for a new episode."""
self.month = 1
self.year = 1
self.month_count = 0
self._first_agent_offset = 0
self._reset_month()
def slots_remaining(self, agent_id: int) -> int:
"""Return how many action slots agent *agent_id* has left this month."""
return max(0, self.action_slots_per_month - self._slots_used[agent_id])
def is_turn_done(self, agent_id: int) -> bool:
"""Return True if the agent has exhausted their slots for this month."""
return self._slots_used[agent_id] >= self.action_slots_per_month
def consume_slot(self, agent_id: int) -> None:
"""
Consume one action slot for *agent_id*.
Raises
------
ValueError
If the agent has no slots remaining (budget exhausted).
"""
if self._slots_used[agent_id] >= self.action_slots_per_month:
raise TurnOverError(
f"Agent {agent_id} has no action slots remaining this month."
)
self._slots_used[agent_id] += 1
def all_done(self) -> bool:
"""Return True when every agent has exhausted their action slots."""
return all(self._slots_used[i] >= self.action_slots_per_month for i in range(self.num_agents))
def advance_month(self) -> bool:
"""
Advance the calendar by one month. Returns True when a year boundary
is crossed (useful for triggering inflation in the outer env).
"""
old_month = self.month
self.month = (self.month % 12) + 1
self.month_count += 1
year_advanced = False
if self.month == 1 and old_month == 12:
self.year += 1
year_advanced = True
# Rotate the first-agent offset
self._first_agent_offset = (self._first_agent_offset + 1) % self.num_agents
self._reset_month()
return year_advanced
def current_slot_for(self, agent_id: int) -> int:
"""Return the slot index (0-based) the agent is *currently* on."""
return self._slots_used[agent_id]
# ──────────────────────────────────────────────────────────────
# Internal helpers
# ──────────────────────────────────────────────────────────────
def _reset_month(self) -> None:
"""Reset per-agent slot budgets for a new month."""
for i in range(self.num_agents):
self._slots_used[i] = 0

Xet Storage Details

Size:
4.8 kB
·
Xet hash:
6d26f12063200dc02096f21429a48db1ae969ca0d688947629704f2dbc0a22b3

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.