diff --git a/demo_ball_placement.py b/demo_ball_placement.py
new file mode 100644
index 00000000..166d5529
--- /dev/null
+++ b/demo_ball_placement.py
@@ -0,0 +1,146 @@
+"""demo_ball_placement.py — Entry point for the ball placement feature.
+
+Run:
+ pixi run python demo_ball_placement.py
+ # RSim window opens; open http://localhost:8080 in a browser
+
+What this sets up
+-----------------
+- Exhibition Road field (4 m × 3 m, ``GREAT_EXHIBITION_FIELD_DIMS``)
+- 2v2 format: two yellow robots (your team) vs two blue robots that
+ deliberately push the ball out once per side, then attack the yellow goal
+- CustomReferee pre-configured with:
+ - Goal detection enabled (issues PREPARE_KICKOFF → NORMAL_START cycle)
+ - Out-of-bounds enabled (issues STOP → BALL_PLACEMENT_YELLOW → DIRECT_FREE)
+ - Defense area and keep-out rules OFF (less noise during development)
+ - Auto-advance fully enabled so state transitions fire automatically —
+ you can watch the full placement → free-kick → restart cycle without
+ touching the GUI
+- Browser GUI at http://localhost:8080 — use "Manual Commands" to fire
+ BALL_PLACEMENT_YELLOW at any time and set the target position
+
+Workflow
+--------
+1. Run this script and open http://localhost:8080.
+2. Let the blue robots push the ball out of bounds, or use the GUI "Manual
+ Commands" panel to issue BALL_PLACEMENT_YELLOW.
+3. Watch one of your robots (yellow) drive to the ball, capture it with the
+ dribbler, and carry it to the target circle shown in the GUI.
+4. After the referee advances to NORMAL_START, yellow kicks once toward the
+ configured blue robot target.
+"""
+
+from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
+from utama_core.custom_referee import CustomReferee
+from utama_core.custom_referee.profiles.profile_loader import (
+ AutoAdvanceConfig,
+ DefenseAreaConfig,
+ GameConfig,
+ GoalDetectionConfig,
+ KeepOutConfig,
+ OutOfBoundsConfig,
+ RefereeProfile,
+ RulesConfig,
+)
+from utama_core.run import StrategyRunner
+from utama_core.strategy.examples.ball_placement_and_kick_strategy import (
+ BallPlacementAndKickStrategy,
+)
+from utama_core.strategy.examples.deliberate_out_of_bounds_strategy import (
+ DeliberateOutOfBoundsStrategy,
+)
+
+# ---------------------------------------------------------------------------
+# Configuration
+# ---------------------------------------------------------------------------
+
+GUI_PORT = 8080
+N_ROBOTS = 2
+MY_TEAM_IS_YELLOW = True
+MY_TEAM_IS_RIGHT = True
+
+
+# ---------------------------------------------------------------------------
+# Referee profile
+#
+# Out-of-bounds is the primary trigger for ball placement in a real game.
+# Defense area and keep-out rules are disabled to reduce noise while you're
+# developing the placement skill itself.
+# ---------------------------------------------------------------------------
+
+_BALL_PLACEMENT_PROFILE = RefereeProfile(
+ profile_name="ball_placement_dev",
+ rules=RulesConfig(
+ goal_detection=GoalDetectionConfig(
+ enabled=True,
+ cooldown_seconds=1.0,
+ ),
+ out_of_bounds=OutOfBoundsConfig(
+ enabled=True,
+ free_kick_assigner="last_touch",
+ ),
+ defense_area=DefenseAreaConfig(
+ enabled=False,
+ max_defenders=1,
+ attacker_infringement=False,
+ ),
+ keep_out=KeepOutConfig(
+ enabled=False,
+ radius_meters=0.3,
+ violation_persistence_frames=30,
+ ),
+ ),
+ game=GameConfig(
+ half_duration_seconds=300.0,
+ kickoff_team="yellow",
+ force_start_after_goal=False,
+ stop_duration_seconds=2.0,
+ prepare_duration_seconds=3.0,
+ kickoff_timeout_seconds=10.0,
+ auto_advance=AutoAdvanceConfig(
+ # All auto-advance enabled: state machine drives itself so you can
+ # observe the full placement → free-kick → normal-start cycle.
+ stop_to_next_command=True,
+ prepare_kickoff_to_normal=True,
+ prepare_penalty_to_normal=True,
+ direct_free_to_normal=True,
+ ball_placement_to_next=True,
+ normal_start_to_force=True,
+ ),
+ ),
+)
+
+# ---------------------------------------------------------------------------
+# Entry point
+# ---------------------------------------------------------------------------
+
+
+def main() -> None:
+ referee = CustomReferee(
+ _BALL_PLACEMENT_PROFILE,
+ n_robots_yellow=N_ROBOTS,
+ n_robots_blue=N_ROBOTS,
+ enable_gui=True,
+ gui_port=GUI_PORT,
+ )
+
+ runner = StrategyRunner(
+ strategy=BallPlacementAndKickStrategy(),
+ # Opponents create one out-of-bounds event per side, then attack yellow's goal.
+ opp_strategy=DeliberateOutOfBoundsStrategy(field_dims=GREAT_EXHIBITION_FIELD_DIMS),
+ my_team_is_yellow=MY_TEAM_IS_YELLOW,
+ my_team_is_right=MY_TEAM_IS_RIGHT,
+ mode="rsim",
+ control_scheme="pid",
+ exp_friendly=N_ROBOTS,
+ exp_enemy=N_ROBOTS,
+ full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
+ referee=referee,
+ show_live_status=True,
+ )
+
+ runner.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/demo_ball_placement_real.py b/demo_ball_placement_real.py
new file mode 100644
index 00000000..5c02667c
--- /dev/null
+++ b/demo_ball_placement_real.py
@@ -0,0 +1,138 @@
+"""demo_ball_placement_real.py — Test ball placement with a single real robot.
+
+Run:
+ pixi run python demo_ball_placement_real.py
+ # Then open http://localhost:8080 in a browser
+
+What this sets up
+-----------------
+- Exhibition Road field (4 m x 3 m)
+- 1 yellow robot, no opponents
+- CustomReferee starts in HALT — the robot will not move until you issue a
+ command from the GUI
+- All automatic rule detection (out-of-bounds, goal, keep-out) is disabled so
+ the only commands come from the operator via the GUI
+
+Workflow
+--------
+1. Run this script and open http://localhost:8080.
+2. Place the ball somewhere on the field by hand.
+3. In the GUI "Manual Commands" panel, set a target position and issue
+ BALL_PLACEMENT_YELLOW.
+4. The robot drives to the ball with its dribbler on and attempts to carry it
+ to the target.
+5. Issue HALT from the GUI at any time to stop the robot immediately.
+
+Safety notes
+------------
+- The robot starts in HALT and will not move until you issue a command.
+- Keep the target position within the physical field bounds.
+- Issue HALT from the GUI before approaching the field.
+"""
+
+from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
+from utama_core.custom_referee import CustomReferee
+from utama_core.custom_referee.profiles.profile_loader import (
+ AutoAdvanceConfig,
+ DefenseAreaConfig,
+ GameConfig,
+ GoalDetectionConfig,
+ KeepOutConfig,
+ OutOfBoundsConfig,
+ RefereeProfile,
+ RulesConfig,
+)
+from utama_core.run import StrategyRunner
+from utama_core.strategy.examples.ball_placement_strategy import BallPlacementStrategy
+
+# ---------------------------------------------------------------------------
+# Configuration -- edit these to match your setup
+# ---------------------------------------------------------------------------
+
+GUI_PORT = 8080
+MY_TEAM_IS_YELLOW = True
+MY_TEAM_IS_RIGHT = True
+
+YELLOW_VISION_TO_CMD: dict[int, int] = {
+ 1: 3, # vision 1 → firmware 3
+}
+
+# ---------------------------------------------------------------------------
+# Referee profile
+#
+# All automatic rules are OFF. Commands come from the operator only.
+# auto_advance is also OFF so the state never transitions without you.
+# ---------------------------------------------------------------------------
+
+_REAL_TEST_PROFILE = RefereeProfile(
+ profile_name="ball_placement_real_test",
+ rules=RulesConfig(
+ goal_detection=GoalDetectionConfig(
+ enabled=False,
+ cooldown_seconds=1.0,
+ ),
+ out_of_bounds=OutOfBoundsConfig(
+ enabled=False,
+ free_kick_assigner="last_touch",
+ ),
+ defense_area=DefenseAreaConfig(
+ enabled=False,
+ max_defenders=1,
+ attacker_infringement=False,
+ ),
+ keep_out=KeepOutConfig(
+ enabled=False,
+ radius_meters=0.3,
+ violation_persistence_frames=30,
+ ),
+ ),
+ game=GameConfig(
+ half_duration_seconds=300.0,
+ kickoff_team="yellow",
+ force_start_after_goal=False,
+ stop_duration_seconds=2.0,
+ prepare_duration_seconds=3.0,
+ kickoff_timeout_seconds=10.0,
+ auto_advance=AutoAdvanceConfig(
+ stop_to_next_command=False,
+ prepare_kickoff_to_normal=False,
+ prepare_penalty_to_normal=False,
+ direct_free_to_normal=False,
+ ball_placement_to_next=False,
+ normal_start_to_force=False,
+ ),
+ ),
+)
+
+# ---------------------------------------------------------------------------
+# Entry point
+# ---------------------------------------------------------------------------
+
+
+def main() -> None:
+ referee = CustomReferee(
+ _REAL_TEST_PROFILE,
+ n_robots_yellow=1,
+ n_robots_blue=0,
+ enable_gui=True,
+ gui_port=GUI_PORT,
+ )
+
+ runner = StrategyRunner(
+ strategy=BallPlacementStrategy(),
+ my_team_is_yellow=MY_TEAM_IS_YELLOW,
+ my_team_is_right=MY_TEAM_IS_RIGHT,
+ mode="real",
+ exp_friendly=1,
+ exp_enemy=0,
+ full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
+ referee=referee,
+ yellow_vision_to_cmd_mapping=YELLOW_VISION_TO_CMD,
+ show_live_status=True,
+ )
+
+ runner.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/custom_referee.md b/docs/custom_referee.md
index 41e20283..fead17a9 100644
--- a/docs/custom_referee.md
+++ b/docs/custom_referee.md
@@ -151,7 +151,7 @@ Fires when `abs(ball.p.x) > half_length` (not in a goal) or `abs(ball.p.y) > hal
1. Checking `robot.has_ball` (reliable IR sensor on friendly robots).
2. Falling back to the closest robot within 0.15 m.
-The non-touching team receives the `DIRECT_FREE_*`. The `designated_position` is placed 0.1 m infield from the nearest boundary point.
+The non-touching team receives the `DIRECT_FREE_*`. The `designated_position` is placed 0.25 m infield from the nearest boundary point so the restart is playable by the robot/dribbler geometry.
### DefenseAreaRule
diff --git a/main.py b/main.py
index 75cd4870..559f3cbc 100644
--- a/main.py
+++ b/main.py
@@ -22,10 +22,10 @@ def main():
custom_bounds = FieldBounds(top_left=(-2, 1.5), bottom_right=(1, -1.5))
runner = StrategyRunner(
- strategy=RandomMovementStrategy(n_robots=2, field_bounds=custom_bounds, endpoint_tolerance=0.1, seed=42),
+ strategy=StartupStrategy(),
my_team_is_yellow=True,
my_team_is_right=True,
- mode="rsim",
+ mode="grsim",
exp_friendly=2,
exp_enemy=0,
replay_writer_config=ReplayWriterConfig(replay_name="test_replay", overwrite_existing=True),
diff --git a/pixi.lock b/pixi.lock
index 589c8cdb..0f745c15 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -7,411 +7,402 @@ environments:
- https://pypi.org/simple
packages:
linux-64:
- - conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2
- - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2
+ - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda
- conda: https://prefix.dev/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2
- conda: https://prefix.dev/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2
- conda: https://prefix.dev/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda
- conda: https://prefix.dev/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda
- - conda: https://prefix.dev/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda
- - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/backports.zstd-1.4.0-py311h6b1f9c4_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda
+ - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/click-8.3.1-pyh707e725_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/cloudpickle-3.1.2-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/click-8.3.3-pyhc90fa1f_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda
- conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/cryptography-46.0.3-py311h8488d03_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/cryptography-48.0.0-py311h2005dd1_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda
- conda: https://prefix.dev/conda-forge/noarch/distlib-0.4.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/editables-0.6-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/epoxy-1.5.10-hb03c661_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/farama-notifications-0.0.4-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/filelock-3.20.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/fluidsynth-2.3.7-hd992666_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/filelock-3.29.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/fluidsynth-2.5.4-h07a5eef_0.conda
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda
- - conda: https://prefix.dev/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/fontconfig-2.17.1-h27c8c51_0.conda
- conda: https://prefix.dev/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/gdk-pixbuf-2.44.4-h2b0a6b4_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/glib-tools-2.86.4-hf516916_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/glib-tools-2.88.1-hcfc306f_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
- conda: https://prefix.dev/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/gtk3-3.24.43-h993cebd_6.conda
- conda: https://prefix.dev/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda
- - conda: https://prefix.dev/conda-forge/linux-64/gymnasium-1.0.0-np2py311hec5889a_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/gymnasium-1.3.0-np2py311hec5889a_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda
- conda: https://prefix.dev/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/harfbuzz-12.3.2-h6083320_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/hatch-1.14.1-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/hatchling-1.27.0-pypyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2
+ - conda: https://prefix.dev/conda-forge/linux-64/harfbuzz-11.5.1-h15599e2_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/hatch-1.16.5-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/hatchling-1.29.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
- conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
- conda: https://prefix.dev/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/hyperlink-21.0.0-pyh29332c3_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/jack-1.9.22-hf4617a5_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhd8ed1ab_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.0.1-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.3.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda
+ - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.1.2-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda
- conda: https://prefix.dev/conda-forge/noarch/jax-jumpy-1.0.0-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/keyring-25.7.0-pyha804496_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2
- - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libavif16-1.3.0-h6395336_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libcap-2.77-h3ff7636_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libavif16-1.4.1-hcfa2d63_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libblas-3.11.0-6_h4a7cf45_openblas.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libcap-2.77-hd0affe5_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
- conda: https://prefix.dev/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda
- conda: https://prefix.dev/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgd-2.3.3-h5fbf134_12.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda
- conda: https://prefix.dev/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda
- conda: https://prefix.dev/conda-forge/linux-64/libgl-devel-1.7.0-ha4b6fd6_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libglib-2.86.4-h6548e54_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libglib-2.88.1-h0d30a3d_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda
- conda: https://prefix.dev/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda
- conda: https://prefix.dev/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
- conda: https://prefix.dev/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda
- - conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/liblapack-3.11.0-6_h47877c9_openblas.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libmad-0.15.1b-h0b41bf4_1001.conda
- conda: https://prefix.dev/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libopus-1.5.2-hd0c01bc_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libpng-1.6.55-h421ea60_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libprotobuf-3.20.2-h6239696_0.tar.bz2
- - conda: https://prefix.dev/conda-forge/linux-64/librsvg-2.60.0-h61e6d4b_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libsystemd0-257.10-hd0affe5_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/librsvg-2.60.2-h61e6d4b_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libsystemd0-257.13-hd0affe5_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libudev1-257.10-hd0affe5_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libudev1-257.13-hd0affe5_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libunwind-1.8.3-h65a8314_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/liburing-2.12-hb700be7_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/liburing-2.14-hb700be7_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda
- conda: https://prefix.dev/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libxml2-16-2.15.1-hca6bf5a_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libxml2-2.15.1-he237659_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libxml2-16-2.15.1-ha9997c6_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libxml2-2.15.1-h26afc86_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda
+ - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/more-itertools-10.8.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/more-itertools-11.0.2-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/numpy-2.0.2-py311h71ddf71_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/opusfile-0.12-h3358134_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/pandas-3.0.1-py311h8032f78_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda
- conda: https://prefix.dev/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/portaudio-19.6.0-h7c63dc7_9.conda
- - conda: https://prefix.dev/conda-forge/linux-64/portmidi-2.0.7-h3cb78e3_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pre-commit-4.4.0-pyha770c72_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/portaudio-19.7.0-hf4617a5_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/portmidi-2.0.8-h3cb78e3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pre-commit-4.6.0-pyha770c72_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/protobuf-3.20.2-py311ha362b79_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- conda: https://prefix.dev/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/pulseaudio-client-17.0-h9a6aba3_3.conda
- conda: https://prefix.dev/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/pygame-2.6.1-py311h3209946_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pyserial-3.5-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pyproject_hooks-1.2.0-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pyserial-3.5-pyhcf101f3_2.conda
- conda: https://prefix.dev/conda-forge/noarch/pytest-8.3.4-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/python-3.11.15-hd63d673_0_cpython.conda
- conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda
+ - conda: https://prefix.dev/conda-forge/noarch/python-discovery-1.3.0-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda
- - conda: https://prefix.dev/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/rav1e-0.7.1-h8fae777_3.conda
- - conda: https://prefix.dev/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/ruff-0.14.5-h813ae00_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.17.0-py311hbe70eeb_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/rav1e-0.8.1-h1fbca29_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/rich-14.3.4-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.17.1-py311hbe70eeb_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/sdl2-2.32.56-h54a6638_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/sdl2_image-2.8.2-h06ee604_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/sdl2_mixer-2.6.3-h8830914_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/sdl2_ttf-2.24.0-h287479f_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/sdl3-3.2.26-h68140b3_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/sdl2_mixer-2.6.3-hb8bcbd3_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/sdl2_ttf-2.24.0-hc1d668e_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/sdl3-3.4.8-hdeec2a5_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/secretstorage-3.4.1-py311h38be061_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda
- conda: https://prefix.dev/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
- conda: https://prefix.dev/conda-forge/noarch/snakeviz-2.2.2-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda
- - conda: https://prefix.dev/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/svt-av1-4.0.1-hecca717_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/noarch/tomli-w-1.2.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/tornado-6.5.2-py311h49ec1c0_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/trove-classifiers-2025.11.14.15-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.0-pyha770c72_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/tornado-6.5.5-py311h49ec1c0_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/trove-classifiers-2026.5.7.17-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/ukkonen-1.0.1-py311hdf67eae_6.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/ukkonen-1.1.0-py311hdf67eae_0.conda
- conda: https://prefix.dev/conda-forge/noarch/userpath-1.9.2-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/uv-0.9.10-h76e24b7_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/virtualenv-20.35.4-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/uv-0.11.12-h29f5ae7_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/virtualenv-21.3.1-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xkeyboard-config-2.47-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xorg-libxcomposite-0.4.7-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xorg-libxinerama-1.1.6-hecca717_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxrandr-1.5.5-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxxf86vm-1.1.7-hb03c661_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xorg-xorgproto-2024.1-hb9d3cd8_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xorg-xorgproto-2025.1-hb03c661_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda
+ - conda: https://prefix.dev/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/fb/06/6a7c30c3519181263e1bbf0cbea67ef4a43a64443ee16717c7d760214943/lenses-1.2.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7d/0a/2e0c30342586dabb0b4b7946393a7aeb725fca7b4549fb02e128a1a0fe87/py_trees-2.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl
osx-arm64:
+ - conda: https://prefix.dev/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda
- conda: https://prefix.dev/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda
- conda: https://prefix.dev/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda
- - conda: https://prefix.dev/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
- - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/cairo-1.18.4-he0f2337_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/backports.zstd-1.4.0-py311h36d4fbb_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda
+ - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/cffi-2.0.0-py311hd10dc20_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/click-8.3.1-pyh707e725_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/cloudpickle-3.1.2-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/click-8.3.3-pyhc90fa1f_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda
- conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/dbus-1.16.2-hda038a8_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/dbus-1.16.2-h3ff7a7c_1.conda
- conda: https://prefix.dev/conda-forge/noarch/distlib-0.4.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/editables-0.6-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/farama-notifications-0.0.4-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/filelock-3.20.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/fluidsynth-2.3.7-h80fea77_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/filelock-3.29.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/fluidsynth-2.5.4-h985f16e_0.conda
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/fontconfig-2.17.1-h2b252f5_0.conda
- conda: https://prefix.dev/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2
- conda: https://prefix.dev/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/freetype-2.14.3-hce30654_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/gdk-pixbuf-2.44.4-h7542897_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/gettext-0.25.1-h3dcc1bd_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/gettext-tools-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/glib-tools-2.86.4-h60c1bae_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/glib-tools-2.88.1-h357b478_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/gtk3-3.24.43-h5febe37_6.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/gymnasium-1.0.0-np2py311he8d3834_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/gymnasium-1.3.0-np2py311h56d0776_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda
- conda: https://prefix.dev/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/harfbuzz-12.3.2-h3103d1b_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/hatch-1.14.1-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/hatchling-1.27.0-pypyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2
+ - conda: https://prefix.dev/conda-forge/osx-arm64/harfbuzz-11.5.1-haf38c7b_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/hatch-1.16.5-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/hatchling-1.29.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
- conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
- conda: https://prefix.dev/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/hyperlink-21.0.0-pyh29332c3_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/icu-78.2-h38cb7af_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhd8ed1ab_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.0.1-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.3.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda
+ - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.1.2-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda
- conda: https://prefix.dev/conda-forge/noarch/jax-jumpy-1.0.0-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/noarch/keyring-25.7.0-pyh534df25_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/lame-3.100-h1a8c8d9_1003.tar.bz2
- - conda: https://prefix.dev/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libasprintf-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libasprintf-devel-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libavif16-1.3.0-hb06b76e_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.11.0-1_h51639a9_openblas.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.11.0-1_hb0561ab_openblas.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-21.1.6-hf598326_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libavif16-1.4.1-hfce71f6_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libflac-1.4.3-hb765f3a_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libgd-2.3.3-h05bcc79_12.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libgettextpo-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libgettextpo-devel-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libglib-2.86.4-he378b5c_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libflac-1.5.0-h6824b09_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_19.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libintl-devel-0.25.1-h493aca8_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/liblapack-3.11.0-1_hd9741b5_openblas.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/liblapack-3.11.0-6_hd9741b5_openblas.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libmad-0.15.1b-h1a8c8d9_1001.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libopus-1.5.2-h48c0fde_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libpng-1.6.55-h132b30e_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libopus-1.6.1-h1a92334_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libprotobuf-3.20.2-hb5ab8b9_0.tar.bz2
- - conda: https://prefix.dev/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libsndfile-1.2.2-h9739721_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.51.2-h1ae2325_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/librsvg-2.60.0-h9001022_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libsndfile-1.2.2-hf95f74e_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libusb-1.0.29-hbc156a2_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libvorbis-1.3.7-h81086ad_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libvulkan-loader-1.4.328.1-h49c215f_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libvulkan-loader-1.4.341.0-h3feff0a_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libxml2-16-2.15.1-h5ef1a60_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/llvm-openmp-21.1.6-h4a912ad_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/more-itertools-10.8.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/more-itertools-11.0.2-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/mpg123-1.32.9-hf642e45_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/numpy-2.0.2-py311h649a571_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/opusfile-0.12-h5643135_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/pandas-3.0.1-py311h4a068b9_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda
- conda: https://prefix.dev/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda
- - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/portaudio-19.6.0-h13dd4ca_9.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/portmidi-2.0.7-haf25636_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pre-commit-4.4.0-pyha770c72_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/portaudio-19.7.0-h5833ebf_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/portmidi-2.0.8-haf25636_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pre-commit-4.6.0-pyha770c72_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/protobuf-3.20.2-py311ha397e9f_1.conda
- conda: https://prefix.dev/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/pygame-2.6.1-py311hc3e7c57_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/pyserial-3.5-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pyproject_hooks-1.2.0-pyhd8ed1ab_1.conda
+ - conda: https://prefix.dev/conda-forge/noarch/pyserial-3.5-pyhcf101f3_2.conda
- conda: https://prefix.dev/conda-forge/noarch/pytest-8.3.4-pyhd8ed1ab_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.11.15-h8561d8f_0_cpython.conda
- conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda
+ - conda: https://prefix.dev/conda-forge/noarch/python-discovery-1.3.0-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/pyyaml-6.0.3-py311ha9b3269_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/rav1e-0.7.1-h0716509_3.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/ruff-0.14.5-h382de68_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.17.0-py311he9931d0_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/rav1e-0.8.1-h8246384_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/rich-14.3.4-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.17.1-py311he9931d0_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/sdl2-2.32.56-h248ca61_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_image-2.8.2-h376e2e1_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_mixer-2.6.3-h4fe3bdc_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_ttf-2.24.0-h443c5de_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/sdl3-3.2.26-h919df07_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_mixer-2.6.3-h45fadf6_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_ttf-2.24.0-h49b19f9_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/sdl3-3.4.8-h6fa9c73_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda
- conda: https://prefix.dev/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
- conda: https://prefix.dev/conda-forge/noarch/snakeviz-2.2.2-pyhd8ed1ab_1.conda
- conda: https://prefix.dev/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/svt-av1-4.0.1-h0cb729a_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/noarch/tomli-w-1.2.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/tornado-6.5.2-py311h9408147_2.conda
- - conda: https://prefix.dev/conda-forge/noarch/trove-classifiers-2025.11.14.15-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.0-pyha770c72_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/tornado-6.5.5-py311hc949640_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/trove-classifiers-2026.5.7.17-pyhd8ed1ab_0.conda
- conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/ukkonen-1.0.1-py311h57a9ea7_6.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/ukkonen-1.1.0-py311h572238d_0.conda
- conda: https://prefix.dev/conda-forge/noarch/userpath-1.9.2-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/uv-0.9.10-h1bde295_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/virtualenv-20.35.4-pyhd8ed1ab_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/uv-0.11.12-h2c65d96_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/virtualenv-21.3.1-pyhcf101f3_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda
+ - conda: https://prefix.dev/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/fb/06/6a7c30c3519181263e1bbf0cbea67ef4a43a64443ee16717c7d760214943/lenses-1.2.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7d/0a/2e0c30342586dabb0b4b7946393a7aeb725fca7b4549fb02e128a1a0fe87/py_trees-2.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl
@@ -423,116 +414,117 @@ environments:
- https://pypi.org/simple
packages:
linux-64:
- - conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2
- - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2
- - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda
- - conda: https://prefix.dev/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/cmake-3.26.4-hcfe8598_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/expat-2.7.3-hecca717_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/expat-2.8.0-hecca717_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
- conda: https://prefix.dev/conda-forge/linux-64/libccd-double-2.1-h59595ed_3.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libev-4.33-hd590300_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/libode-0.16.6-h18afd86_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda
- conda: https://prefix.dev/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda
- - conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda
- - conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- - conda: https://prefix.dev/conda-forge/linux-64/python-3.10.19-h3c07f61_2_cpython.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/python-3.10.20-h3c07f61_0_cpython.conda
- conda: https://prefix.dev/conda-forge/noarch/python_abi-3.10-8_cp310.conda
- - conda: https://prefix.dev/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda
- conda: https://prefix.dev/conda-forge/linux-64/rhash-1.4.3-hd590300_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda
- - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda
- - conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
+ - conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/d3/90/5c5960e30acd13eb43a780023761218612d26dd540d90b15edfef730b180/rc_robosim-1.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
osx-arm64:
- - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda
- - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda
+ - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/cmake-3.26.4-hc0af03a_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/expat-2.7.3-haf25636_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/expat-2.8.0-hf6b4638_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libccd-double-2.1-h9a09cb3_2.tar.bz2
- - conda: https://prefix.dev/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-21.1.6-hf598326_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-devel-5.8.3-h8088a28_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libode-0.16.6-py310h46abf29_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.51.0-h8adb53f_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.10.19-hcd7f573_2_cpython.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.10.20-h1b19095_0_cpython.conda
- conda: https://prefix.dev/conda-forge/noarch/python_abi-3.10-8_cp310.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda
- conda: https://prefix.dev/conda-forge/osx-arm64/rhash-1.4.3-hb547adb_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda
- - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda
- - conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/xz-5.8.3-hd0f0c4f_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/xz-gpl-tools-5.8.3-hd0f0c4f_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/xz-tools-5.8.3-h8088a28_0.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
+ - conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl
- pypi: https://files.pythonhosted.org/packages/f0/af/fbd88656ad694164defc69d032faf606d938db05051c8f1f87449e42e70a/rc-robosim-1.2.tar.gz
packages:
-- conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2
- sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726
- md5: d7c89558ba9fa0495403155b64376d81
- license: None
- purls: []
- size: 2562
- timestamp: 1578324546067
-- conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2
- build_number: 16
- sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22
- md5: 73aaf86a425cc6e73fcf236a5a46396d
- depends:
- - _libgcc_mutex 0.1 conda_forge
+- conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda
+ build_number: 20
+ sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9
+ md5: a9f577daf3de00bca7c3c76c0ecbd1de
+ depends:
+ - __glibc >=2.17,<3.0.a0
- libgomp >=7.5.0
constrains:
- - openmp_impl 9999
+ - openmp_impl <0.0a0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 28948
+ timestamp: 1770939786096
+- conda: https://prefix.dev/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda
+ build_number: 7
+ sha256: 7acaa2e0782cad032bdaf756b536874346ac1375745fb250e9bdd6a48a7ab3cd
+ md5: a44032f282e7d2acdeb1c240308052dd
+ depends:
+ - llvm-openmp >=9.0.1
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 23621
- timestamp: 1650670423406
+ size: 8325
+ timestamp: 1764092507920
- conda: https://prefix.dev/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
sha256: a362b4f5c96a0bf4def96be1a77317e2730af38915eb9bec85e2a92836501ed7
md5: b3f0179590f3c0637b7eb5309898f79e
@@ -556,25 +548,25 @@ packages:
purls: []
size: 584660
timestamp: 1768327524772
-- conda: https://prefix.dev/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda
- sha256: 7378b5b9d81662d73a906fabfc2fb81daddffe8dc0680ed9cda7a9562af894b0
- md5: 814472b61da9792fae28156cb9ee54f5
+- conda: https://prefix.dev/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
+ sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e
+ md5: af2df4b9108808da3dc76710fe50eae2
depends:
- exceptiongroup >=1.0.2
- idna >=2.8
- python >=3.10
- - sniffio >=1.1
- typing_extensions >=4.5
- python
constrains:
- - trio >=0.31.0
- - uvloop >=0.21
+ - trio >=0.32.0
+ - uvloop >=0.22.1
+ - winloop >=0.2.3
license: MIT
license_family: MIT
purls:
- pkg:pypi/anyio?source=hash-mapping
- size: 138159
- timestamp: 1758634638734
+ size: 146764
+ timestamp: 1774359453364
- conda: https://prefix.dev/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda
sha256: b08ef033817b5f9f76ce62dfcac7694e7b6b4006420372de22494503decac855
md5: 346722a0be40f6edc53f12640d301338
@@ -655,17 +647,6 @@ packages:
purls: []
size: 347530
timestamp: 1713896411580
-- conda: https://prefix.dev/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda
- sha256: a9c114cbfeda42a226e2db1809a538929d2f118ef855372293bd188f71711c48
- md5: 791365c5f65975051e4e017b5da3abf5
- depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=13
- license: GPL-2.0-or-later
- license_family: GPL
- purls: []
- size: 68072
- timestamp: 1756738968573
- conda: https://prefix.dev/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda
sha256: e1c3dc8b5aa6e12145423fed262b4754d70fec601339896b9ccf483178f690a6
md5: 767d508c1a67e02ae8f50e44cacfadb2
@@ -676,126 +657,152 @@ packages:
purls: []
size: 7069
timestamp: 1733218168786
-- conda: https://prefix.dev/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda
- sha256: a0f41db6d7580cec3c850e5d1b82cb03197dd49a3179b1cee59c62cd2c761b36
- md5: df837d654933488220b454c6a3b0fad6
+- conda: https://prefix.dev/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda
+ sha256: 25abdb37e186f0d6ac3b774a63c81c5bc4bf554b5096b51343fa5e7c381193b1
+ md5: bea46844deb274b2cc2a3a941745fa73
depends:
+ - python >=3.10
- backports
- - python >=3.9
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/backports-tarfile?source=hash-mapping
- size: 32786
- timestamp: 1733325872620
-- conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda
- sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5
- md5: 51a19bba1b8ebfb60df25cde030b7ebc
+ size: 35739
+ timestamp: 1767290467820
+- conda: https://prefix.dev/conda-forge/linux-64/backports.zstd-1.4.0-py311h6b1f9c4_0.conda
+ sha256: a23717f9dc06924b988b92ba8dca0f4cb9154cac954457a40adb2f19d57896de
+ md5: aa8c3009fd8903bebdcb22fbcb4c0dea
+ depends:
+ - python
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - python_abi 3.11.* *_cp311
+ - zstd >=1.5.7,<1.6.0a0
+ license: BSD-3-Clause AND MIT AND EPL-2.0
+ purls:
+ - pkg:pypi/backports-zstd?source=hash-mapping
+ size: 245341
+ timestamp: 1777848716685
+- conda: https://prefix.dev/conda-forge/osx-arm64/backports.zstd-1.4.0-py311h36d4fbb_0.conda
+ sha256: 2e6399a3663e7e20134a208cd09befc44ce1e9efe94d1d36d8911db04f186270
+ md5: 3ee7e8ae532366221658b80baee36dc0
+ depends:
+ - python
+ - __osx >=11.0
+ - python_abi 3.11.* *_cp311
+ - zstd >=1.5.7,<1.6.0a0
+ license: BSD-3-Clause AND MIT AND EPL-2.0
+ purls:
+ - pkg:pypi/backports-zstd?source=hash-mapping
+ size: 244743
+ timestamp: 1777848723707
+- conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda
+ sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6
+ md5: d2ffd7602c02f2b316fd921d39876885
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
license: bzip2-1.0.6
license_family: BSD
purls: []
- size: 260341
- timestamp: 1757437258798
-- conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda
- sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1
- md5: 58fd217444c2a5701a44244faf518206
+ size: 260182
+ timestamp: 1771350215188
+- conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda
+ sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df
+ md5: 620b85a3f45526a8bc4d23fd78fc22f0
depends:
- __osx >=11.0
license: bzip2-1.0.6
license_family: BSD
purls: []
- size: 125061
- timestamp: 1757437486465
-- conda: https://prefix.dev/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda
- sha256: f8003bef369f57396593ccd03d08a8e21966157269426f71e943f96e4b579aeb
- md5: f7f0d6cc2dc986d42ac2689ec88192be
+ size: 124834
+ timestamp: 1771350416561
+- conda: https://prefix.dev/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda
+ sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e
+ md5: 920bb03579f15389b9e512095ad995b7
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
license: MIT
license_family: MIT
purls: []
- size: 206884
- timestamp: 1744127994291
-- conda: https://prefix.dev/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda
- sha256: b4bb55d0806e41ffef94d0e3f3c97531f322b3cb0ca1f7cdf8e47f62538b7a2b
- md5: f8cd1beb98240c7edb1a95883360ccfa
+ size: 207882
+ timestamp: 1765214722852
+- conda: https://prefix.dev/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda
+ sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196
+ md5: bcb3cba70cf1eec964a03b4ba7775f01
depends:
- __osx >=11.0
license: MIT
license_family: MIT
purls: []
- size: 179696
- timestamp: 1744128058734
-- conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda
- sha256: b986ba796d42c9d3265602bc038f6f5264095702dd546c14bc684e60c385e773
- md5: f0991f0f84902f6b6009b4d2350a83aa
+ size: 180327
+ timestamp: 1765215064054
+- conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda
+ sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d
+ md5: e18ad67cf881dcadee8b8d9e2f8e5f73
depends:
- __unix
license: ISC
purls: []
- size: 152432
- timestamp: 1762967197890
-- conda: https://prefix.dev/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda
- sha256: 06525fa0c4e4f56e771a3b986d0fdf0f0fc5a3270830ee47e127a5105bde1b9a
- md5: bb6c4808bfa69d6f7f6b07e5846ced37
+ size: 131039
+ timestamp: 1776865545798
+- conda: https://prefix.dev/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda
+ sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7
+ md5: 09262e66b19567aff4f592fb53b28760
depends:
- __glibc >=2.17,<3.0.a0
- fontconfig >=2.15.0,<3.0a0
- fonts-conda-ecosystem
- - icu >=78.1,<79.0a0
- - libexpat >=2.7.3,<3.0a0
- - libfreetype >=2.14.1
- - libfreetype6 >=2.14.1
- - libgcc >=14
- - libglib >=2.86.3,<3.0a0
- - libpng >=1.6.53,<1.7.0a0
- - libstdcxx >=14
+ - freetype >=2.12.1,<3.0a0
+ - icu >=75.1,<76.0a0
+ - libexpat >=2.6.4,<3.0a0
+ - libgcc >=13
+ - libglib >=2.82.2,<3.0a0
+ - libpng >=1.6.47,<1.7.0a0
+ - libstdcxx >=13
- libxcb >=1.17.0,<2.0a0
- libzlib >=1.3.1,<2.0a0
- - pixman >=0.46.4,<1.0a0
+ - pixman >=0.44.2,<1.0a0
- xorg-libice >=1.1.2,<2.0a0
- - xorg-libsm >=1.2.6,<2.0a0
- - xorg-libx11 >=1.8.12,<2.0a0
+ - xorg-libsm >=1.2.5,<2.0a0
+ - xorg-libx11 >=1.8.11,<2.0a0
- xorg-libxext >=1.3.6,<2.0a0
- xorg-libxrender >=0.9.12,<0.10.0a0
license: LGPL-2.1-only or MPL-1.1
purls: []
- size: 989514
- timestamp: 1766415934926
-- conda: https://prefix.dev/conda-forge/osx-arm64/cairo-1.18.4-he0f2337_1.conda
- sha256: cde9b79ee206fe3ba6ca2dc5906593fb7a1350515f85b2a1135a4ce8ec1539e3
- md5: 36200ecfbbfbcb82063c87725434161f
+ size: 978114
+ timestamp: 1741554591855
+- conda: https://prefix.dev/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda
+ sha256: 00439d69bdd94eaf51656fdf479e0c853278439d22ae151cabf40eb17399d95f
+ md5: 38f6df8bc8c668417b904369a01ba2e2
depends:
- __osx >=11.0
- fontconfig >=2.15.0,<3.0a0
- fonts-conda-ecosystem
- - icu >=78.1,<79.0a0
- - libcxx >=19
- - libexpat >=2.7.3,<3.0a0
- - libfreetype >=2.14.1
- - libfreetype6 >=2.14.1
- - libglib >=2.86.3,<3.0a0
- - libpng >=1.6.53,<1.7.0a0
+ - freetype >=2.12.1,<3.0a0
+ - icu >=75.1,<76.0a0
+ - libcxx >=18
+ - libexpat >=2.6.4,<3.0a0
+ - libglib >=2.82.2,<3.0a0
+ - libpng >=1.6.47,<1.7.0a0
- libzlib >=1.3.1,<2.0a0
- - pixman >=0.46.4,<1.0a0
+ - pixman >=0.44.2,<1.0a0
license: LGPL-2.1-only or MPL-1.1
purls: []
- size: 900035
- timestamp: 1766416416791
-- conda: https://prefix.dev/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda
- sha256: 083a2bdad892ccf02b352ecab38ee86c3e610ba9a4b11b073ea769d55a115d32
- md5: 96a02a5c1a65470a7e4eedb644c872fd
+ size: 896173
+ timestamp: 1741554795915
+- conda: https://prefix.dev/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda
+ sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878
+ md5: 929471569c93acefb30282a22060dcd5
depends:
- python >=3.10
license: ISC
purls:
- - pkg:pypi/certifi?source=compressed-mapping
- size: 157131
- timestamp: 1762976260320
+ - pkg:pypi/certifi?source=hash-mapping
+ size: 135656
+ timestamp: 1776866680878
- conda: https://prefix.dev/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda
sha256: 3ad13377356c86d3a945ae30e9b8c8734300925ef81a3cb0a9db0d755afbe7bb
md5: 3912e4373de46adafd8f1e97e4bd166b
@@ -828,40 +835,42 @@ packages:
- pkg:pypi/cffi?source=hash-mapping
size: 294848
timestamp: 1761203196617
-- conda: https://prefix.dev/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_1.conda
- sha256: d5696636733b3c301054b948cdd793f118efacce361d9bd4afb57d5980a9064f
- md5: 57df494053e17dce2ac3a0b33e1b2a2e
+- conda: https://prefix.dev/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda
+ sha256: aa589352e61bb221351a79e5946d56916e3c595783994884accdb3b97fe9d449
+ md5: 381bd45fb7aa032691f3063aff47e3a1
depends:
- - python >=3.9
+ - python >=3.10
license: MIT
license_family: MIT
purls:
- pkg:pypi/cfgv?source=hash-mapping
- size: 12973
- timestamp: 1734267180483
-- conda: https://prefix.dev/conda-forge/noarch/click-8.3.1-pyh707e725_0.conda
- sha256: 970b12fb186c3451eee9dd0f10235aeb75fb570b0e9dc83250673c2f0b196265
- md5: 9ba00b39e03a0afb2b1cc0767d4c6175
+ size: 13589
+ timestamp: 1763607964133
+- conda: https://prefix.dev/conda-forge/noarch/click-8.3.3-pyhc90fa1f_0.conda
+ sha256: 37a5d8b10ea3516e2c42f870c9c351b9f7b31ff48c66d83490039f417e1e5228
+ md5: 2266262ce8a425ecb6523d765f79b303
depends:
- __unix
+ - python
- python >=3.10
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/click?source=compressed-mapping
- size: 92604
- timestamp: 1763248639281
-- conda: https://prefix.dev/conda-forge/noarch/cloudpickle-3.1.2-pyhd8ed1ab_0.conda
- sha256: 57050bd1bbac9e4be3728da4d33dee2168884d61d0ec51cd2ac72a1b34e11fc3
- md5: fcac5929097ba1f2a0e5b6ecaa13b253
+ - pkg:pypi/click?source=hash-mapping
+ size: 100048
+ timestamp: 1777219902525
+- conda: https://prefix.dev/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda
+ sha256: 4c287c2721d8a34c94928be8fe0e9a85754e90189dd4384a31b1806856b50a67
+ md5: 61b8078a0905b12529abc622406cb62c
depends:
- python >=3.10
+ - python
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/cloudpickle?source=compressed-mapping
- size: 26621
- timestamp: 1762167702602
+ - pkg:pypi/cloudpickle?source=hash-mapping
+ size: 27353
+ timestamp: 1765303462831
- conda: https://prefix.dev/conda-forge/linux-64/cmake-3.26.4-hcfe8598_0.conda
sha256: 37533b572a676017704c989c392998c344e889010786d6555dccdfc524a8e238
md5: 1714cf0f0facaeb609a0846e4270aff2
@@ -916,14 +925,14 @@ packages:
- pkg:pypi/colorama?source=hash-mapping
size: 27011
timestamp: 1733218222191
-- conda: https://prefix.dev/conda-forge/linux-64/cryptography-46.0.3-py311h8488d03_0.conda
- sha256: 893d87bee341ce987fc8d461ff96c880d16568508318bbe524069ea3aa98599d
- md5: 6bdc040b8f49f10d776ad2f9f5704e91
+- conda: https://prefix.dev/conda-forge/linux-64/cryptography-48.0.0-py311h2005dd1_0.conda
+ sha256: 838cde68e70f4e0dc6458613316fa0e50eee5cf86cafb1cee4b7ca1a701a8436
+ md5: 96f6e551c7ff30c95c1c8b4d2d1c5c9f
depends:
- __glibc >=2.17,<3.0.a0
- - cffi >=1.14
+ - cffi >=2.0
- libgcc >=14
- - openssl >=3.5.4,<4.0a0
+ - openssl >=3.5.6,<4.0a0
- python >=3.11,<3.12.0a0
- python_abi 3.11.* *_cp311
constrains:
@@ -932,8 +941,8 @@ packages:
license_family: BSD
purls:
- pkg:pypi/cryptography?source=hash-mapping
- size: 1718616
- timestamp: 1760605259113
+ size: 1913519
+ timestamp: 1777966158377
- conda: https://prefix.dev/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda
sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020
md5: 418c6ca5929a611cbd69204907a83995
@@ -952,36 +961,33 @@ packages:
purls: []
size: 316394
timestamp: 1685695959391
-- conda: https://prefix.dev/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda
- sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068
- md5: 679616eb5ad4e521c83da4650860aba7
+- conda: https://prefix.dev/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda
+ sha256: 8bb557af1b2b7983cf56292336a1a1853f26555d9c6cecf1e5b2b96838c9da87
+ md5: ce96f2f470d39bd96ce03945af92e280
depends:
- - libstdcxx >=13
- - libgcc >=13
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - libexpat >=2.7.0,<3.0a0
+ - libgcc >=14
+ - libstdcxx >=14
- libzlib >=1.3.1,<2.0a0
- - libglib >=2.84.2,<3.0a0
- license: GPL-2.0-or-later
- license_family: GPL
+ - libglib >=2.86.2,<3.0a0
+ - libexpat >=2.7.3,<3.0a0
+ license: AFL-2.1 OR GPL-2.0-or-later
purls: []
- size: 437860
- timestamp: 1747855126005
-- conda: https://prefix.dev/conda-forge/osx-arm64/dbus-1.16.2-hda038a8_0.conda
- sha256: 2ef01ab52dedb477cb7291994ad556279b37c8ad457521e75c47cad20248ea30
- md5: 80c663e4f6b0fd8d6723ff7d68f09429
+ size: 447649
+ timestamp: 1764536047944
+- conda: https://prefix.dev/conda-forge/osx-arm64/dbus-1.16.2-h3ff7a7c_1.conda
+ sha256: a8207751ed261764061866880da38e4d3063e167178bfe85b6db9501432462ba
+ md5: 5a3506971d2d53023c1c4450e908a8da
depends:
+ - libcxx >=19
- __osx >=11.0
- - libcxx >=18
+ - libglib >=2.86.2,<3.0a0
- libzlib >=1.3.1,<2.0a0
- - libglib >=2.84.2,<3.0a0
- - libexpat >=2.7.0,<3.0a0
- license: GPL-2.0-or-later
- license_family: GPL
+ - libexpat >=2.7.3,<3.0a0
+ license: AFL-2.1 OR GPL-2.0-or-later
purls: []
- size: 384376
- timestamp: 1747855177419
+ size: 393811
+ timestamp: 1764536084131
- conda: https://prefix.dev/conda-forge/noarch/distlib-0.4.0-pyhd8ed1ab_0.conda
sha256: 6d977f0b2fc24fee21a9554389ab83070db341af6d6f09285360b2e09ef8b26e
md5: 003b8ba0a94e2f1e117d0bd46aebc901
@@ -993,17 +999,18 @@ packages:
- pkg:pypi/distlib?source=hash-mapping
size: 275642
timestamp: 1752823081585
-- conda: https://prefix.dev/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda
- sha256: 8d4f908e670be360617d418c328213bc46e7100154c3742db085148141712f60
- md5: 2cf824fe702d88e641eec9f9f653e170
+- conda: https://prefix.dev/conda-forge/noarch/editables-0.6-pyhcf101f3_0.conda
+ sha256: bb826ff403b8195467e7cd5f504bc14767b424dac27bb225508ca2c1852554b2
+ md5: 86b177231eecb011fe00e2117dbc3348
depends:
- - python >=3.9
+ - python >=3.10
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/editables?source=hash-mapping
- size: 10828
- timestamp: 1733208220327
+ size: 13160
+ timestamp: 1776172429816
- conda: https://prefix.dev/conda-forge/linux-64/epoxy-1.5.10-hb03c661_2.conda
sha256: a5b51e491fec22bcc1765f5b2c8fff8a97428e9a5a7ee6730095fb9d091b0747
md5: 057083b06ccf1c2778344b6dabace38b
@@ -1037,40 +1044,40 @@ packages:
purls: []
size: 296347
timestamp: 1758743805063
-- conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda
- sha256: ce61f4f99401a4bd455b89909153b40b9c823276aefcbb06f2044618696009ca
- md5: 72e42d28960d875c7654614f8b50939a
+- conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda
+ sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144
+ md5: 8e662bd460bda79b1ea39194e3c4c9ab
depends:
- - python >=3.9
+ - python >=3.10
- typing_extensions >=4.6.0
license: MIT and PSF-2.0
purls:
- pkg:pypi/exceptiongroup?source=hash-mapping
- size: 21284
- timestamp: 1746947398083
-- conda: https://prefix.dev/conda-forge/linux-64/expat-2.7.3-hecca717_0.conda
- sha256: c5d573e6831fb41177fb5ae0f1ee09caed55a868ec9887bc80ccc22c3e57b9b4
- md5: c81f6fa1865526f5ab1e6b669b3ee877
+ size: 21333
+ timestamp: 1763918099466
+- conda: https://prefix.dev/conda-forge/linux-64/expat-2.8.0-hecca717_0.conda
+ sha256: ca4dc1da00a8aaa56c1088e7f45f1859ecea6f75874e67584f1af6e5cf8179f8
+ md5: 992e529e407c9d67d50be1d7543fde4c
depends:
- __glibc >=2.17,<3.0.a0
- - libexpat 2.7.3 hecca717_0
+ - libexpat 2.8.0 hecca717_0
- libgcc >=14
license: MIT
license_family: MIT
purls: []
- size: 143991
- timestamp: 1763549744569
-- conda: https://prefix.dev/conda-forge/osx-arm64/expat-2.7.3-haf25636_0.conda
- sha256: 43175c9c1f8a259776a215fe2a77c6a29cf79cc47ee1a98f5267ae47f904c9c8
- md5: 884f1698556805bc43c42e80c9c19f3d
+ size: 148114
+ timestamp: 1777846120303
+- conda: https://prefix.dev/conda-forge/osx-arm64/expat-2.8.0-hf6b4638_0.conda
+ sha256: 9f8657c32bcff353b218b1bd7369de34c0534dc5a461630b296265e65e94e2d8
+ md5: 90f3b6fa818b07e9402d5ee89f3892d5
depends:
- __osx >=11.0
- - libexpat 2.7.3 haf25636_0
+ - libexpat 2.8.0 hf6b4638_0
license: MIT
license_family: MIT
purls: []
- size: 131515
- timestamp: 1763550010595
+ size: 135334
+ timestamp: 1777846191235
- conda: https://prefix.dev/conda-forge/noarch/farama-notifications-0.0.4-pyhd8ed1ab_0.conda
sha256: 8df190d75bac104c6c2317b13397eed9adfa6889b12405b0288ca2ca86855c06
md5: 61f63fcf1c2deddeae7a85a516d46c94
@@ -1082,52 +1089,55 @@ packages:
- pkg:pypi/farama-notifications?source=hash-mapping
size: 8406
timestamp: 1684258265351
-- conda: https://prefix.dev/conda-forge/noarch/filelock-3.20.0-pyhd8ed1ab_0.conda
- sha256: 19025a4078ff3940d97eb0da29983d5e0deac9c3e09b0eabf897daeaf9d1114e
- md5: 66b8b26023b8efdf8fcb23bac4b6325d
+- conda: https://prefix.dev/conda-forge/noarch/filelock-3.29.0-pyhd8ed1ab_0.conda
+ sha256: 6b471a18372bbd52bdf32fc965f71de3bc1b5219418b8e6b3875a67a7b08c483
+ md5: 8fa8358d022a3a9bd101384a808044c6
depends:
- python >=3.10
license: Unlicense
purls:
- pkg:pypi/filelock?source=hash-mapping
- size: 17976
- timestamp: 1759948208140
-- conda: https://prefix.dev/conda-forge/linux-64/fluidsynth-2.3.7-hd992666_0.conda
- sha256: 0bf26d25ae79e6f5f01a49a00e9ba3b60b10dd4c12ec43bdba51055c26bc9dd6
- md5: dd6c7b8a1b217ef7522ca987c465651d
+ size: 34211
+ timestamp: 1776621506566
+- conda: https://prefix.dev/conda-forge/linux-64/fluidsynth-2.5.4-h07a5eef_0.conda
+ sha256: bb759e354a9f383635e1982d5a7ea547ac2a3a9d73ec62b07e489f26b8967ebc
+ md5: e48d6e1f837a307d824f2b3f9a7065d7
depends:
- __glibc >=2.17,<3.0.a0
- - alsa-lib >=1.2.12,<1.3.0a0
+ - _openmp_mutex >=4.5
+ - alsa-lib >=1.2.15.3,<1.3.0a0
+ - dbus >=1.16.2,<2.0a0
- jack >=1.9.22,<1.10.0a0
- - libgcc >=13
- - libglib >=2.82.2,<3.0a0
+ - libgcc >=14
+ - libglib >=2.86.4,<3.0a0
- libsndfile >=1.2.2,<1.3.0a0
- - libstdcxx >=13
- - portaudio >=19.6.0,<19.7.0a0
+ - libstdcxx >=14
+ - portaudio >=19.7.0,<19.8.0a0
- pulseaudio-client >=17.0,<17.1.0a0
- - readline >=8.2,<9.0a0
- - sdl2 >=2.30.7,<3.0a0
+ - readline >=8.3,<9.0a0
+ - sdl2 >=2.32.56,<3.0a0
license: GPL-2.0-or-later
license_family: LGPL
purls: []
- size: 279996
- timestamp: 1729590344462
-- conda: https://prefix.dev/conda-forge/osx-arm64/fluidsynth-2.3.7-h80fea77_0.conda
- sha256: e4c39119797493d4f085cd6274e123372bec77e05ef93203ccf5dee714bfd9c3
- md5: dc2fc082a05af6a790d3e8e3e6489e6c
+ size: 341443
+ timestamp: 1776634510786
+- conda: https://prefix.dev/conda-forge/osx-arm64/fluidsynth-2.5.4-h985f16e_0.conda
+ sha256: 6fab2acb6b1c7ba0fc06c3d4cefa552c3da4c3d6a79dcdd2158eea96931c3929
+ md5: 1f716f3ba6477330807ed061637048c7
depends:
- __osx >=11.0
- - libcxx >=17
- - libglib >=2.82.2,<3.0a0
+ - libcxx >=19
+ - libglib >=2.86.4,<3.0a0
- libsndfile >=1.2.2,<1.3.0a0
- - portaudio >=19.6.0,<19.7.0a0
- - readline >=8.2,<9.0a0
- - sdl2 >=2.30.7,<3.0a0
+ - llvm-openmp >=19.1.7
+ - portaudio >=19.7.0,<19.8.0a0
+ - readline >=8.3,<9.0a0
+ - sdl2 >=2.32.56,<3.0a0
license: GPL-2.0-or-later
license_family: LGPL
purls: []
- size: 234967
- timestamp: 1729590579216
+ size: 291966
+ timestamp: 1776634889426
- conda: https://prefix.dev/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b
md5: 0c96522c6bdaed4b1566d11387caaf45
@@ -1160,34 +1170,36 @@ packages:
purls: []
size: 1620504
timestamp: 1727511233259
-- conda: https://prefix.dev/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda
- sha256: 7093aa19d6df5ccb6ca50329ef8510c6acb6b0d8001191909397368b65b02113
- md5: 8f5b0b297b59e1ac160ad4beec99dbee
+- conda: https://prefix.dev/conda-forge/linux-64/fontconfig-2.17.1-h27c8c51_0.conda
+ sha256: aa4a44dba97151221100a637c7f4bde619567afade9c0265f8e1c8eed8d7bd8c
+ md5: 867127763fbe935bab59815b6e0b7b5c
depends:
- __glibc >=2.17,<3.0.a0
- - freetype >=2.12.1,<3.0a0
- - libexpat >=2.6.3,<3.0a0
- - libgcc >=13
- - libuuid >=2.38.1,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
+ - libfreetype >=2.14.1
+ - libfreetype6 >=2.14.1
+ - libgcc >=14
+ - libuuid >=2.41.3,<3.0a0
- libzlib >=1.3.1,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 265599
- timestamp: 1730283881107
-- conda: https://prefix.dev/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda
- sha256: f79d3d816fafbd6a2b0f75ebc3251a30d3294b08af9bb747194121f5efa364bc
- md5: 7b29f48742cea5d1ccb5edd839cb5621
+ size: 270705
+ timestamp: 1771382710863
+- conda: https://prefix.dev/conda-forge/osx-arm64/fontconfig-2.17.1-h2b252f5_0.conda
+ sha256: 851e9c778bfc54645dcab7038c0383445cbebf16f6bb2d3f62ce422b1605385a
+ md5: d06ae1a11b46cc4c74177ecd28de7c7a
depends:
- __osx >=11.0
- - freetype >=2.12.1,<3.0a0
- - libexpat >=2.6.3,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
+ - libfreetype >=2.14.1
+ - libfreetype6 >=2.14.1
- libzlib >=1.3.1,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 234227
- timestamp: 1730284037572
+ size: 237308
+ timestamp: 1771382999247
- conda: https://prefix.dev/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2
sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61
md5: fee5683a3f04bd15cbd8318b096a27ab
@@ -1211,26 +1223,26 @@ packages:
purls: []
size: 4059
timestamp: 1762351264405
-- conda: https://prefix.dev/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda
- sha256: bf8e4dffe46f7d25dc06f31038cacb01672c47b9f45201f065b0f4d00ab0a83e
- md5: 4afc585cd97ba8a23809406cd8a9eda8
+- conda: https://prefix.dev/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda
+ sha256: c934c385889c7836f034039b43b05ccfa98f53c900db03d8411189892ced090b
+ md5: 8462b5322567212beeb025f3519fb3e2
depends:
- - libfreetype 2.14.1 ha770c72_0
- - libfreetype6 2.14.1 h73754d4_0
+ - libfreetype 2.14.3 ha770c72_0
+ - libfreetype6 2.14.3 h73754d4_0
license: GPL-2.0-only OR FTL
purls: []
- size: 173114
- timestamp: 1757945422243
-- conda: https://prefix.dev/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda
- sha256: 14427aecd72e973a73d5f9dfd0e40b6bc3791d253de09b7bf233f6a9a190fd17
- md5: 1ec9a1ee7a2c9339774ad9bb6fe6caec
+ size: 173839
+ timestamp: 1774298173462
+- conda: https://prefix.dev/conda-forge/osx-arm64/freetype-2.14.3-hce30654_0.conda
+ sha256: 5952bd9db12207a18a112e8924aa2ce8c2f9d57b62584d58a97d2f6afe1ea324
+ md5: 6dcc75ba2e04c555e881b72793d3282f
depends:
- - libfreetype 2.14.1 hce30654_0
- - libfreetype6 2.14.1 h6da58f4_0
+ - libfreetype 2.14.3 hce30654_0
+ - libfreetype6 2.14.3 hdfa99f5_0
license: GPL-2.0-only OR FTL
purls: []
- size: 173399
- timestamp: 1757947175403
+ size: 173313
+ timestamp: 1774298702053
- conda: https://prefix.dev/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
sha256: 858283ff33d4c033f4971bf440cebff217d5552a5222ba994c49be990dacd40d
md5: f9f81ea472684d75b9dd8d0b328cf655
@@ -1250,121 +1262,62 @@ packages:
purls: []
size: 59391
timestamp: 1757438897523
-- conda: https://prefix.dev/conda-forge/linux-64/gdk-pixbuf-2.44.4-h2b0a6b4_0.conda
- sha256: f47222f58839bcc77c15f11a8814c1d8cb8080c5ca6ba83398a12b640fd3c85c
- md5: c379d67c686fb83475c1a6ed41cc41ff
+- conda: https://prefix.dev/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ sha256: c5594497f0646e9079705b3199dbb2d5b13c48173cf110000fa1c8818e2b3e0c
+ md5: 7892f39a39ed39591a89a28eba03e987
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
- - libglib >=2.86.0,<3.0a0
- - libjpeg-turbo >=3.1.0,<4.0a0
- - liblzma >=5.8.1,<6.0a0
- - libpng >=1.6.50,<1.7.0a0
+ - libglib >=2.86.4,<3.0a0
+ - libjpeg-turbo >=3.1.2,<4.0a0
+ - liblzma >=5.8.2,<6.0a0
+ - libpng >=1.6.56,<1.7.0a0
- libtiff >=4.7.1,<4.8.0a0
license: LGPL-2.1-or-later
license_family: LGPL
purls: []
- size: 572093
- timestamp: 1761082340749
-- conda: https://prefix.dev/conda-forge/osx-arm64/gdk-pixbuf-2.44.4-h7542897_0.conda
- sha256: 1164ba63360736439c6e50f2d390e93f04df86901e7711de41072a32d9b8bfc9
- md5: 0b349c0400357e701cf2fa69371e5d39
+ size: 577414
+ timestamp: 1774985848058
+- conda: https://prefix.dev/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ sha256: 07cbba4e12430de35ea608eb3006cf1f7f63832c4f89a081cd6f3872944c1aa6
+ md5: e67ebd2f639f46e52af8531622fa6051
depends:
- __osx >=11.0
- - libglib >=2.86.0,<3.0a0
+ - libglib >=2.86.4,<3.0a0
- libintl >=0.25.1,<1.0a0
- - libjpeg-turbo >=3.1.0,<4.0a0
- - liblzma >=5.8.1,<6.0a0
- - libpng >=1.6.50,<1.7.0a0
+ - libjpeg-turbo >=3.1.2,<4.0a0
+ - liblzma >=5.8.2,<6.0a0
+ - libpng >=1.6.56,<1.7.0a0
- libtiff >=4.7.1,<4.8.0a0
license: LGPL-2.1-or-later
license_family: LGPL
purls: []
- size: 544149
- timestamp: 1761082904334
-- conda: https://prefix.dev/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda
- sha256: cbfa8c80771d1842c2687f6016c5e200b52d4ca8f2cc119f6377f64f899ba4ff
- md5: c42356557d7f2e37676e121515417e3b
+ size: 548309
+ timestamp: 1774986047281
+- conda: https://prefix.dev/conda-forge/linux-64/glib-tools-2.88.1-hcfc306f_1.conda
+ sha256: 628015696c106665ae0043f7e9f51298ec9e8f11573734ad67a849c8279cbe33
+ md5: ff216b19c24f3a46e9d17ebcf2f96390
depends:
- - __glibc >=2.17,<3.0.a0
- - gettext-tools 0.25.1 h3f43e3d_1
- - libasprintf 0.25.1 h3f43e3d_1
- - libasprintf-devel 0.25.1 h3f43e3d_1
- - libgcc >=14
- - libgettextpo 0.25.1 h3f43e3d_1
- - libgettextpo-devel 0.25.1 h3f43e3d_1
- - libiconv >=1.18,<2.0a0
- - libstdcxx >=14
- license: LGPL-2.1-or-later AND GPL-3.0-or-later
- purls: []
- size: 541357
- timestamp: 1753343006214
-- conda: https://prefix.dev/conda-forge/osx-arm64/gettext-0.25.1-h3dcc1bd_0.conda
- sha256: 129a81e9da9f60ae6955b49938447e7faeb7e1be815b2db99e76956dddf8c392
- md5: 7059ba83fd98707b2cd9a5f06f589dd4
- depends:
- - __osx >=11.0
- - gettext-tools 0.25.1 h493aca8_0
- - libasprintf 0.25.1 h493aca8_0
- - libasprintf-devel 0.25.1 h493aca8_0
- - libcxx >=18
- - libgettextpo 0.25.1 h493aca8_0
- - libgettextpo-devel 0.25.1 h493aca8_0
- - libiconv >=1.18,<2.0a0
- - libintl 0.25.1 h493aca8_0
- - libintl-devel 0.25.1 h493aca8_0
- license: LGPL-2.1-or-later AND GPL-3.0-or-later
- purls: []
- size: 543276
- timestamp: 1751558682952
-- conda: https://prefix.dev/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda
- sha256: c792729288bdd94f21f25f80802d4c66957b4e00a57f7cb20513f07aadfaff06
- md5: a59c05d22bdcbb4e984bf0c021a2a02f
- depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=14
- - libiconv >=1.18,<2.0a0
- license: GPL-3.0-or-later
- license_family: GPL
- purls: []
- size: 3644103
- timestamp: 1753342966311
-- conda: https://prefix.dev/conda-forge/osx-arm64/gettext-tools-0.25.1-h493aca8_0.conda
- sha256: e8dd68706676d5b6f6ee09240936a0ecd1ae12b87dbb37e4c4be263e332ab125
- md5: 817042c017930497931da6aa04a47f09
- depends:
- - __osx >=11.0
- - libiconv >=1.18,<2.0a0
- - libintl 0.25.1 h493aca8_0
- license: GPL-3.0-or-later
- license_family: GPL
- purls: []
- size: 3748044
- timestamp: 1751558602508
-- conda: https://prefix.dev/conda-forge/linux-64/glib-tools-2.86.4-hf516916_0.conda
- sha256: 20972d7770ccfb4cf9e77f5262195228e6f357626d172c195a9fa2a64f4818e8
- md5: 70a09b6817c7ad694ef4543204c59c25
- depends:
- - __glibc >=2.17,<3.0.a0
+ - libglib ==2.88.1 h0d30a3d_1
- libffi
- libgcc >=14
- - libglib 2.86.4 h6548e54_0
+ - __glibc >=2.17,<3.0.a0
license: LGPL-2.1-or-later
purls: []
- size: 214256
- timestamp: 1771291791256
-- conda: https://prefix.dev/conda-forge/osx-arm64/glib-tools-2.86.4-h60c1bae_0.conda
- sha256: c2eb2e02858127fb1673e63d9feda1512bf1626c208653e75c8e8613c8f677fa
- md5: fbffb85901084131bcc8b0592bbe39e5
+ size: 237141
+ timestamp: 1777904907738
+- conda: https://prefix.dev/conda-forge/osx-arm64/glib-tools-2.88.1-h357b478_1.conda
+ sha256: f00315c79a956ed4a8bdf2681f19f116061e33bed918b71ad41fb798facd35d8
+ md5: 91ea0d13e8ba62a5aa46443081abf635
depends:
- - __osx >=11.0
+ - libglib ==2.88.1 ha08bb59_1
- libffi
- - libglib 2.86.4 he378b5c_0
+ - __osx >=11.0
- libintl >=0.25.1,<1.0a0
license: LGPL-2.1-or-later
purls: []
- size: 182997
- timestamp: 1771293723140
+ size: 204878
+ timestamp: 1777905039771
- conda: https://prefix.dev/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c
md5: 2cd94587f3a401ae05e03a6caf09539d
@@ -1525,53 +1478,53 @@ packages:
purls: []
size: 304331
timestamp: 1686545503242
-- conda: https://prefix.dev/conda-forge/linux-64/gymnasium-1.0.0-np2py311hec5889a_2.conda
- sha256: 7d57ef4a135ba143946c905be9b0f24951fb1e10dd21c6048ff5ac7f73b5af03
- md5: 72e6c1871badba8b2534ba6d6f080530
+- conda: https://prefix.dev/conda-forge/linux-64/gymnasium-1.3.0-np2py311hec5889a_0.conda
+ sha256: d1e8bc964d6a2a70b52ebd36f0815b9867a3db8a973e3f6da2e0c1a57b0144a2
+ md5: f7e72a66dea8ee8eeab367bd824a7d95
depends:
- python
- cloudpickle >=1.2.0
- farama-notifications
- jax-jumpy >=1.0.0
- typing_extensions >=4.3.0
- - numpy >=1.23,<3
- python_abi 3.11.* *_cp311
+ - numpy >=1.23,<3
license: MIT
license_family: MIT
purls:
- pkg:pypi/gymnasium?source=hash-mapping
- size: 1123892
- timestamp: 1756082125909
-- conda: https://prefix.dev/conda-forge/osx-arm64/gymnasium-1.0.0-np2py311he8d3834_2.conda
- sha256: b9bd90e8537778a64d43bcdae8e8e0af758aa53fc7ad3eac626d329a141624bc
- md5: 3e5c87a9115f257d72271aec5344efea
+ size: 1139365
+ timestamp: 1777204693923
+- conda: https://prefix.dev/conda-forge/osx-arm64/gymnasium-1.3.0-np2py311h56d0776_0.conda
+ sha256: ab03d30592506e1dfe9e6fc30cddea577dd42a7eb4b6da8a80de543928e2e6fd
+ md5: d64237fee1a064db6f7c53090a6844da
depends:
- python
- cloudpickle >=1.2.0
- farama-notifications
- jax-jumpy >=1.0.0
- typing_extensions >=4.3.0
- - python 3.11.* *_cpython
- - numpy >=1.23,<3
- python_abi 3.11.* *_cp311
+ - numpy >=1.23,<3
license: MIT
license_family: MIT
purls:
- pkg:pypi/gymnasium?source=hash-mapping
- size: 1128628
- timestamp: 1756082198279
-- conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda
- sha256: f64b68148c478c3bfc8f8d519541de7d2616bf59d44485a5271041d40c061887
- md5: 4b69232755285701bc86a5afe4d9933a
+ size: 1138451
+ timestamp: 1777204698446
+- conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda
+ sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb
+ md5: b8993c19b0c32a2f7b66cbb58ca27069
depends:
- - python >=3.9
+ - python >=3.10
- typing_extensions
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/h11?source=hash-mapping
- size: 37697
- timestamp: 1745526482242
+ size: 39069
+ timestamp: 1767729720872
- conda: https://prefix.dev/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3
md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9
@@ -1583,110 +1536,112 @@ packages:
license: MIT
license_family: MIT
purls:
- - pkg:pypi/h2?source=compressed-mapping
+ - pkg:pypi/h2?source=hash-mapping
size: 95967
timestamp: 1756364871835
-- conda: https://prefix.dev/conda-forge/linux-64/harfbuzz-12.3.2-h6083320_0.conda
- sha256: 92015faf283f9c0a8109e2761042cd47ae7a4505e24af42a53bc3767cb249912
- md5: d170a70fc1d5c605fcebdf16851bd54a
+- conda: https://prefix.dev/conda-forge/linux-64/harfbuzz-11.5.1-h15599e2_0.conda
+ sha256: 3bf149eab76768ed10f95eba015ca996cd6be7dc666996a004c4a8340a57cd60
+ md5: b90a6ec73cc7d630981f78d4c7ca8fed
depends:
- __glibc >=2.17,<3.0.a0
- cairo >=1.18.4,<2.0a0
- graphite2 >=1.3.14,<2.0a0
- - icu >=78.2,<79.0a0
- - libexpat >=2.7.3,<3.0a0
+ - icu >=75.1,<76.0a0
+ - libexpat >=2.7.1,<3.0a0
- libfreetype >=2.14.1
- libfreetype6 >=2.14.1
- libgcc >=14
- - libglib >=2.86.3,<3.0a0
+ - libglib >=2.86.0,<3.0a0
- libstdcxx >=14
- libzlib >=1.3.1,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 2035859
- timestamp: 1769445400168
-- conda: https://prefix.dev/conda-forge/osx-arm64/harfbuzz-12.3.2-h3103d1b_0.conda
- sha256: f68c6704610396012124a3d86b087581174c2cf7968a46b6d95ba84cd87063c7
- md5: d0af4858d81c0c7abddc6b71fd8c0340
+ size: 2427482
+ timestamp: 1758640288422
+- conda: https://prefix.dev/conda-forge/osx-arm64/harfbuzz-11.5.1-haf38c7b_0.conda
+ sha256: ceea9b35e8e81a0e345035f2e46574a213b7d3d7b2f8fdd0cb049490bddde6c2
+ md5: effd4cec7d221d77cd937a83132f5b9e
depends:
- __osx >=11.0
- cairo >=1.18.4,<2.0a0
- graphite2 >=1.3.14,<2.0a0
- - icu >=78.2,<79.0a0
+ - icu >=75.1,<76.0a0
- libcxx >=19
- - libexpat >=2.7.3,<3.0a0
+ - libexpat >=2.7.1,<3.0a0
- libfreetype >=2.14.1
- libfreetype6 >=2.14.1
- - libglib >=2.86.3,<3.0a0
+ - libglib >=2.86.0,<3.0a0
- libzlib >=1.3.1,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 1441619
- timestamp: 1769446246348
-- conda: https://prefix.dev/conda-forge/noarch/hatch-1.14.1-pyhd8ed1ab_0.conda
- sha256: d245185287bdf5d3c808267aebda2cb7fbce779099493f92e368f813e6a157d4
- md5: 82f74ce5f4548c3627ed52dfac9da8ca
+ size: 1514711
+ timestamp: 1758640885114
+- conda: https://prefix.dev/conda-forge/noarch/hatch-1.16.5-pyhcf101f3_0.conda
+ sha256: e298f93e25d98f5b90f231cf9cd6467773fc3fd242a9a0dce2b82fcfa1b38971
+ md5: 3191c464a2222e8af63e57926da9315c
depends:
- click >=8.0.6
- - hatchling >=1.26.3
+ - hatchling >=1.27.0
- httpx >=0.22.0
- hyperlink >=21.0.0
- keyring >=23.5.0
- - packaging >=23.2
+ - packaging >=24.2
- pexpect >=4.8,<5.dev0
- platformdirs >=2.5.0
- - python >=3.9
+ - pyproject_hooks
+ - python >=3.10
- rich >=11.2.0
- shellingham >=1.4.0
- tomli-w >=1.0
- tomlkit >=0.11.1
- userpath >=1.7,<2.dev0
- uv >=0.5.23
- - virtualenv >=20.26.6
- - zstandard <1
+ - virtualenv >=21
+ - backports.zstd >=1.0.0
+ - python-discovery >=1.1
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/hatch?source=hash-mapping
- size: 178008
- timestamp: 1744039278512
-- conda: https://prefix.dev/conda-forge/noarch/hatchling-1.27.0-pypyhd8ed1ab_0.conda
- sha256: e83420f81390535774ac33b83d05249b8993e5376b76b4d461f83a77549e493d
- md5: b85c18ba6e927ae0da3fde426c893cc8
+ size: 208420
+ timestamp: 1772806610433
+- conda: https://prefix.dev/conda-forge/noarch/hatchling-1.29.0-pyhcf101f3_0.conda
+ sha256: bb86ff4ca54a2a0f63714766c7653a772c13d34c9e7cfb7be653db2fb806f961
+ md5: 9d67ecd4cd5e6a9be36522be95951785
depends:
- - editables >=0.3
- - importlib-metadata
- - packaging >=21.3
+ - packaging >=24.2
- pathspec >=0.10.1
- pluggy >=1.0.0
- - python >=3.7
- - python >=3.8
+ - python >=3.10
- tomli >=1.2.2
- trove-classifiers
+ - editables >=0.3
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/hatchling?source=hash-mapping
- size: 56598
- timestamp: 1734311718682
-- conda: https://prefix.dev/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2
- sha256: 336f29ceea9594f15cc8ec4c45fdc29e10796573c697ee0d57ebb7edd7e92043
- md5: bbf6f174dcd3254e19a2f5d2295ce808
+ size: 61052
+ timestamp: 1773194193187
+- conda: https://prefix.dev/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
+ sha256: 6d7e6e1286cb521059fe69696705100a03b006efb914ffe82a2ae97ecbae66b7
+ md5: 129e404c5b001f3ef5581316971e3ea0
license: GPL-2.0-or-later
license_family: GPL
purls: []
- size: 13841
- timestamp: 1605162808667
-- conda: https://prefix.dev/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2
- sha256: 286e33fb452f61133a3a61d002890235d1d1378554218ab063d6870416440281
- md5: 237b05b7eb284d7eebc3c5d93f5e4bca
+ size: 17625
+ timestamp: 1771539597968
+- conda: https://prefix.dev/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
+ sha256: 46a4958f2f916c5938f2a6dc0709f78b175ece42f601d79a04e0276d55d25d07
+ md5: cfb39109ac5fa8601eb595d66d5bf156
license: GPL-2.0-or-later
license_family: GPL
purls: []
- size: 13800
- timestamp: 1611053664863
+ size: 17616
+ timestamp: 1771539622983
- conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba
md5: 0a802cb9888dd14eeefc611f05c40b6e
@@ -1766,18 +1721,6 @@ packages:
purls: []
size: 12129203
timestamp: 1720853576813
-- conda: https://prefix.dev/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda
- sha256: 142a722072fa96cf16ff98eaaf641f54ab84744af81754c292cb81e0881c0329
- md5: 186a18e3ba246eccfc7cff00cd19a870
- depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=14
- - libstdcxx >=14
- license: MIT
- license_family: MIT
- purls: []
- size: 12728445
- timestamp: 1767969922681
- conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda
sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620
md5: 5eb22c1d7b3fc4abb50d92d621583137
@@ -1788,19 +1731,9 @@ packages:
purls: []
size: 11857802
timestamp: 1720853997952
-- conda: https://prefix.dev/conda-forge/osx-arm64/icu-78.2-h38cb7af_0.conda
- sha256: d4cefbca587429d1192509edc52c88de52bc96c2447771ddc1f8bee928aed5ef
- md5: 1e93aca311da0210e660d2247812fa02
- depends:
- - __osx >=11.0
- license: MIT
- license_family: MIT
- purls: []
- size: 12358010
- timestamp: 1767970350308
-- conda: https://prefix.dev/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda
- sha256: 32d5007d12e5731867908cbf5345f5cd44a6c8755a2e8e63e15a184826a51f82
- md5: 25f954b7dae6dd7b0dc004dab74f1ce9
+- conda: https://prefix.dev/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda
+ sha256: 381cedccf0866babfc135d65ee40b778bd20e927d2a5ec810f750c5860a7c5b8
+ md5: 84a3233b709a289a4ddd7a2fd27dd988
depends:
- python >=3.10
- ukkonen
@@ -1808,46 +1741,47 @@ packages:
license_family: MIT
purls:
- pkg:pypi/identify?source=hash-mapping
- size: 79151
- timestamp: 1759437561529
-- conda: https://prefix.dev/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda
- sha256: ae89d0299ada2a3162c2614a9d26557a92aa6a77120ce142f8e0109bbf0342b0
- md5: 53abe63df7e10a6ba605dc5f9f961d36
+ size: 79757
+ timestamp: 1776455344188
+- conda: https://prefix.dev/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48
+ md5: fb7130c190f9b4ec91219840a05ba3ac
depends:
- python >=3.10
+ - python
license: BSD-3-Clause
license_family: BSD
purls:
- pkg:pypi/idna?source=hash-mapping
- size: 50721
- timestamp: 1760286526795
-- conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda
- sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745
- md5: 63ccfdc3a3ce25b027b8767eb722fca8
+ size: 59038
+ timestamp: 1776947141407
+- conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
+ sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16
+ md5: 080594bf4493e6bae2607e65390c520a
depends:
- - python >=3.9
+ - python >=3.10
- zipp >=3.20
- python
license: Apache-2.0
license_family: APACHE
purls:
- pkg:pypi/importlib-metadata?source=hash-mapping
- size: 34641
- timestamp: 1747934053147
-- conda: https://prefix.dev/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda
- sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80
- md5: c85c76dc67d75619a92f51dfbce06992
+ size: 34387
+ timestamp: 1773931568510
+- conda: https://prefix.dev/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
+ sha256: a563a51aa522998172838e867e6dedcf630bc45796e8612f5a1f6d73e9c8125a
+ md5: 0ba6225c279baf7ea9473a62ea0ec9ae
depends:
- - python >=3.9
+ - python >=3.10
- zipp >=3.1.0
constrains:
- - importlib-resources >=6.5.2,<6.5.3.0a0
+ - importlib-resources >=7.1.0,<7.1.1.0a0
license: Apache-2.0
license_family: APACHE
purls:
- pkg:pypi/importlib-resources?source=hash-mapping
- size: 33781
- timestamp: 1736252433366
+ size: 34809
+ timestamp: 1776068839274
- conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19
md5: 9614359868482abba1bd15ce465e3c42
@@ -1856,7 +1790,7 @@ packages:
license: MIT
license_family: MIT
purls:
- - pkg:pypi/iniconfig?source=compressed-mapping
+ - pkg:pypi/iniconfig?source=hash-mapping
size: 13387
timestamp: 1760831448842
- conda: https://prefix.dev/conda-forge/linux-64/jack-1.9.22-hf4617a5_3.conda
@@ -1873,42 +1807,45 @@ packages:
purls: []
size: 461260
timestamp: 1747574434594
-- conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhd8ed1ab_2.conda
- sha256: 3d16a0fa55a29fe723c918a979b2ee927eb0bf9616381cdfd26fa9ea2b649546
- md5: ade6b25a6136661dadd1a43e4350b10b
+- conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda
+ sha256: 3cc991f0f09dfd00d2626e745ba68da03e4f1dcbb7b36dd20f7a7373643cd5d5
+ md5: d59568bad316413c89831456e691de29
depends:
+ - python >=3.10
- more-itertools
- - python >=3.9
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/jaraco-classes?source=hash-mapping
- size: 12109
- timestamp: 1733326001034
-- conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.0.1-pyhd8ed1ab_0.conda
- sha256: bfaba92cd33a0ae2488ab64a1d4e062bcf52b26a71f88292c62386ccac4789d7
- md5: bcc023a32ea1c44a790bbf1eae473486
+ size: 14831
+ timestamp: 1767294269456
+- conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.1.2-pyhcf101f3_0.conda
+ sha256: 108b3919db8ef81b5585b38a3fedbe9eeccdf8fa576c250a13559a1188f597cc
+ md5: b9d2b1ffa307961f6bb7d83c8ba8a8be
depends:
+ - python >=3.10
- backports.tarfile
- - python >=3.9
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/jaraco-context?source=hash-mapping
- size: 12483
- timestamp: 1733382698758
-- conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.3.0-pyhd8ed1ab_0.conda
- sha256: 89320bb2c6bef18f5109bee6cb07a193701cf00552a4cfc6f75073cf0d3e44f6
- md5: b86839fa387a5b904846e77c84167e57
+ size: 16222
+ timestamp: 1776862415793
+- conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.4.0-pyhcf101f3_1.conda
+ sha256: 6a91447b3bb4d7ae94cc0d77ed12617796629aee11111efe7ea43cbd0e113bda
+ md5: aa83cc08626bf6b613a3103942be8951
depends:
+ - python >=3.10
- more-itertools
- - python >=3.9
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/jaraco-functools?source=hash-mapping
- size: 16238
- timestamp: 1755584796828
+ size: 18744
+ timestamp: 1767294193246
- conda: https://prefix.dev/conda-forge/noarch/jax-jumpy-1.0.0-pyhd8ed1ab_1.conda
sha256: c414b2be28e7d963e55c962caad1a25833d2e415a3d7a16e4f9da9e187dd37b3
md5: d15bad1eca61880b5e465c4038d7c3f4
@@ -1965,7 +1902,7 @@ packages:
license: MIT
license_family: MIT
purls:
- - pkg:pypi/keyring?source=compressed-mapping
+ - pkg:pypi/keyring?source=hash-mapping
size: 37717
timestamp: 1763320674488
- conda: https://prefix.dev/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
@@ -1978,35 +1915,36 @@ packages:
purls: []
size: 134088
timestamp: 1754905959823
-- conda: https://prefix.dev/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda
- sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238
- md5: 3f43953b7d3fb3aaa1d0d0723d91e368
+- conda: https://prefix.dev/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda
+ sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25
+ md5: fb53fb07ce46a575c5d004bbc96032c2
depends:
- - keyutils >=1.6.1,<2.0a0
- - libedit >=3.1.20191231,<3.2.0a0
- - libedit >=3.1.20191231,<4.0a0
- - libgcc-ng >=12
- - libstdcxx-ng >=12
- - openssl >=3.3.1,<4.0a0
+ - __glibc >=2.17,<3.0.a0
+ - keyutils >=1.6.3,<2.0a0
+ - libedit >=3.1.20250104,<3.2.0a0
+ - libedit >=3.1.20250104,<4.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - openssl >=3.5.5,<4.0a0
license: MIT
license_family: MIT
purls: []
- size: 1370023
- timestamp: 1719463201255
-- conda: https://prefix.dev/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda
- sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b
- md5: c6dc8a0fdec13a0565936655c33069a1
+ size: 1386730
+ timestamp: 1769769569681
+- conda: https://prefix.dev/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda
+ sha256: c0a0bf028fe7f3defcdcaa464e536cf1b202d07451e18ad83fdd169d15bef6ed
+ md5: e446e1822f4da8e5080a9de93474184d
depends:
- __osx >=11.0
- - libcxx >=16
- - libedit >=3.1.20191231,<3.2.0a0
- - libedit >=3.1.20191231,<4.0a0
- - openssl >=3.3.1,<4.0a0
+ - libcxx >=19
+ - libedit >=3.1.20250104,<3.2.0a0
+ - libedit >=3.1.20250104,<4.0a0
+ - openssl >=3.5.5,<4.0a0
license: MIT
license_family: MIT
purls: []
- size: 1155530
- timestamp: 1719463474401
+ size: 1160828
+ timestamp: 1769770119811
- conda: https://prefix.dev/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2
sha256: aad2a703b9d7b038c0f745b853c6bb5f122988fe1a7a096e0e606d9cbec4eaab
md5: a8832b479f93521a9e7b5b743803be51
@@ -2025,19 +1963,19 @@ packages:
purls: []
size: 528805
timestamp: 1664996399305
-- conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda
- sha256: 32321d38b8785ef8ddcfef652ee370acee8d944681014d47797a18637ff16854
- md5: 1450224b3e7d17dfeb985364b77a4d47
+- conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
+ sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c
+ md5: 18335a698559cdbcd86150a48bf54ba6
depends:
- __glibc >=2.17,<3.0.a0
- zstd >=1.5.7,<1.6.0a0
constrains:
- - binutils_impl_linux-64 2.45
+ - binutils_impl_linux-64 2.45.1
license: GPL-3.0-only
license_family: GPL
purls: []
- size: 753744
- timestamp: 1763060439129
+ size: 728002
+ timestamp: 1774197446916
- pypi: https://files.pythonhosted.org/packages/fb/06/6a7c30c3519181263e1bbf0cbea67ef4a43a64443ee16717c7d760214943/lenses-1.2.0-py3-none-any.whl
name: lenses
version: 1.2.0
@@ -2056,178 +1994,135 @@ packages:
- pytest-coverage ; extra == 'tests'
- hypothesis ; extra == 'tests'
requires_python: '>=3.7,<4'
-- conda: https://prefix.dev/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda
- sha256: 412381a43d5ff9bbed82cd52a0bbca5b90623f62e41007c9c42d3870c60945ff
- md5: 9344155d33912347b37f0ae6c410a835
+- conda: https://prefix.dev/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
+ sha256: f84cb54782f7e9cea95e810ea8fef186e0652d0fa73d3009914fa2c1262594e1
+ md5: a752488c68f2e7c456bcbd8f16eec275
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - libstdcxx >=13
+ - libgcc >=14
+ - libstdcxx >=14
license: Apache-2.0
license_family: Apache
purls: []
- size: 264243
- timestamp: 1745264221534
-- conda: https://prefix.dev/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda
- sha256: 12361697f8ffc9968907d1a7b5830e34c670e4a59b638117a2cdfed8f63a38f8
- md5: a74332d9b60b62905e3d30709df08bf1
+ size: 261513
+ timestamp: 1773113328888
+- conda: https://prefix.dev/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
+ sha256: 66e5ffd301a44da696f3efc2f25d6d94f42a9adc0db06c44ad753ab844148c51
+ md5: 095e5749868adab9cae42d4b460e5443
depends:
- __osx >=11.0
- - libcxx >=18
+ - libcxx >=19
license: Apache-2.0
license_family: Apache
purls: []
- size: 188306
- timestamp: 1745264362794
-- conda: https://prefix.dev/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda
- sha256: cb728a2a95557bb6a5184be2b8be83a6f2083000d0c7eff4ad5bbe5792133541
- md5: 3b0d184bc9404516d418d4509e418bdc
- depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=14
- - libstdcxx >=14
- license: LGPL-2.1-or-later
- purls: []
- size: 53582
- timestamp: 1753342901341
-- conda: https://prefix.dev/conda-forge/osx-arm64/libasprintf-0.25.1-h493aca8_0.conda
- sha256: 7265547424e978ea596f51cc8e7b81638fb1c660b743e98cc4deb690d9d524ab
- md5: 0deb80a2d6097c5fb98b495370b2435b
- depends:
- - __osx >=11.0
- - libcxx >=18
- license: LGPL-2.1-or-later
- purls: []
- size: 52316
- timestamp: 1751558366611
-- conda: https://prefix.dev/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda
- sha256: 2fc95060efc3d76547b7872875af0b7212d4b1407165be11c5f830aeeb57fc3a
- md5: fd9cf4a11d07f0ef3e44fc061611b1ed
- depends:
- - __glibc >=2.17,<3.0.a0
- - libasprintf 0.25.1 h3f43e3d_1
- - libgcc >=14
- license: LGPL-2.1-or-later
- purls: []
- size: 34734
- timestamp: 1753342921605
-- conda: https://prefix.dev/conda-forge/osx-arm64/libasprintf-devel-0.25.1-h493aca8_0.conda
- sha256: fc76b07620eabde52928c69bcdcb5497da3fdad3331a76f9d4bffeb27e0bdd8f
- md5: c18067d2d5864e77f84456d97c1c17cc
- depends:
- - __osx >=11.0
- - libasprintf 0.25.1 h493aca8_0
- license: LGPL-2.1-or-later
- purls: []
- size: 35256
- timestamp: 1751558418167
-- conda: https://prefix.dev/conda-forge/linux-64/libavif16-1.3.0-h6395336_2.conda
- sha256: e3a44c0eda23aa15c9a8dfa8c82ecf5c8b073e68a16c29edd0e409e687056d30
- md5: c09c4ac973f7992ba0c6bb1aafd77bd4
+ size: 164222
+ timestamp: 1773114244984
+- conda: https://prefix.dev/conda-forge/linux-64/libavif16-1.4.1-hcfa2d63_0.conda
+ sha256: e29d8ed0334305c6bafecb32f9a1967cfc0a081eac916e947a1f2f7c4bb41947
+ md5: f79415aee8862b3af85ea55dea37e46b
depends:
- __glibc >=2.17,<3.0.a0
- aom >=3.9.1,<3.10.0a0
- dav1d >=1.2.1,<1.2.2.0a0
- libgcc >=14
- - rav1e >=0.7.1,<0.8.0a0
- - svt-av1 >=3.1.2,<3.1.3.0a0
+ - rav1e >=0.8.1,<0.9.0a0
+ - svt-av1 >=4.0.1,<4.0.2.0a0
license: BSD-2-Clause
license_family: BSD
purls: []
- size: 139399
- timestamp: 1756124751131
-- conda: https://prefix.dev/conda-forge/osx-arm64/libavif16-1.3.0-hb06b76e_2.conda
- sha256: 8bd31f1fc65a177815d9abebf42768a1d8b5b07b055d54485bcb4b1beb93993a
- md5: ab7aaf5c139849228894d3ac72ec8f77
+ size: 148710
+ timestamp: 1774042709303
+- conda: https://prefix.dev/conda-forge/osx-arm64/libavif16-1.4.1-hfce71f6_0.conda
+ sha256: 09e31e51026a3b74d947ba4b30a68dd99013aeef2860bcb03565bf43cad18da6
+ md5: 2df04ee54a2ce2d34cf375eb02a63725
depends:
- __osx >=11.0
- aom >=3.9.1,<3.10.0a0
- dav1d >=1.2.1,<1.2.2.0a0
- - rav1e >=0.7.1,<0.8.0a0
- - svt-av1 >=3.1.2,<3.1.3.0a0
+ - rav1e >=0.8.1,<0.9.0a0
+ - svt-av1 >=4.0.1,<4.0.2.0a0
license: BSD-2-Clause
license_family: BSD
purls: []
- size: 110723
- timestamp: 1756124882419
-- conda: https://prefix.dev/conda-forge/linux-64/libblas-3.11.0-1_h4a7cf45_openblas.conda
- build_number: 1
- sha256: a36d1230c435d9b06c3bbd1c5c32c695bc341a413719d6e8c4bb6574818f46ea
- md5: 8b39e1ae950f1b54a3959c58ca2c32b8
+ size: 118959
+ timestamp: 1774043016600
+- conda: https://prefix.dev/conda-forge/linux-64/libblas-3.11.0-6_h4a7cf45_openblas.conda
+ build_number: 6
+ sha256: 7bfe936dbb5db04820cf300a9cc1f5ee8d5302fc896c2d66e30f1ee2f20fbfd6
+ md5: 6d6d225559bfa6e2f3c90ee9c03d4e2e
depends:
- - libopenblas >=0.3.30,<0.3.31.0a0
- - libopenblas >=0.3.30,<1.0a0
+ - libopenblas >=0.3.32,<0.3.33.0a0
+ - libopenblas >=0.3.32,<1.0a0
constrains:
- - liblapack 3.11.0 1*_openblas
- - libcblas 3.11.0 1*_openblas
+ - blas 2.306 openblas
+ - liblapack 3.11.0 6*_openblas
+ - liblapacke 3.11.0 6*_openblas
+ - libcblas 3.11.0 6*_openblas
- mkl <2026
- - blas 2.301 openblas
- - liblapacke 3.11.0 1*_openblas
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 18492
- timestamp: 1763447017981
-- conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.11.0-1_h51639a9_openblas.conda
- build_number: 1
- sha256: 7096038e2231bfe315e7e5d3faba2371b70f9d6d897e065afd085781304dc8d1
- md5: 379254bdc34eec0bd4464935c3bff8ba
+ size: 18621
+ timestamp: 1774503034895
+- conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.conda
+ build_number: 6
+ sha256: 979227fc03628925037ab2dfda008eb7b5592644d9c2c21dd285cefe8c42553d
+ md5: e551103471911260488a02155cef9c94
depends:
- - libopenblas >=0.3.30,<0.3.31.0a0
- - libopenblas >=0.3.30,<1.0a0
+ - libopenblas >=0.3.32,<0.3.33.0a0
+ - libopenblas >=0.3.32,<1.0a0
constrains:
- - blas 2.301 openblas
- - liblapacke 3.11.0 1*_openblas
- - liblapack 3.11.0 1*_openblas
- - libcblas 3.11.0 1*_openblas
+ - liblapacke 3.11.0 6*_openblas
+ - liblapack 3.11.0 6*_openblas
+ - blas 2.306 openblas
+ - libcblas 3.11.0 6*_openblas
- mkl <2026
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 18675
- timestamp: 1763447903446
-- conda: https://prefix.dev/conda-forge/linux-64/libcap-2.77-h3ff7636_0.conda
- sha256: 9517cce5193144af0fcbf19b7bd67db0a329c2cc2618f28ffecaa921a1cbe9d3
- md5: 09c264d40c67b82b49a3f3b89037bd2e
+ size: 18859
+ timestamp: 1774504387211
+- conda: https://prefix.dev/conda-forge/linux-64/libcap-2.77-hd0affe5_1.conda
+ sha256: 37c41b1024d0c75da76822e3c079aabaf121618a32fe05e53a897b35a88008fc
+ md5: 499cd8e2d4358986dbe3b30e8fe1bf6a
depends:
- __glibc >=2.17,<3.0.a0
- - attr >=2.5.2,<2.6.0a0
- libgcc >=14
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 121429
- timestamp: 1762349484074
-- conda: https://prefix.dev/conda-forge/linux-64/libcblas-3.11.0-1_h0358290_openblas.conda
- build_number: 1
- sha256: f39c69450d14049463a15adfffa01447cfe9e9497e323800d747ee828ae43a2b
- md5: a670bff9eb7963ea41b4e09a4e4ab608
+ size: 124432
+ timestamp: 1774333989027
+- conda: https://prefix.dev/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda
+ build_number: 6
+ sha256: 57edafa7796f6fa3ebbd5367692dd4c7f552be42109c2dd1a7c89b55089bf374
+ md5: 36ae340a916635b97ac8a0655ace2a35
depends:
- - libblas 3.11.0 1_h4a7cf45_openblas
+ - libblas 3.11.0 6_h4a7cf45_openblas
constrains:
- - blas 2.301 openblas
- - liblapack 3.11.0 1*_openblas
- - liblapacke 3.11.0 1*_openblas
+ - blas 2.306 openblas
+ - liblapack 3.11.0 6*_openblas
+ - liblapacke 3.11.0 6*_openblas
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 18491
- timestamp: 1763447025579
-- conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.11.0-1_hb0561ab_openblas.conda
- build_number: 1
- sha256: 816592d4f39a30db77b5de45e532b6f536f740d333840af21fcf6daf2f0b0c18
- md5: f2b9d50745b55f4a837b333e69b5974a
+ size: 18622
+ timestamp: 1774503050205
+- conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda
+ build_number: 6
+ sha256: 2e6b3e9b1ab672133b70fc6730e42290e952793f132cb5e72eee22835463eba0
+ md5: 805c6d31c5621fd75e53dfcf21fb243a
depends:
- - libblas 3.11.0 1_h51639a9_openblas
+ - libblas 3.11.0 6_h51639a9_openblas
constrains:
- - blas 2.301 openblas
- - liblapacke 3.11.0 1*_openblas
- - liblapack 3.11.0 1*_openblas
+ - liblapacke 3.11.0 6*_openblas
+ - blas 2.306 openblas
+ - liblapack 3.11.0 6*_openblas
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 18671
- timestamp: 1763447915947
+ size: 18863
+ timestamp: 1774504433388
- conda: https://prefix.dev/conda-forge/linux-64/libccd-double-2.1-h59595ed_3.conda
sha256: 4695ce68eda595b4f53146bea1096a9f2e0d33290618ba83a246b5ed8871ebc9
md5: 6a3d962d34385e0a511b859d679f6ea2
@@ -2253,63 +2148,63 @@ packages:
purls: []
size: 29852
timestamp: 1654586481871
-- conda: https://prefix.dev/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda
- sha256: cb83980c57e311783ee831832eb2c20ecb41e7dee6e86e8b70b8cef0e43eab55
- md5: d4a250da4737ee127fb1fa6452a9002e
+- conda: https://prefix.dev/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
+ sha256: 205c4f19550f3647832ec44e35e6d93c8c206782bdd620c1d7cf66237580ff9c
+ md5: 49c553b47ff679a6a1e9fc80b9c5a2d4
depends:
- __glibc >=2.17,<3.0.a0
- - krb5 >=1.21.3,<1.22.0a0
- - libgcc >=13
- - libstdcxx >=13
+ - krb5 >=1.22.2,<1.23.0a0
+ - libgcc >=14
+ - libstdcxx >=14
- libzlib >=1.3.1,<2.0a0
license: Apache-2.0
license_family: Apache
purls: []
- size: 4523621
- timestamp: 1749905341688
-- conda: https://prefix.dev/conda-forge/linux-64/libcurl-8.17.0-h4e3cde8_0.conda
- sha256: 100e29ca864c32af15a5cc354f502d07b2600218740fdf2439fa7d66b50b3529
- md5: 01e149d4a53185622dc2e788281961f2
+ size: 4518030
+ timestamp: 1770902209173
+- conda: https://prefix.dev/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
+ sha256: 75963a5dd913311f59a35dbd307592f4fa754c4808aff9c33edb430c415e38eb
+ md5: c3cc2864f82a944bc90a7beb4d3b0e88
depends:
- __glibc >=2.17,<3.0.a0
- - krb5 >=1.21.3,<1.22.0a0
+ - krb5 >=1.22.2,<1.23.0a0
- libgcc >=14
- - libnghttp2 >=1.67.0,<2.0a0
+ - libnghttp2 >=1.68.1,<2.0a0
- libssh2 >=1.11.1,<2.0a0
- - libzlib >=1.3.1,<2.0a0
- - openssl >=3.5.4,<4.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
- zstd >=1.5.7,<1.6.0a0
license: curl
license_family: MIT
purls: []
- size: 460366
- timestamp: 1762333743748
-- conda: https://prefix.dev/conda-forge/osx-arm64/libcurl-8.17.0-hdece5d2_0.conda
- sha256: 2980c5de44ac3ca2ecbd4a00756da1648ea2945d9e4a2ad9f216c7787df57f10
- md5: 791003efe92c17ed5949b309c61a5ab1
+ size: 468706
+ timestamp: 1777461492876
+- conda: https://prefix.dev/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
+ sha256: 38c0bc634b61e542776e97cfd15d5d41edd304d4e47c333004d2d622439b2381
+ md5: 2f57b7d0c6adda88957586b7afd78438
depends:
- __osx >=11.0
- - krb5 >=1.21.3,<1.22.0a0
- - libnghttp2 >=1.67.0,<2.0a0
+ - krb5 >=1.22.2,<1.23.0a0
+ - libnghttp2 >=1.68.1,<2.0a0
- libssh2 >=1.11.1,<2.0a0
- - libzlib >=1.3.1,<2.0a0
- - openssl >=3.5.4,<4.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
- zstd >=1.5.7,<1.6.0a0
license: curl
license_family: MIT
purls: []
- size: 394183
- timestamp: 1762334288445
-- conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-21.1.6-hf598326_0.conda
- sha256: 6c8d5c50f398035c39f118a6decf91b11d2461c88aef99f81e5c5de200d2a7fa
- md5: 3ea79e55a64bff6c3cbd4588c89a527a
+ size: 400568
+ timestamp: 1777462251987
+- conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
+ sha256: dddd01bd6b338221342a89530a1caffe6051a70cc8f8b1d8bb591d5447a3c603
+ md5: ff484b683fecf1e875dfc7aa01d19796
depends:
- __osx >=11.0
license: Apache-2.0 WITH LLVM-exception
license_family: Apache
purls: []
- size: 569823
- timestamp: 1763470498512
+ size: 569359
+ timestamp: 1778191546305
- conda: https://prefix.dev/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680
md5: 6c77a605a7a689d17d4819c0f8ac9a00
@@ -2408,267 +2303,230 @@ packages:
purls: []
size: 107458
timestamp: 1702146414478
-- conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda
- sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f
- md5: 8b09ae86839581147ef2e5c5e229d164
+- conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
+ sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12
+ md5: a3b390520c563d78cc58974de95a03e5
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
constrains:
- - expat 2.7.3.*
+ - expat 2.8.0.*
license: MIT
license_family: MIT
purls: []
- size: 76643
- timestamp: 1763549731408
-- conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda
- sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98
- md5: b79875dbb5b1db9a4a22a4520f918e1a
+ size: 77241
+ timestamp: 1777846112704
+- conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
+ sha256: f4b1cafc59afaede8fa0a2d9cf376840f1c553001acd72f6ead18bbc8ac8c49c
+ md5: 65466e82c09e888ca7560c11a97d5450
depends:
- __osx >=11.0
constrains:
- - expat 2.7.3.*
+ - expat 2.8.0.*
license: MIT
license_family: MIT
purls: []
- size: 67800
- timestamp: 1763549994166
-- conda: https://prefix.dev/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda
- sha256: 25cbdfa65580cfab1b8d15ee90b4c9f1e0d72128f1661449c9a999d341377d54
- md5: 35f29eec58405aaf55e01cb470d8c26a
+ size: 68789
+ timestamp: 1777846180142
+- conda: https://prefix.dev/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
+ sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6
+ md5: a360c33a5abe61c07959e449fa1453eb
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
license: MIT
license_family: MIT
purls: []
- size: 57821
- timestamp: 1760295480630
-- conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda
- sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f
- md5: 411ff7cd5d1472bba0f55c0faf04453b
+ size: 58592
+ timestamp: 1769456073053
+- conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
+ sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7
+ md5: 43c04d9cb46ef176bb2a4c77e324d599
depends:
- __osx >=11.0
license: MIT
license_family: MIT
purls: []
- size: 40251
- timestamp: 1760295839166
-- conda: https://prefix.dev/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda
- sha256: 65908b75fa7003167b8a8f0001e11e58ed5b1ef5e98b96ab2ba66d7c1b822c7d
- md5: ee48bf17cc83a00f59ca1494d5646869
+ size: 40979
+ timestamp: 1769456747661
+- conda: https://prefix.dev/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda
+ sha256: e755e234236bdda3d265ae82e5b0581d259a9279e3e5b31d745dc43251ad64fb
+ md5: 47595b9d53054907a00d95e4d47af1d6
depends:
- - gettext >=0.21.1,<1.0a0
- - libgcc-ng >=12
- - libogg 1.3.*
- - libogg >=1.3.4,<1.4.0a0
- - libstdcxx-ng >=12
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - libiconv >=1.18,<2.0a0
+ - libogg >=1.3.5,<1.4.0a0
+ - libstdcxx >=14
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 394383
- timestamp: 1687765514062
-- conda: https://prefix.dev/conda-forge/osx-arm64/libflac-1.4.3-hb765f3a_0.conda
- sha256: 3990b52782fe7207ab642df25368ed443094f6d1a7ea61854935c24192b388aa
- md5: 356faba64411660f6c4d24ea31640733
+ size: 424563
+ timestamp: 1764526740626
+- conda: https://prefix.dev/conda-forge/osx-arm64/libflac-1.5.0-h6824b09_1.conda
+ sha256: 565e5c38e80eb5752bfa52f6b771fa2fd75c0551a1f61ea84a12a2fb4454900c
+ md5: b14b19359aed60c200b1ca58b53f5ca3
depends:
- - gettext >=0.21.1,<1.0a0
- - libcxx >=15.0.7
- - libogg 1.3.*
- - libogg >=1.3.4,<1.4.0a0
+ - __osx >=11.0
+ - libcxx >=19
+ - libiconv >=1.18,<2.0a0
+ - libogg >=1.3.5,<1.4.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 314408
- timestamp: 1687766236790
-- conda: https://prefix.dev/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda
- sha256: 4641d37faeb97cf8a121efafd6afd040904d4bca8c46798122f417c31d5dfbec
- md5: f4084e4e6577797150f9b04a4560ceb0
+ size: 321872
+ timestamp: 1764527007890
+- conda: https://prefix.dev/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
+ sha256: 38f014a7129e644636e46064ecd6b1945e729c2140e21d75bb476af39e692db2
+ md5: e289f3d17880e44b633ba911d57a321b
depends:
- - libfreetype6 >=2.14.1
+ - libfreetype6 >=2.14.3
license: GPL-2.0-only OR FTL
purls: []
- size: 7664
- timestamp: 1757945417134
-- conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda
- sha256: 9de25a86066f078822d8dd95a83048d7dc2897d5d655c0e04a8a54fca13ef1ef
- md5: f35fb38e89e2776994131fbf961fa44b
+ size: 8049
+ timestamp: 1774298163029
+- conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
+ sha256: a047a2f238362a37d484f9620e8cba29f513a933cd9eb68571ad4b270d6f8f3e
+ md5: f73b109d49568d5d1dda43bb147ae37f
depends:
- - libfreetype6 >=2.14.1
+ - libfreetype6 >=2.14.3
license: GPL-2.0-only OR FTL
purls: []
- size: 7810
- timestamp: 1757947168537
-- conda: https://prefix.dev/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda
- sha256: 4a7af818a3179fafb6c91111752954e29d3a2a950259c14a2fc7ba40a8b03652
- md5: 8e7251989bca326a28f4a5ffbd74557a
+ size: 8091
+ timestamp: 1774298691258
+- conda: https://prefix.dev/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda
+ sha256: 16f020f96da79db1863fcdd8f2b8f4f7d52f177dd4c58601e38e9182e91adf1d
+ md5: fb16b4b69e3f1dcfe79d80db8fd0c55d
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
- - libpng >=1.6.50,<1.7.0a0
- - libzlib >=1.3.1,<2.0a0
+ - libpng >=1.6.55,<1.7.0a0
+ - libzlib >=1.3.2,<2.0a0
constrains:
- - freetype >=2.14.1
+ - freetype >=2.14.3
license: GPL-2.0-only OR FTL
purls: []
- size: 386739
- timestamp: 1757945416744
-- conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda
- sha256: cc4aec4c490123c0f248c1acd1aeab592afb6a44b1536734e20937cda748f7cd
- md5: 6d4ede03e2a8e20eb51f7f681d2a2550
+ size: 384575
+ timestamp: 1774298162622
+- conda: https://prefix.dev/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_0.conda
+ sha256: ff764608e1f2839e95e2cf9b243681475f8778c36af7a42b3f78f476fdbb1dd3
+ md5: e98ba7b5f09a5f450eca083d5a1c4649
depends:
- __osx >=11.0
- - libpng >=1.6.50,<1.7.0a0
- - libzlib >=1.3.1,<2.0a0
+ - libpng >=1.6.55,<1.7.0a0
+ - libzlib >=1.3.2,<2.0a0
constrains:
- - freetype >=2.14.1
+ - freetype >=2.14.3
license: GPL-2.0-only OR FTL
purls: []
- size: 346703
- timestamp: 1757947166116
-- conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda
- sha256: 08f9b87578ab981c7713e4e6a7d935e40766e10691732bba376d4964562bcb45
- md5: c0374badb3a5d4b1372db28d19462c53
+ size: 338085
+ timestamp: 1774298689297
+- conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda
+ sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46
+ md5: 57736f29cc2b0ec0b6c2952d3f101b6a
depends:
- __glibc >=2.17,<3.0.a0
- _openmp_mutex >=4.5
constrains:
- - libgomp 15.2.0 h767d61c_7
- - libgcc-ng ==15.2.0=*_7
+ - libgcc-ng ==15.2.0=*_19
+ - libgomp 15.2.0 he0feb66_19
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 822552
- timestamp: 1759968052178
-- conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda
- sha256: 2045066dd8e6e58aaf5ae2b722fb6dfdbb57c862b5f34ac7bfb58c40ef39b6ad
- md5: 280ea6eee9e2ddefde25ff799c4f0363
+ size: 1041084
+ timestamp: 1778269013026
+- conda: https://prefix.dev/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda
+ sha256: 06644fa4d34d57c9e48f4d84b1256f9e5f654fdb37f43acc8a58a396952d42b7
+ md5: 644058123986582db33aebd4ae2ca184
depends:
- - libgcc 15.2.0 h767d61c_7
+ - _openmp_mutex
+ constrains:
+ - libgcc-ng ==15.2.0=*_19
+ - libgomp 15.2.0 19
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 29313
- timestamp: 1759968065504
-- conda: https://prefix.dev/conda-forge/linux-64/libgd-2.3.3-h5fbf134_12.conda
- sha256: 245be793e831170504f36213134f4c24eedaf39e634679809fd5391ad214480b
- md5: 88c1c66987cd52a712eea89c27104be6
+ size: 404080
+ timestamp: 1778273064154
+- conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda
+ sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98
+ md5: 331ee9b72b9dff570d56b1302c5ab37d
+ depends:
+ - libgcc 15.2.0 he0feb66_19
+ license: GPL-3.0-only WITH GCC-exception-3.1
+ license_family: GPL
+ purls: []
+ size: 27694
+ timestamp: 1778269016987
+- conda: https://prefix.dev/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda
+ sha256: 19e5be91445db119152217e8e8eec4fd0499d854acc7d8062044fb55a70971cd
+ md5: 68fc66282364981589ef36868b1a7c78
depends:
- __glibc >=2.17,<3.0.a0
- fontconfig >=2.15.0,<3.0a0
- fonts-conda-ecosystem
- - icu >=78.1,<79.0a0
- - libexpat >=2.7.3,<3.0a0
- - libfreetype >=2.14.1
- - libfreetype6 >=2.14.1
- - libgcc >=14
- - libjpeg-turbo >=3.1.2,<4.0a0
- - libpng >=1.6.53,<1.7.0a0
- - libtiff >=4.7.1,<4.8.0a0
- - libwebp-base >=1.6.0,<2.0a0
+ - freetype >=2.12.1,<3.0a0
+ - icu >=75.1,<76.0a0
+ - libexpat >=2.6.4,<3.0a0
+ - libgcc >=13
+ - libjpeg-turbo >=3.0.0,<4.0a0
+ - libpng >=1.6.45,<1.7.0a0
+ - libtiff >=4.7.0,<4.8.0a0
+ - libwebp-base >=1.5.0,<2.0a0
- libzlib >=1.3.1,<2.0a0
license: GD
license_family: BSD
purls: []
- size: 177306
- timestamp: 1766331805898
-- conda: https://prefix.dev/conda-forge/osx-arm64/libgd-2.3.3-h05bcc79_12.conda
- sha256: 269edce527e204a80d3d05673301e0207efcd0dbeebc036a118ceb52690d6341
- md5: fa4a92cfaae9570d89700a292a9ca714
+ size: 177082
+ timestamp: 1737548051015
+- conda: https://prefix.dev/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda
+ sha256: be038eb8dfe296509aee2df21184c72cb76285b0340448525664bc396aa6146d
+ md5: 4581aa3cfcd1a90967ed02d4a9f3db4b
depends:
- __osx >=11.0
- fontconfig >=2.15.0,<3.0a0
- fonts-conda-ecosystem
- - icu >=78.1,<79.0a0
- - libexpat >=2.7.3,<3.0a0
- - libfreetype >=2.14.1
- - libfreetype6 >=2.14.1
- - libiconv >=1.18,<2.0a0
- - libjpeg-turbo >=3.1.2,<4.0a0
- - libpng >=1.6.53,<1.7.0a0
- - libtiff >=4.7.1,<4.8.0a0
- - libwebp-base >=1.6.0,<2.0a0
+ - freetype >=2.12.1,<3.0a0
+ - icu >=75.1,<76.0a0
+ - libexpat >=2.6.4,<3.0a0
+ - libiconv >=1.17,<2.0a0
+ - libjpeg-turbo >=3.0.0,<4.0a0
+ - libpng >=1.6.45,<1.7.0a0
+ - libtiff >=4.7.0,<4.8.0a0
+ - libwebp-base >=1.5.0,<2.0a0
- libzlib >=1.3.1,<2.0a0
license: GD
license_family: BSD
purls: []
- size: 159247
- timestamp: 1766331953491
-- conda: https://prefix.dev/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda
- sha256: 50a9e9815cf3f5bce1b8c5161c0899cc5b6c6052d6d73a4c27f749119e607100
- md5: 2f4de899028319b27eb7a4023be5dfd2
- depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=14
- - libiconv >=1.18,<2.0a0
- license: GPL-3.0-or-later
- license_family: GPL
- purls: []
- size: 188293
- timestamp: 1753342911214
-- conda: https://prefix.dev/conda-forge/osx-arm64/libgettextpo-0.25.1-h493aca8_0.conda
- sha256: 3ba35ff26b3b9573b5df5b9bbec5c61476157ec3a9f12c698e2a9350cd4338fd
- md5: 98acd9989d0d8d5914ccc86dceb6c6c2
- depends:
- - __osx >=11.0
- - libiconv >=1.18,<2.0a0
- - libintl 0.25.1 h493aca8_0
- license: GPL-3.0-or-later
- license_family: GPL
- purls: []
- size: 183091
- timestamp: 1751558452316
-- conda: https://prefix.dev/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda
- sha256: c7ea10326fd450a2a21955987db09dde78c99956a91f6f05386756a7bfe7cc04
- md5: 3f7a43b3160ec0345c9535a9f0d7908e
+ size: 156868
+ timestamp: 1737548290283
+- conda: https://prefix.dev/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda
+ sha256: 561a42758ef25b9ce308c4e2cf56daee4f06138385a17e29a492cd928e00be6f
+ md5: 42bf7eca1a951735fa06c0e3c0d5c8e6
depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=14
- - libgettextpo 0.25.1 h3f43e3d_1
- - libiconv >=1.18,<2.0a0
- license: GPL-3.0-or-later
- license_family: GPL
- purls: []
- size: 37407
- timestamp: 1753342931100
-- conda: https://prefix.dev/conda-forge/osx-arm64/libgettextpo-devel-0.25.1-h493aca8_0.conda
- sha256: 976941e18f879e5c1e67553f9657f7bb9d3935c89014ebfeafe89dcfba2de9e7
- md5: 91c2fdde1cb4a61b5cb7afa682af359e
- depends:
- - __osx >=11.0
- - libgettextpo 0.25.1 h493aca8_0
- - libiconv >=1.18,<2.0a0
- - libintl 0.25.1 h493aca8_0
- license: GPL-3.0-or-later
- license_family: GPL
- purls: []
- size: 37894
- timestamp: 1751558502415
-- conda: https://prefix.dev/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_7.conda
- sha256: 9ca24328e31c8ef44a77f53104773b9fe50ea8533f4c74baa8489a12de916f02
- md5: 8621a450add4e231f676646880703f49
- depends:
- - libgfortran5 15.2.0 hcd61629_7
+ - libgfortran5 15.2.0 h68bc16d_19
constrains:
- - libgfortran-ng ==15.2.0=*_7
+ - libgfortran-ng ==15.2.0=*_19
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 29275
- timestamp: 1759968110483
-- conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda
- sha256: e9a5d1208b9dc0b576b35a484d527d9b746c4e65620e0d77c44636033b2245f0
- md5: f699348e3f4f924728e33551b1920f79
+ size: 27655
+ timestamp: 1778269042954
+- conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_19.conda
+ sha256: d4837b3b9b30af3132d260225e91ab9dde83be04c59513f500cc81050fb37486
+ md5: 1ea03f87cdb1078fbc0e2b2deb63752c
depends:
- - libgfortran5 15.2.0 h742603c_1
+ - libgfortran5 15.2.0 hdae7583_19
+ constrains:
+ - libgfortran-ng ==15.2.0=*_19
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 134016
- timestamp: 1759712902814
-- conda: https://prefix.dev/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_7.conda
- sha256: e93ceda56498d98c9f94fedec3e2d00f717cbedfc97c49be0e5a5828802f2d34
- md5: f116940d825ffc9104400f0d7f1a4551
+ size: 139675
+ timestamp: 1778273280875
+- conda: https://prefix.dev/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda
+ sha256: 057978bb69fea29ed715a9b98adf71015c31baecc4aeb2bfc20d4fd5d83579d4
+ md5: 85072b0ad177c966294f129b7c04a2d5
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=15.2.0
@@ -2677,20 +2535,20 @@ packages:
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 1572758
- timestamp: 1759968082504
-- conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda
- sha256: 18808697013a625ca876eeee3d86ee5b656f17c391eca4a4bc70867717cc5246
- md5: afccf412b03ce2f309f875ff88419173
+ size: 2483673
+ timestamp: 1778269025089
+- conda: https://prefix.dev/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
+ sha256: d0a68b7a121d115b80c169e24d1265dcc25a3fe58d107df1bbc430797e226d88
+ md5: ba36d8c606a6a53fe0b8c12d47267b3d
depends:
- - llvm-openmp >=8.0.0
+ - libgcc >=15.2.0
constrains:
- libgfortran 15.2.0
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 764028
- timestamp: 1759712189275
+ size: 599691
+ timestamp: 1778273075448
- conda: https://prefix.dev/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda
sha256: dc2752241fa3d9e40ce552c1942d0a4b5eeb93740c9723873f6fcf8d39ef8d2d
md5: 928b8be80851f5d8ffb016f9c81dae7a
@@ -2713,38 +2571,38 @@ packages:
purls: []
size: 113911
timestamp: 1731331012126
-- conda: https://prefix.dev/conda-forge/linux-64/libglib-2.86.4-h6548e54_0.conda
- sha256: 0d8cf491cb00aeb35fcfb68dfcb5b0ad188a98fb35c21c2421d2b2acc128cbf5
- md5: b7113551db5a3e2403cdd052c66e9999
+- conda: https://prefix.dev/conda-forge/linux-64/libglib-2.88.1-h0d30a3d_1.conda
+ sha256: a0899efbae2a6a9102c796c0b11ac371a3190da5afa28512eeb2879c65d1419c
+ md5: 6016ea5ee9e986bc683879408cc87529
depends:
- __glibc >=2.17,<3.0.a0
- - libffi >=3.5.2,<3.6.0a0
- libgcc >=14
- - libiconv >=1.18,<2.0a0
- - libzlib >=1.3.1,<2.0a0
- pcre2 >=10.47,<10.48.0a0
+ - libffi >=3.5.2,<3.6.0a0
+ - libiconv >=1.18,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
constrains:
- - glib 2.86.4 *_0
+ - glib >2.66
license: LGPL-2.1-or-later
purls: []
- size: 4432190
- timestamp: 1771291719860
-- conda: https://prefix.dev/conda-forge/osx-arm64/libglib-2.86.4-he378b5c_0.conda
- sha256: e305f7b1f2202d4efcdb8856abb28d79dc012d85a2155fbfbfee96069e017073
- md5: 2d02b60ec23066e45c578c1524e9ca12
+ size: 4754370
+ timestamp: 1777904907738
+- conda: https://prefix.dev/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_1.conda
+ sha256: 0a55d29313baf76e46443c4b1ca4fa28b845df67e02b1f40f9847519d16c42fd
+ md5: ed49aeb876b26dbd9d20c2bb3bad1080
depends:
- __osx >=11.0
- - libffi >=3.5.2,<3.6.0a0
+ - pcre2 >=10.47,<10.48.0a0
- libiconv >=1.18,<2.0a0
- libintl >=0.25.1,<1.0a0
- - libzlib >=1.3.1,<2.0a0
- - pcre2 >=10.47,<10.48.0a0
+ - libffi >=3.5.2,<3.6.0a0
+ - libzlib >=1.3.2,<2.0a0
constrains:
- - glib 2.86.4 *_0
+ - glib >2.66
license: LGPL-2.1-or-later
purls: []
- size: 4124444
- timestamp: 1771293559119
+ size: 4439433
+ timestamp: 1777905039771
- conda: https://prefix.dev/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda
sha256: 1175f8a7a0c68b7f81962699751bb6574e6f07db4c9f72825f978e3016f46850
md5: 434ca7e50e40f4918ab701e3facd59a0
@@ -2777,16 +2635,16 @@ packages:
purls: []
size: 26388
timestamp: 1731331003255
-- conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda
- sha256: e9fb1c258c8e66ee278397b5822692527c5f5786d372fe7a869b900853f3f5ca
- md5: f7b4d76975aac7e5d9e6ad13845f92fe
+- conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b
+ md5: faac990cb7aedc7f3a2224f2c9b0c26c
depends:
- __glibc >=2.17,<3.0.a0
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 447919
- timestamp: 1759967942498
+ size: 603817
+ timestamp: 1778268942614
- conda: https://prefix.dev/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f
md5: 915f5995e94f60e9a4826e0b0920ee88
@@ -2816,20 +2674,9 @@ packages:
purls: []
size: 90957
timestamp: 1751558394144
-- conda: https://prefix.dev/conda-forge/osx-arm64/libintl-devel-0.25.1-h493aca8_0.conda
- sha256: 5a446cb0501d87e0816da0bce524c60a053a4cf23c94dfd3e2b32a8499009e36
- md5: 5f9888e1cdbbbef52c8cf8b567393535
- depends:
- - __osx >=11.0
- - libiconv >=1.18,<2.0a0
- - libintl 0.25.1 h493aca8_0
- license: LGPL-2.1-or-later
- purls: []
- size: 40340
- timestamp: 1751558481257
-- conda: https://prefix.dev/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda
- sha256: cc9aba923eea0af8e30e0f94f2ad7156e2984d80d1e8e7fe6be5a1f257f0eb32
- md5: 8397539e3a0bbd1695584fb4f927485a
+- conda: https://prefix.dev/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
+ sha256: 10056646c28115b174de81a44e23e3a0a3b95b5347d2e6c45cc6d49d35294256
+ md5: 6178c6f2fb254558238ef4e6c56fb782
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
@@ -2837,93 +2684,93 @@ packages:
- jpeg <0.0.0a
license: IJG AND BSD-3-Clause AND Zlib
purls: []
- size: 633710
- timestamp: 1762094827865
-- conda: https://prefix.dev/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda
- sha256: 6c061c56058bb10374daaef50e81b39cf43e8aee21f0037022c0c39c4f31872f
- md5: f0695fbecf1006f27f4395d64bd0c4b8
+ size: 633831
+ timestamp: 1775962768273
+- conda: https://prefix.dev/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
+ sha256: 17e035ae6a520ff6a6bb5dd93a4a7c3895891f4f9743bcb8c6ef607445a31cd0
+ md5: b8a7544c83a67258b0e8592ec6a5d322
depends:
- __osx >=11.0
constrains:
- jpeg <0.0.0a
license: IJG AND BSD-3-Clause AND Zlib
purls: []
- size: 551197
- timestamp: 1762095054358
-- conda: https://prefix.dev/conda-forge/linux-64/liblapack-3.11.0-1_h47877c9_openblas.conda
- build_number: 1
- sha256: b87938dc1220984c4313045d97422723f96ba4639676639a95ba144e2359f875
- md5: dee12a83aa4aca5077ea23c0605de044
+ size: 555681
+ timestamp: 1775962975624
+- conda: https://prefix.dev/conda-forge/linux-64/liblapack-3.11.0-6_h47877c9_openblas.conda
+ build_number: 6
+ sha256: 371f517eb7010b21c6cc882c7606daccebb943307cb9a3bf2c70456a5c024f7d
+ md5: 881d801569b201c2e753f03c84b85e15
depends:
- - libblas 3.11.0 1_h4a7cf45_openblas
+ - libblas 3.11.0 6_h4a7cf45_openblas
constrains:
- - libcblas 3.11.0 1*_openblas
- - blas 2.301 openblas
- - liblapacke 3.11.0 1*_openblas
+ - blas 2.306 openblas
+ - liblapacke 3.11.0 6*_openblas
+ - libcblas 3.11.0 6*_openblas
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 18486
- timestamp: 1763447033135
-- conda: https://prefix.dev/conda-forge/osx-arm64/liblapack-3.11.0-1_hd9741b5_openblas.conda
- build_number: 1
- sha256: acee73900f85c8cf2db56540e905c8ac32e08bccc08d8b54bf4091b5a9ad1ed9
- md5: 5659bf8243896cb24e3de819d422b1a3
+ size: 18624
+ timestamp: 1774503065378
+- conda: https://prefix.dev/conda-forge/osx-arm64/liblapack-3.11.0-6_hd9741b5_openblas.conda
+ build_number: 6
+ sha256: 21606b7346810559e259807497b86f438950cf19e71838e44ebaf4bd2b35b549
+ md5: ee33d2d05a7c5ea1f67653b37eb74db1
depends:
- - libblas 3.11.0 1_h51639a9_openblas
+ - libblas 3.11.0 6_h51639a9_openblas
constrains:
- - blas 2.301 openblas
- - liblapacke 3.11.0 1*_openblas
- - libcblas 3.11.0 1*_openblas
+ - liblapacke 3.11.0 6*_openblas
+ - libcblas 3.11.0 6*_openblas
+ - blas 2.306 openblas
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 18703
- timestamp: 1763447928749
-- conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda
- sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8
- md5: 1a580f7796c7bf6393fddb8bbbde58dc
+ size: 18863
+ timestamp: 1774504467905
+- conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d
+ md5: b88d90cad08e6bc8ad540cb310a761fb
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
constrains:
- - xz 5.8.1.*
+ - xz 5.8.3.*
license: 0BSD
purls: []
- size: 112894
- timestamp: 1749230047870
-- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda
- sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285
- md5: d6df911d4564d77c4374b02552cb17d1
+ size: 113478
+ timestamp: 1775825492909
+- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e
+ md5: b1fd823b5ae54fbec272cea0811bd8a9
depends:
- __osx >=11.0
constrains:
- - xz 5.8.1.*
+ - xz 5.8.3.*
license: 0BSD
purls: []
- size: 92286
- timestamp: 1749230283517
-- conda: https://prefix.dev/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda
- sha256: 329e66330a8f9cbb6a8d5995005478188eb4ba8a6b6391affa849744f4968492
- md5: f61edadbb301530bd65a32646bd81552
+ size: 92472
+ timestamp: 1775825802659
+- conda: https://prefix.dev/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda
+ sha256: 7858f6a173206bc8a5bdc8e75690483bb66c0dcc3809ac1cb43c561a4723623a
+ md5: 55c20edec8e90c4703787acaade60808
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - liblzma 5.8.1 hb9d3cd8_2
+ - libgcc >=14
+ - liblzma 5.8.3 hb03c661_0
license: 0BSD
purls: []
- size: 439868
- timestamp: 1749230061968
-- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda
- sha256: 974804430e24f0b00f3a48b67ec10c9f5441c9bb3d82cc0af51ba45b8a75a241
- md5: 1201137f1a5ec9556032ffc04dcdde8d
+ size: 491429
+ timestamp: 1775825511214
+- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-devel-5.8.3-h8088a28_0.conda
+ sha256: 3002be39c0e98ec6cd103b0dc2963dc9e0d7cab127fb2fe9a8de9707a76ed1f0
+ md5: ebe1f5418d6e2d4bbc26b2c906a0a470
depends:
- __osx >=11.0
- - liblzma 5.8.1 h39f12f2_2
+ - liblzma 5.8.3 h8088a28_0
license: 0BSD
purls: []
- size: 116244
- timestamp: 1749230297170
+ size: 118482
+ timestamp: 1775825828010
- conda: https://prefix.dev/conda-forge/linux-64/libmad-0.15.1b-h0b41bf4_1001.conda
sha256: 9e94cec54c4baadaa652c761179b8d32771fe7fa55faf6c78c2e35f942367f74
md5: dc5cc4700f02ffeecc48253c9f29025b
@@ -2942,39 +2789,39 @@ packages:
purls: []
size: 77255
timestamp: 1670815732700
-- conda: https://prefix.dev/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda
- sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690
- md5: b499ce4b026493a13774bcf0f4c33849
+- conda: https://prefix.dev/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
+ sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f
+ md5: 2a45e7f8af083626f009645a6481f12d
depends:
- __glibc >=2.17,<3.0.a0
- - c-ares >=1.34.5,<2.0a0
+ - c-ares >=1.34.6,<2.0a0
- libev >=4.33,<4.34.0a0
- libev >=4.33,<5.0a0
- libgcc >=14
- libstdcxx >=14
- libzlib >=1.3.1,<2.0a0
- - openssl >=3.5.2,<4.0a0
+ - openssl >=3.5.5,<4.0a0
license: MIT
license_family: MIT
purls: []
- size: 666600
- timestamp: 1756834976695
-- conda: https://prefix.dev/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda
- sha256: a07cb53b5ffa2d5a18afc6fd5a526a5a53dd9523fbc022148bd2f9395697c46d
- md5: a4b4dd73c67df470d091312ab87bf6ae
+ size: 663344
+ timestamp: 1773854035739
+- conda: https://prefix.dev/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
+ sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a
+ md5: 6ea18834adbc3b33df9bd9fb45eaf95b
depends:
- __osx >=11.0
- - c-ares >=1.34.5,<2.0a0
+ - c-ares >=1.34.6,<2.0a0
- libcxx >=19
- libev >=4.33,<4.34.0a0
- libev >=4.33,<5.0a0
- libzlib >=1.3.1,<2.0a0
- - openssl >=3.5.2,<4.0a0
+ - openssl >=3.5.5,<4.0a0
license: MIT
license_family: MIT
purls: []
- size: 575454
- timestamp: 1756835746393
+ size: 576526
+ timestamp: 1773854624224
- conda: https://prefix.dev/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5
md5: d864d34357c3b65a4b731f78c0801dc4
@@ -3035,57 +2882,57 @@ packages:
purls: []
size: 216719
timestamp: 1745826006052
-- conda: https://prefix.dev/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda
- sha256: 199d79c237afb0d4780ccd2fbf829cea80743df60df4705202558675e07dd2c5
- md5: be43915efc66345cccb3c310b6ed0374
+- conda: https://prefix.dev/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda
+ sha256: 6dc30b28f32737a1c52dada10c8f3a41bc9e021854215efca04a7f00487d09d9
+ md5: 89d61bc91d3f39fda0ca10fcd3c68594
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
- libgfortran
- libgfortran5 >=14.3.0
constrains:
- - openblas >=0.3.30,<0.3.31.0a0
+ - openblas >=0.3.32,<0.3.33.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 5927939
- timestamp: 1763114673331
-- conda: https://prefix.dev/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda
- sha256: dcc626c7103503d1dfc0371687ad553cb948b8ed0249c2a721147bdeb8db4a73
- md5: a18a7f471c517062ee71b843ef95eb8a
+ size: 5928890
+ timestamp: 1774471724897
+- conda: https://prefix.dev/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda
+ sha256: 713e453bde3531c22a660577e59bf91ef578dcdfd5edb1253a399fa23514949a
+ md5: 3a1111a4b6626abebe8b978bb5a323bf
depends:
- __osx >=11.0
- libgfortran
- libgfortran5 >=14.3.0
- llvm-openmp >=19.1.7
constrains:
- - openblas >=0.3.30,<0.3.31.0a0
+ - openblas >=0.3.32,<0.3.33.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 4285762
- timestamp: 1761749506256
-- conda: https://prefix.dev/conda-forge/linux-64/libopus-1.5.2-hd0c01bc_0.conda
- sha256: 786d43678d6d1dc5f88a6bad2d02830cfd5a0184e84a8caa45694049f0e3ea5f
- md5: b64523fb87ac6f87f0790f324ad43046
+ size: 4308797
+ timestamp: 1774472508546
+- conda: https://prefix.dev/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda
+ sha256: f1061a26213b9653bbb8372bfa3f291787ca091a9a3060a10df4d5297aad74fd
+ md5: 2446ac1fe030c2aa6141386c1f5a6aed
depends:
- - libgcc >=13
- __glibc >=2.17,<3.0.a0
+ - libgcc >=14
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 312472
- timestamp: 1744330953241
-- conda: https://prefix.dev/conda-forge/osx-arm64/libopus-1.5.2-h48c0fde_0.conda
- sha256: 3a01094a59dd59d7a5a1c8e838c2ef3fccf9e098af575c38c26fceb56c6bb917
- md5: 882feb9903f31dca2942796a360d1007
+ size: 324993
+ timestamp: 1768497114401
+- conda: https://prefix.dev/conda-forge/osx-arm64/libopus-1.6.1-h1a92334_0.conda
+ sha256: 5c95a5f7712f543c59083e62fc3a95efec8b7f3773fbf4542ad1fb87fbf51ff4
+ md5: 7f414dd3fd1cb7a76e51fec074a9c49e
depends:
- __osx >=11.0
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 299498
- timestamp: 1744330988108
+ size: 308000
+ timestamp: 1768497248058
- conda: https://prefix.dev/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda
sha256: 0bd91de9b447a2991e666f284ae8c722ffb1d84acb594dbd0c031bd656fa32b2
md5: 70e3400cbbfa03e96dcde7fc13e38c7b
@@ -3097,27 +2944,27 @@ packages:
purls: []
size: 28424
timestamp: 1749901812541
-- conda: https://prefix.dev/conda-forge/linux-64/libpng-1.6.55-h421ea60_0.conda
- sha256: 36ade759122cdf0f16e2a2562a19746d96cf9c863ffaa812f2f5071ebbe9c03c
- md5: 5f13ffc7d30ffec87864e678df9957b4
+- conda: https://prefix.dev/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
+ sha256: 377cfe037f3eeb3b1bf3ad333f724a64d32f315ee1958581fc671891d63d3f89
+ md5: eba48a68a1a2b9d3c0d9511548db85db
depends:
- - libgcc >=14
- __glibc >=2.17,<3.0.a0
- - libzlib >=1.3.1,<2.0a0
+ - libgcc >=14
+ - libzlib >=1.3.2,<2.0a0
license: zlib-acknowledgement
purls: []
- size: 317669
- timestamp: 1770691470744
-- conda: https://prefix.dev/conda-forge/osx-arm64/libpng-1.6.55-h132b30e_0.conda
- sha256: 7a4fd29a6ee2d7f7a6e610754dfdf7410ed08f40d8d8b488a27bc0f9981d5abb
- md5: 871dc88b0192ac49b6a5509932c31377
+ size: 317729
+ timestamp: 1776315175087
+- conda: https://prefix.dev/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
+ sha256: 66eae34546df1f098a67064970c92aa14ae7a7505091889e00468294d2882c36
+ md5: 2259ae0949dbe20c0665850365109b27
depends:
- __osx >=11.0
- - libzlib >=1.3.1,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
license: zlib-acknowledgement
purls: []
- size: 288950
- timestamp: 1770691485950
+ size: 289546
+ timestamp: 1776315246750
- conda: https://prefix.dev/conda-forge/linux-64/libprotobuf-3.20.2-h6239696_0.tar.bz2
sha256: 158899bc39d00dc79e9b3fe8e39ed413a7ae266ddf69a7ff149be16859c24149
md5: dd9c398d02aa82219a4ea5fa69739ae7
@@ -3141,118 +2988,95 @@ packages:
purls: []
size: 2281852
timestamp: 1668470370774
-- conda: https://prefix.dev/conda-forge/linux-64/librsvg-2.60.0-h61e6d4b_0.conda
- sha256: 960b137673b2b8293e2a12d194add72967b3bf12fcdf691e7ad8bd5c8318cec3
- md5: 91e6d4d684e237fba31b9815c4b40edf
+- conda: https://prefix.dev/conda-forge/linux-64/librsvg-2.60.2-h61e6d4b_0.conda
+ sha256: 38b3189cf246f7265e06917f32d046ac375117c88834d045efe73ec48ceacc59
+ md5: d62da3d560992bfa2feb611d7be813b8
depends:
- __glibc >=2.17,<3.0.a0
- cairo >=1.18.4,<2.0a0
- - gdk-pixbuf >=2.44.3,<3.0a0
+ - gdk-pixbuf >=2.44.5,<3.0a0
- libgcc >=14
- - libglib >=2.86.0,<3.0a0
+ - libglib >=2.86.4,<3.0a0
- libxml2-16 >=2.14.6
- pango >=1.56.4,<2.0a0
constrains:
- __glibc >=2.17
license: LGPL-2.1-or-later
purls: []
- size: 3421977
- timestamp: 1759327942156
-- conda: https://prefix.dev/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda
- sha256: ca5a2de5d3f68e8d6443ea1bf193c1596a278e6f86018017c0ccd4928eaf8971
- md5: 05ad1d6b6fb3b384f7a07128025725cb
+ size: 4011590
+ timestamp: 1771399906142
+- conda: https://prefix.dev/conda-forge/osx-arm64/librsvg-2.60.0-h9001022_1.conda
+ sha256: 9ba1531b46c75313736e2ef8ba1a4d3ef7d3ffd22efe8d593bfa99da7143a1cb
+ md5: b6c81d5b3324b9ff9fe8f39d25d8be66
depends:
- __osx >=11.0
- cairo >=1.18.4,<2.0a0
- - gdk-pixbuf >=2.44.3,<3.0a0
- - libglib >=2.86.0,<3.0a0
+ - gdk-pixbuf >=2.44.5,<3.0a0
+ - libglib >=2.86.4,<3.0a0
- libxml2-16 >=2.14.6
- pango >=1.56.4,<2.0a0
constrains:
- __osx >=11.0
license: LGPL-2.1-or-later
purls: []
- size: 2344343
- timestamp: 1759328503184
-- conda: https://prefix.dev/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda
- sha256: f709cbede3d4f3aee4e2f8d60bd9e256057f410bd60b8964cb8cf82ec1457573
- md5: ef1910918dd895516a769ed36b5b3a4e
+ size: 2357137
+ timestamp: 1771302195666
+- conda: https://prefix.dev/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda
+ sha256: 57cb5f92110324c04498b96563211a1bca6a74b2918b1e8df578bfed03cc32e4
+ md5: 067590f061c9f6ea7e61e3b2112ed6b3
depends:
+ - __glibc >=2.17,<3.0.a0
- lame >=3.100,<3.101.0a0
- - libflac >=1.4.3,<1.5.0a0
- - libgcc-ng >=12
- - libogg >=1.3.4,<1.4.0a0
- - libopus >=1.3.1,<2.0a0
- - libstdcxx-ng >=12
+ - libflac >=1.5.0,<1.6.0a0
+ - libgcc >=14
+ - libogg >=1.3.5,<1.4.0a0
+ - libopus >=1.5.2,<2.0a0
+ - libstdcxx >=14
- libvorbis >=1.3.7,<1.4.0a0
- - mpg123 >=1.32.1,<1.33.0a0
+ - mpg123 >=1.32.9,<1.33.0a0
license: LGPL-2.1-or-later
license_family: LGPL
purls: []
- size: 354372
- timestamp: 1695747735668
-- conda: https://prefix.dev/conda-forge/osx-arm64/libsndfile-1.2.2-h9739721_1.conda
- sha256: e559f2f72bb03a554aa5b74230fa19160d33c7981ed385294f1eea9a5871cc03
- md5: 77d552455cbc52e089cdb9df5b283199
+ size: 355619
+ timestamp: 1765181778282
+- conda: https://prefix.dev/conda-forge/osx-arm64/libsndfile-1.2.2-hf95f74e_2.conda
+ sha256: f549aafe57439ca7c362884968f24fcc35a26e4e1eb66bb6985aa9e553c25521
+ md5: 27e1a5a9249833f98a739249c7d1518b
depends:
+ - __osx >=11.0
- lame >=3.100,<3.101.0a0
- - libcxx >=15.0.7
- - libflac >=1.4.3,<1.5.0a0
- - libogg >=1.3.4,<1.4.0a0
- - libopus >=1.3.1,<2.0a0
+ - libcxx >=19
+ - libflac >=1.5.0,<1.6.0a0
+ - libogg >=1.3.5,<1.4.0a0
+ - libopus >=1.5.2,<2.0a0
- libvorbis >=1.3.7,<1.4.0a0
- - mpg123 >=1.32.1,<1.33.0a0
+ - mpg123 >=1.32.9,<1.33.0a0
license: LGPL-2.1-or-later
license_family: LGPL
purls: []
- size: 317185
- timestamp: 1695747981394
-- conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda
- sha256: 4c992dcd0e34b68f843e75406f7f303b1b97c248d18f3c7c330bdc0bc26ae0b3
- md5: 729a572a3ebb8c43933b30edcc628ceb
- depends:
- - __glibc >=2.17,<3.0.a0
- - icu >=75.1,<76.0a0
- - libgcc >=14
- - libzlib >=1.3.1,<2.0a0
- license: blessing
- purls: []
- size: 945576
- timestamp: 1762299687230
-- conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda
- sha256: 04596fcee262a870e4b7c9807224680ff48d4d0cc0dac076a602503d3dc6d217
- md5: da5be73701eecd0e8454423fd6ffcf30
+ size: 321233
+ timestamp: 1765182495193
+- conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda
+ sha256: 54cdcd3214313b62c2a8ee277e6f42150d9b748264c1b70d958bf735e420ef8d
+ md5: 7dc38adcbf71e6b38748e919e16e0dce
depends:
- __glibc >=2.17,<3.0.a0
- - icu >=78.2,<79.0a0
- libgcc >=14
- - libzlib >=1.3.1,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
license: blessing
purls: []
- size: 942808
- timestamp: 1768147973361
-- conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.51.0-h8adb53f_0.conda
- sha256: b43d198f147f46866e5336c4a6b91668beef698bfba69d1706158460eadb2c1b
- md5: 5fb1945dbc6380e6fe7e939a62267772
+ size: 954962
+ timestamp: 1777986471789
+- conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
+ sha256: 49daec7c83e70d4efc17b813547824bc2bcf2f7256d84061d24fbfe537da9f74
+ md5: 6681822ea9d362953206352371b6a904
depends:
- __osx >=11.0
- - icu >=75.1,<76.0a0
- - libzlib >=1.3.1,<2.0a0
- license: blessing
- purls: []
- size: 909508
- timestamp: 1762300078624
-- conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.51.2-h1ae2325_0.conda
- sha256: 6e9b9f269732cbc4698c7984aa5b9682c168e2a8d1e0406e1ff10091ca046167
- md5: 4b0bf313c53c3e89692f020fb55d5f2c
- depends:
- - __osx >=11.0
- - icu >=78.2,<79.0a0
- - libzlib >=1.3.1,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
license: blessing
purls: []
- size: 909777
- timestamp: 1768148320535
+ size: 920047
+ timestamp: 1777987051643
- conda: https://prefix.dev/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661
md5: eecce068c7e4eddeb169591baac20ac4
@@ -3277,40 +3101,40 @@ packages:
purls: []
size: 279193
timestamp: 1745608793272
-- conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda
- sha256: 1b981647d9775e1cdeb2fab0a4dd9cd75a6b0de2963f6c3953dbd712f78334b3
- md5: 5b767048b1b3ee9a954b06f4084f93dc
+- conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
+ sha256: dff1058c76ec6b8759e41cefa2508162d00e4a5e6721aa68ec3fd10094e702dc
+ md5: 5794b3bdc38177caf969dabd3af08549
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc 15.2.0 h767d61c_7
+ - libgcc 15.2.0 he0feb66_19
constrains:
- - libstdcxx-ng ==15.2.0=*_7
+ - libstdcxx-ng ==15.2.0=*_19
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 3898269
- timestamp: 1759968103436
-- conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda
- sha256: 024fd46ac3ea8032a5ec3ea7b91c4c235701a8bf0e6520fe5e6539992a6bd05f
- md5: f627678cf829bd70bccf141a19c3ad3e
+ size: 5852044
+ timestamp: 1778269036376
+- conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ sha256: 0672b6b6e1791c92e8eccad58081a99d614fcf82bca5841f9dfa3c3e658f83b9
+ md5: e5ce228e579726c07255dbf90dc62101
depends:
- - libstdcxx 15.2.0 h8f9b012_7
+ - libstdcxx 15.2.0 h934c35e_19
license: GPL-3.0-only WITH GCC-exception-3.1
license_family: GPL
purls: []
- size: 29343
- timestamp: 1759968157195
-- conda: https://prefix.dev/conda-forge/linux-64/libsystemd0-257.10-hd0affe5_2.conda
- sha256: b30c06f60f03c2cf101afeb3452f48f12a2553b4cb631c9460c8a8ccf0813ae5
- md5: b04e0a2163a72588a40cde1afd6f2d18
+ size: 27776
+ timestamp: 1778269074600
+- conda: https://prefix.dev/conda-forge/linux-64/libsystemd0-257.13-hd0affe5_0.conda
+ sha256: c5008b602cb5c819f7b52d418b3ed17e1818cbbf6705b189e7ab36bb70cce3d8
+ md5: 8ee3cb7f64be0e8c4787f3a4dbe024e6
depends:
- __glibc >=2.17,<3.0.a0
- libcap >=2.77,<2.78.0a0
- libgcc >=14
license: LGPL-2.1-or-later
purls: []
- size: 491211
- timestamp: 1763011323224
+ size: 492799
+ timestamp: 1773797095649
- conda: https://prefix.dev/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
sha256: e5f8c38625aa6d567809733ae04bb71c161a42e44a9fa8227abe61fa5c60ebe0
md5: cd5a90476766d53e901500df9215e927
@@ -3346,17 +3170,17 @@ packages:
purls: []
size: 373892
timestamp: 1762022345545
-- conda: https://prefix.dev/conda-forge/linux-64/libudev1-257.10-hd0affe5_2.conda
- sha256: 751cf346f0f56cc9bfa43f7b5c9c30df2fcec8d84d164ac0cd74a27a3af79f30
- md5: 2f6b30acaa0d6e231d01166549108e2c
+- conda: https://prefix.dev/conda-forge/linux-64/libudev1-257.13-hd0affe5_0.conda
+ sha256: 1a1e367c04d66030aa93b4d33905f7f6fbb59cfc292e816fe3e9c1e8b3f4d1e2
+ md5: 2c2270f93d6f9073cbf72d821dfc7d72
depends:
- __glibc >=2.17,<3.0.a0
- libcap >=2.77,<2.78.0a0
- libgcc >=14
license: LGPL-2.1-or-later
purls: []
- size: 144395
- timestamp: 1763011330153
+ size: 145087
+ timestamp: 1773797108513
- conda: https://prefix.dev/conda-forge/linux-64/libunwind-1.8.3-h65a8314_0.conda
sha256: 71c8b9d5c72473752a0bb6e91b01dd209a03916cb71f36cc6a564e3a2a132d7a
md5: e179a69edd30d75c0144d7a380b88f28
@@ -3369,9 +3193,9 @@ packages:
purls: []
size: 75995
timestamp: 1757032240102
-- conda: https://prefix.dev/conda-forge/linux-64/liburing-2.12-hb700be7_0.conda
- sha256: 880b1f76b24814c9f07b33402e82fa66d5ae14738a35a943c21c4434eef2403d
- md5: f0531fc1ebc0902555670e9cb0127758
+- conda: https://prefix.dev/conda-forge/linux-64/liburing-2.14-hb700be7_0.conda
+ sha256: 3d17b7aa90610afc65356e9e6149aeac0b2df19deda73a51f0a09cf04fd89286
+ md5: 56f65185b520e016d29d01657ac02c0d
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
@@ -3379,8 +3203,8 @@ packages:
license: MIT
license_family: MIT
purls: []
- size: 127967
- timestamp: 1756125594973
+ size: 154203
+ timestamp: 1770566529700
- conda: https://prefix.dev/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda
sha256: 89c84f5b26028a9d0f5c4014330703e7dff73ba0c98f90103e9cef6b43a5323c
md5: d17e3fb595a9f24fa9e149239a33475d
@@ -3401,17 +3225,17 @@ packages:
purls: []
size: 83849
timestamp: 1748856224950
-- conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda
- sha256: e5ec6d2ad7eef538ddcb9ea62ad4346fde70a4736342c4ad87bd713641eb9808
- md5: 80c07c68d2f6870250959dcc95b209d1
+- conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
+ sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829
+ md5: 38ffe67b78c9d4de527be8315e5ada2c
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 37135
- timestamp: 1758626800002
+ size: 40297
+ timestamp: 1775052476770
- conda: https://prefix.dev/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda
sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b
md5: 0f03292cc56bf91a077a134ea8747118
@@ -3477,19 +3301,19 @@ packages:
purls: []
size: 199795
timestamp: 1770077125520
-- conda: https://prefix.dev/conda-forge/osx-arm64/libvulkan-loader-1.4.328.1-h49c215f_0.conda
- sha256: 7cdf4f61f38dad4765762d1e8f916c81e8221414911012f8aba294f5dce0e0ba
- md5: 978586f8c141eed794868a8f9834e3b0
+- conda: https://prefix.dev/conda-forge/osx-arm64/libvulkan-loader-1.4.341.0-h3feff0a_0.conda
+ sha256: d2790dafc9149b1acd45b9033d02cfa3f3e9ee5af97bd61e0a5718c414a0a135
+ md5: 6b4c9a5b130759136a0dde0c373cb0ea
depends:
- - libcxx >=19
- __osx >=11.0
+ - libcxx >=19
constrains:
- - libvulkan-headers 1.4.328.1.*
+ - libvulkan-headers 1.4.341.0.*
license: Apache-2.0
license_family: APACHE
purls: []
- size: 177829
- timestamp: 1759972150912
+ size: 180304
+ timestamp: 1770077143460
- conda: https://prefix.dev/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b
md5: aea31d2e5b1091feca96fcfe945c3cf9
@@ -3555,28 +3379,28 @@ packages:
purls: []
size: 837922
timestamp: 1764794163823
-- conda: https://prefix.dev/conda-forge/linux-64/libxml2-2.15.1-he237659_1.conda
- sha256: 047be059033c394bd32ae5de66ce389824352120b3a7c0eff980195f7ed80357
- md5: 417955234eccd8f252b86a265ccdab7f
+- conda: https://prefix.dev/conda-forge/linux-64/libxml2-2.15.1-h26afc86_0.conda
+ sha256: ec0735ae56c3549149eebd7dc22c0bed91fd50c02eaa77ff418613ddda190aa8
+ md5: e512be7dc1f84966d50959e900ca121f
depends:
- __glibc >=2.17,<3.0.a0
- - icu >=78.1,<79.0a0
+ - icu >=75.1,<76.0a0
- libgcc >=14
- libiconv >=1.18,<2.0a0
- liblzma >=5.8.1,<6.0a0
- - libxml2-16 2.15.1 hca6bf5a_1
+ - libxml2-16 2.15.1 ha9997c6_0
- libzlib >=1.3.1,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 45402
- timestamp: 1766327161688
-- conda: https://prefix.dev/conda-forge/linux-64/libxml2-16-2.15.1-hca6bf5a_1.conda
- sha256: 8331284bf9ae641b70cdc0e5866502dd80055fc3b9350979c74bb1d192e8e09e
- md5: 3fdd8d99683da9fe279c2f4cecd1e048
+ size: 45283
+ timestamp: 1761015644057
+- conda: https://prefix.dev/conda-forge/linux-64/libxml2-16-2.15.1-ha9997c6_0.conda
+ sha256: 71436e72a286ef8b57d6f4287626ff91991eb03c7bdbe835280521791efd1434
+ md5: e7733bc6785ec009e47a224a71917e84
depends:
- __glibc >=2.17,<3.0.a0
- - icu >=78.1,<79.0a0
+ - icu >=75.1,<76.0a0
- libgcc >=14
- libiconv >=1.18,<2.0a0
- liblzma >=5.8.1,<6.0a0
@@ -3586,14 +3410,14 @@ packages:
license: MIT
license_family: MIT
purls: []
- size: 555747
- timestamp: 1766327145986
-- conda: https://prefix.dev/conda-forge/osx-arm64/libxml2-16-2.15.1-h5ef1a60_1.conda
- sha256: 2d5ab15113b0ba21f4656d387d26ab59e4fbaf3027f5e58a2a4fe370821eb106
- md5: 7eed1026708e26ee512f43a04d9d0027
+ size: 556302
+ timestamp: 1761015637262
+- conda: https://prefix.dev/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda
+ sha256: ebe2dd9da94280ad43da936efa7127d329b559f510670772debc87602b49b06d
+ md5: 438c97d1e9648dd7342f86049dd44638
depends:
- __osx >=11.0
- - icu >=78.1,<79.0a0
+ - icu >=75.1,<76.0a0
- libiconv >=1.18,<2.0a0
- liblzma >=5.8.1,<6.0a0
- libzlib >=1.3.1,<2.0a0
@@ -3602,49 +3426,48 @@ packages:
license: MIT
license_family: MIT
purls: []
- size: 464886
- timestamp: 1766327479416
-- conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda
- sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4
- md5: edb0dca6bc32e4f4789199455a1dbeb8
+ size: 464952
+ timestamp: 1761016087733
+- conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda
+ sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9
+ md5: d87ff7921124eccd67248aa483c23fec
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
constrains:
- - zlib 1.3.1 *_2
+ - zlib 1.3.2 *_2
license: Zlib
license_family: Other
purls: []
- size: 60963
- timestamp: 1727963148474
-- conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda
- sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b
- md5: 369964e85dc26bfe78f41399b366c435
+ size: 63629
+ timestamp: 1774072609062
+- conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda
+ sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05
+ md5: bc5a5721b6439f2f62a84f2548136082
depends:
- __osx >=11.0
constrains:
- - zlib 1.3.1 *_2
+ - zlib 1.3.2 *_2
license: Zlib
license_family: Other
purls: []
- size: 46438
- timestamp: 1727963202283
-- conda: https://prefix.dev/conda-forge/osx-arm64/llvm-openmp-21.1.6-h4a912ad_0.conda
- sha256: 51ebeacae9225649e2c3bbfc9ed2ed690400b78ba79d0d3ee9ff428e8b951fed
- md5: 4a274d80967416bce3c7d89bf43923ec
+ size: 47759
+ timestamp: 1774072956767
+- conda: https://prefix.dev/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda
+ sha256: a269273ccf48be6ac582bb958713ba8373262b9157a0fc76b7e5475e8a1d2a78
+ md5: 46d04a647df7a4525e487d88068d19ef
depends:
- __osx >=11.0
constrains:
- - openmp 21.1.6|21.1.6.*
+ - openmp 22.1.4|22.1.4.*
- intel-openmp <0.0a0
license: Apache-2.0 WITH LLVM-exception
license_family: APACHE
purls: []
- size: 286206
- timestamp: 1763529774822
-- conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda
- sha256: 7b1da4b5c40385791dbc3cc85ceea9fad5da680a27d5d3cb8bfaa185e304a89e
- md5: 5b5203189eb668f042ac2b0826244964
+ size: 286406
+ timestamp: 1776846235007
+- conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
+ sha256: 0c4c35376fe920714390d46e4b8d31c876d65f18e1655899e0763ec25f2a902f
+ md5: 6d03368f2b2b0a5fb6839df53b2eb5e0
depends:
- mdurl >=0.1,<1
- python >=3.10
@@ -3652,8 +3475,8 @@ packages:
license_family: MIT
purls:
- pkg:pypi/markdown-it-py?source=hash-mapping
- size: 64736
- timestamp: 1754951288511
+ size: 69017
+ timestamp: 1778169663339
- conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda
sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7
md5: 592132998493b3ff25fd7479396e8351
@@ -3665,17 +3488,18 @@ packages:
- pkg:pypi/mdurl?source=hash-mapping
size: 14465
timestamp: 1733255681319
-- conda: https://prefix.dev/conda-forge/noarch/more-itertools-10.8.0-pyhd8ed1ab_0.conda
- sha256: fabe81c8f8f3e1d0ef227fc1306526c76189b3f1175f12302c707e0972dd707c
- md5: d7620a15dc400b448e1c88a981b23ddd
+- conda: https://prefix.dev/conda-forge/noarch/more-itertools-11.0.2-pyhcf101f3_0.conda
+ sha256: 74f7b461e0f0e0709a0c8abb018de9ad885258b74790ffda1e750ac5ddde0a85
+ md5: b874955758a30a37c78b82ea5cf78fdb
depends:
- python >=3.10
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/more-itertools?source=hash-mapping
- size: 65129
- timestamp: 1756855971031
+ size: 71254
+ timestamp: 1775762492525
- conda: https://prefix.dev/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda
sha256: 39c4700fb3fbe403a77d8cc27352fa72ba744db487559d5d44bf8411bb4ea200
md5: c7f302fd11eeb0987a6a5e1f3aed6a21
@@ -3699,37 +3523,37 @@ packages:
purls: []
size: 360712
timestamp: 1730581491116
-- conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda
- sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586
- md5: 47e340acb35de30501a76c7c799c41d7
+- conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
+ sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3
+ md5: fc21868a1a5aacc937e7a18747acb8a5
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
license: X11 AND BSD-3-Clause
purls: []
- size: 891641
- timestamp: 1738195959188
-- conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda
- sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733
- md5: 068d497125e4bf8a66bf707254fff5ae
+ size: 918956
+ timestamp: 1777422145199
+- conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
+ sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d
+ md5: 343d10ed5b44030a2f67193905aea159
depends:
- __osx >=11.0
license: X11 AND BSD-3-Clause
purls: []
- size: 797030
- timestamp: 1738196177597
-- conda: https://prefix.dev/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
- sha256: 3636eec0e60466a00069b47ce94b6d88b01419b6577d8e393da44bb5bc8d3468
- md5: 7ba3f09fceae6a120d664217e58fe686
+ size: 805509
+ timestamp: 1777423252320
+- conda: https://prefix.dev/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda
+ sha256: 4fa40e3e13fc6ea0a93f67dfc76c96190afd7ea4ffc1bac2612d954b42cdc3ee
+ md5: eb52d14a901e23c39e9e7b4a1a5c015f
depends:
- - python >=3.9
+ - python >=3.10
- setuptools
license: BSD-3-Clause
license_family: BSD
purls:
- pkg:pypi/nodeenv?source=hash-mapping
- size: 34574
- timestamp: 1734112236147
+ size: 40866
+ timestamp: 1766261270149
- pypi: https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl
name: numpy
version: 2.0.2
@@ -3780,9 +3604,9 @@ packages:
- pkg:pypi/numpy?source=hash-mapping
size: 6893464
timestamp: 1732314808608
-- conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda
- sha256: a47271202f4518a484956968335b2521409c8173e123ab381e775c358c67fe6d
- md5: 9ee58d5c534af06558933af3c845a780
+- conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda
+ sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb
+ md5: da1b85b6a87e141f5140bb9924cecab0
depends:
- __glibc >=2.17,<3.0.a0
- ca-certificates
@@ -3790,19 +3614,19 @@ packages:
license: Apache-2.0
license_family: Apache
purls: []
- size: 3165399
- timestamp: 1762839186699
-- conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda
- sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988
- md5: b34dc4172653c13dcf453862f251af2b
+ size: 3167099
+ timestamp: 1775587756857
+- conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda
+ sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea
+ md5: 25dcccd4f80f1638428613e0d7c9b4e1
depends:
- __osx >=11.0
- ca-certificates
license: Apache-2.0
license_family: Apache
purls: []
- size: 3108371
- timestamp: 1762839712322
+ size: 3106008
+ timestamp: 1775587972483
- conda: https://prefix.dev/conda-forge/linux-64/opusfile-0.12-h3358134_2.conda
sha256: f4df9df880e405e5c856383f869d5b9d434f78fb7c234c9e7b099ab604fb7fc3
md5: 5931bcae00b98f952696b6bcdd0be34b
@@ -3828,9 +3652,9 @@ packages:
purls: []
size: 80128
timestamp: 1670387790769
-- conda: https://prefix.dev/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda
- sha256: 289861ed0c13a15d7bbb408796af4de72c2fe67e2bcb0de98f4c3fce259d7991
- md5: 58335b26c38bf4a20f399384c33cbcf9
+- conda: https://prefix.dev/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
+ sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890
+ md5: 4c06a92e74452cfa53623a81592e8934
depends:
- python >=3.8
- python
@@ -3838,8 +3662,8 @@ packages:
license_family: APACHE
purls:
- pkg:pypi/packaging?source=hash-mapping
- size: 62477
- timestamp: 1745345660407
+ size: 91574
+ timestamp: 1777103621679
- conda: https://prefix.dev/conda-forge/linux-64/pandas-3.0.1-py311h8032f78_0.conda
sha256: 3e58067de2e0b8220cf1323c1fc736028b2a117b7a7173bfe67f7da11094271f
md5: 3ac73c673df24589b5b84d43a4afd235
@@ -3894,7 +3718,7 @@ packages:
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/pandas?source=compressed-mapping
+ - pkg:pypi/pandas?source=hash-mapping
size: 15143263
timestamp: 1771408984246
- conda: https://prefix.dev/conda-forge/osx-arm64/pandas-3.0.1-py311h4a068b9_0.conda
@@ -3951,7 +3775,7 @@ packages:
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/pandas?source=compressed-mapping
+ - pkg:pypi/pandas?source=hash-mapping
size: 14295353
timestamp: 1771409101672
- conda: https://prefix.dev/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda
@@ -3995,17 +3819,17 @@ packages:
purls: []
size: 426931
timestamp: 1751292636271
-- conda: https://prefix.dev/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
- sha256: 9f64009cdf5b8e529995f18e03665b03f5d07c0b17445b8badef45bde76249ee
- md5: 617f15191456cc6a13db418a275435e5
+- conda: https://prefix.dev/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda
+ sha256: 6eaee417d33f298db79bc7185ab1208604c0e6cf51dade34cd513c6f9db9c6f3
+ md5: 11adc78451c998c0fd162584abfa3559
depends:
- - python >=3.9
+ - python >=3.10
license: MPL-2.0
license_family: MOZILLA
purls:
- pkg:pypi/pathspec?source=hash-mapping
- size: 41075
- timestamp: 1733233471940
+ size: 56559
+ timestamp: 1777271601895
- conda: https://prefix.dev/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda
sha256: 5e6f7d161356fefd981948bea5139c5aa0436767751a6930cb1ca801ebb113ff
md5: 7a3bff861a6583f1889021facefc08b1
@@ -4066,9 +3890,9 @@ packages:
purls: []
size: 248045
timestamp: 1754665282033
-- conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.5.0-pyhcf101f3_0.conda
- sha256: 7efd51b48d908de2d75cbb3c4a2e80dd9454e1c5bb8191b261af3136f7fa5888
- md5: 5c7a868f8241e64e1cf5fdf4962f23e2
+- conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda
+ sha256: 8f29915c172f1f7f4f7c9391cd5dac3ebf5d13745c8b7c8006032615246345a5
+ md5: 89c0b6d1793601a2a3a3f7d2d3d8b937
depends:
- python >=3.10
- python
@@ -4076,44 +3900,47 @@ packages:
license_family: MIT
purls:
- pkg:pypi/platformdirs?source=hash-mapping
- size: 23625
- timestamp: 1759953252315
-- conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda
- sha256: a8eb555eef5063bbb7ba06a379fa7ea714f57d9741fe0efdb9442dbbc2cccbcc
- md5: 7da7ccd349dbf6487a7778579d2bb971
+ size: 25862
+ timestamp: 1775741140609
+- conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
+ sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e
+ md5: d7585b6550ad04c8c5e21097ada2888e
depends:
- python >=3.9
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/pluggy?source=hash-mapping
- size: 24246
- timestamp: 1747339794916
-- conda: https://prefix.dev/conda-forge/linux-64/portaudio-19.6.0-h7c63dc7_9.conda
- sha256: c09ae032d0303abfea34c0957834538b48133b0431283852741ed3e0f66fdb36
- md5: 893f2c33af6b03cfd04820a8c31f5798
+ size: 25877
+ timestamp: 1764896838868
+- conda: https://prefix.dev/conda-forge/linux-64/portaudio-19.7.0-hf4617a5_0.conda
+ sha256: 3259d2bf63d0c889c516511c8fa73214791ca30baeeee0962eee8b97d17cd1c6
+ md5: 053455c094c711e9aa77cf5023cf2bc3
depends:
- - alsa-lib >=1.2.10,<1.3.0.0a0
- - libgcc-ng >=12
- - libstdcxx-ng >=12
+ - __glibc >=2.17,<3.0.a0
+ - alsa-lib >=1.2.12,<1.3.0a0
+ - libgcc >=13
+ - libstdcxx >=13
license: MIT
license_family: MIT
purls: []
- size: 115512
- timestamp: 1693868383
-- conda: https://prefix.dev/conda-forge/osx-arm64/portaudio-19.6.0-h13dd4ca_9.conda
- sha256: 5ff2b55d685c29dfe632ef856796a4b862305088543d4982f0b807e8d9bb756e
- md5: d325d46394b6c46d15718c855fb20b4a
+ size: 77342
+ timestamp: 1730364040048
+- conda: https://prefix.dev/conda-forge/osx-arm64/portaudio-19.7.0-h5833ebf_0.conda
+ sha256: 3d86803ade1ff3fd29ea1b61ce2e5ad65af0966d795ab8a7a2685fee75cf0f82
+ md5: fb72bb70cfea298990d5e1a12317047c
depends:
- - libcxx >=15.0.7
+ - __osx >=11.0
+ - libcxx >=17
license: MIT
license_family: MIT
purls: []
- size: 78863
- timestamp: 1693868663440
-- conda: https://prefix.dev/conda-forge/linux-64/portmidi-2.0.7-h3cb78e3_0.conda
- sha256: 7139889de1da0e5990c9baface279c62c6be71bfb650897ecc92b9fa84aaa482
- md5: 378b689c31af1260dbf6e5e5eb02166c
+ size: 58138
+ timestamp: 1730364270871
+- conda: https://prefix.dev/conda-forge/linux-64/portmidi-2.0.8-h3cb78e3_0.conda
+ sha256: 13b1f300f609d386d7aec2e31c4ab2fd35c0ed6a2b28ec89605db5b4b044514b
+ md5: 2333d6737b6fd5c6a70977f8ec1bf110
depends:
- __glibc >=2.17,<3.0.a0
- alsa-lib >=1.2.14,<1.3.0a0
@@ -4122,22 +3949,22 @@ packages:
license: MIT
license_family: MIT
purls: []
- size: 44047
- timestamp: 1761471639423
-- conda: https://prefix.dev/conda-forge/osx-arm64/portmidi-2.0.7-haf25636_0.conda
- sha256: d74c61e5de5d6a67484b22fc2d005c97668e400626be07e576580cf461b3c9d9
- md5: effa51f505671206ba4b86acce7946ff
+ size: 44178
+ timestamp: 1764706159843
+- conda: https://prefix.dev/conda-forge/osx-arm64/portmidi-2.0.8-haf25636_0.conda
+ sha256: 2a1fb2c2507a3b88ac68c4dac435a836be365446e66e995749d2188456fb4262
+ md5: 2544b03efb37c9476f3ab4e993c224d1
depends:
- __osx >=11.0
- libcxx >=19
license: MIT
license_family: MIT
purls: []
- size: 41359
- timestamp: 1761471916499
-- conda: https://prefix.dev/conda-forge/noarch/pre-commit-4.4.0-pyha770c72_0.conda
- sha256: 5eee94ab06bb74ad164862d15c18e932493c5b959928e7889b91dee73f550152
- md5: c130573bb7a166e93d5bd01c94ae38e1
+ size: 41391
+ timestamp: 1764706738148
+- conda: https://prefix.dev/conda-forge/noarch/pre-commit-4.6.0-pyha770c72_0.conda
+ sha256: 716960bf0a9eb334458a26b3bdcb17b8d0786062138a4f48c7f335c8418c5d0b
+ md5: 7859736b4f8ebe6c8481bf48d91c9a1e
depends:
- cfgv >=2.0.0
- identify >=1.0.0
@@ -4149,8 +3976,8 @@ packages:
license_family: MIT
purls:
- pkg:pypi/pre-commit?source=hash-mapping
- size: 200293
- timestamp: 1762692428418
+ size: 201606
+ timestamp: 1776858157327
- conda: https://prefix.dev/conda-forge/linux-64/protobuf-3.20.2-py311ha362b79_1.conda
sha256: b5927c5e5d3d2bee29e87cfee3e45781e6f6fd663e890389726cd513ef746177
md5: 9d64275197aab7121cd9318b593a21bd
@@ -4325,17 +4152,17 @@ packages:
- pkg:pypi/pygame?source=hash-mapping
size: 2917534
timestamp: 1727636756068
-- conda: https://prefix.dev/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda
- sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a
- md5: 6b6ece66ebcae2d5f326c77ef2c5a066
+- conda: https://prefix.dev/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
+ sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86
+ md5: 16c18772b340887160c79a6acc022db0
depends:
- - python >=3.9
+ - python >=3.10
license: BSD-2-Clause
license_family: BSD
purls:
- pkg:pypi/pygments?source=hash-mapping
- size: 889287
- timestamp: 1750615908735
+ size: 893031
+ timestamp: 1774796815820
- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl
name: pyparsing
version: 3.3.2
@@ -4344,17 +4171,30 @@ packages:
- railroad-diagrams ; extra == 'diagrams'
- jinja2 ; extra == 'diagrams'
requires_python: '>=3.9'
-- conda: https://prefix.dev/conda-forge/noarch/pyserial-3.5-pyhd8ed1ab_1.conda
- sha256: 48b51797fa1f5d8b2ed5d5f155a18993bb02616eef7a3b053a9125d7650bbe59
- md5: 0127bf00a534e0c7b8075090f162bf82
+- conda: https://prefix.dev/conda-forge/noarch/pyproject_hooks-1.2.0-pyhd8ed1ab_1.conda
+ sha256: 065ac44591da9abf1ff740feb25929554b920b00d09287a551fcced2c9791092
+ md5: d4582021af437c931d7d77ec39007845
depends:
- python >=3.9
+ - tomli >=1.1.0
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/pyproject-hooks?source=hash-mapping
+ size: 15528
+ timestamp: 1733710122949
+- conda: https://prefix.dev/conda-forge/noarch/pyserial-3.5-pyhcf101f3_2.conda
+ sha256: 8c618e8ca376d73133c9971abe45463a48c9cfb529788d609fd19568764941ba
+ md5: b27a6cd32160c34682ce6053b04c12c6
+ depends:
+ - python >=3.10
+ - python
license: BSD-3-Clause
license_family: BSD
purls:
- pkg:pypi/pyserial?source=hash-mapping
- size: 69380
- timestamp: 1734140304034
+ size: 73910
+ timestamp: 1767289527295
- conda: https://prefix.dev/conda-forge/noarch/pytest-8.3.4-pyhd8ed1ab_1.conda
sha256: 75245ca9d0cbd6d38bb45ec02430189a9d4c21c055c5259739d738a2298d61b3
md5: 799ed216dc6af62520f32aa39bc1c2bb
@@ -4374,108 +4214,104 @@ packages:
- pkg:pypi/pytest?source=hash-mapping
size: 259195
timestamp: 1733217599806
-- conda: https://prefix.dev/conda-forge/linux-64/python-3.10.19-h3c07f61_2_cpython.conda
- build_number: 2
- sha256: 6e3b6b69b3cacfc7610155d58407a003820eaacd50fbe039abff52b5e70b1e9b
- md5: 27ac896a8b4970f8977503a9e70dc745
+- conda: https://prefix.dev/conda-forge/linux-64/python-3.10.20-h3c07f61_0_cpython.conda
+ sha256: 8ff2ce308faf2588b69c65b120293f59a8f2577b772b34df4e817d220b09e081
+ md5: 5d4e2b00d99feacd026859b7fa239dc0
depends:
- __glibc >=2.17,<3.0.a0
- bzip2 >=1.0.8,<2.0a0
- ld_impl_linux-64 >=2.36.1
- - libexpat >=2.7.1,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
- libffi >=3.4,<4.0a0
- libgcc >=14
- - liblzma >=5.8.1,<6.0a0
+ - liblzma >=5.8.2,<6.0a0
- libnsl >=2.0.1,<2.1.0a0
- - libsqlite >=3.50.4,<4.0a0
- - libuuid >=2.41.2,<3.0a0
+ - libsqlite >=3.51.2,<4.0a0
+ - libuuid >=2.41.3,<3.0a0
- libxcrypt >=4.4.36
- libzlib >=1.3.1,<2.0a0
- ncurses >=6.5,<7.0a0
- - openssl >=3.5.4,<4.0a0
- - readline >=8.2,<9.0a0
+ - openssl >=3.5.5,<4.0a0
+ - readline >=8.3,<9.0a0
- tk >=8.6.13,<8.7.0a0
- tzdata
constrains:
- python_abi 3.10.* *_cp310
license: Python-2.0
purls: []
- size: 25311690
- timestamp: 1761173015969
-- conda: https://prefix.dev/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda
- build_number: 2
- sha256: 5b872f7747891e50e990a96d2b235236a5c66cc9f8c9dcb7149aee674ea8145a
- md5: c4202a55b4486314fbb8c11bc43a29a0
+ size: 25455342
+ timestamp: 1772729810280
+- conda: https://prefix.dev/conda-forge/linux-64/python-3.11.15-hd63d673_0_cpython.conda
+ sha256: bf6a32c69889d38482436a786bea32276756cedf0e9805cc856ffd088e8d00f0
+ md5: a5ebcefec0c12a333bcd6d7bf3bddc1f
depends:
- __glibc >=2.17,<3.0.a0
- bzip2 >=1.0.8,<2.0a0
- ld_impl_linux-64 >=2.36.1
- - libexpat >=2.7.1,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
- libffi >=3.5.2,<3.6.0a0
- libgcc >=14
- - liblzma >=5.8.1,<6.0a0
+ - liblzma >=5.8.2,<6.0a0
- libnsl >=2.0.1,<2.1.0a0
- - libsqlite >=3.50.4,<4.0a0
- - libuuid >=2.41.2,<3.0a0
+ - libsqlite >=3.51.2,<4.0a0
+ - libuuid >=2.41.3,<3.0a0
- libxcrypt >=4.4.36
- libzlib >=1.3.1,<2.0a0
- ncurses >=6.5,<7.0a0
- - openssl >=3.5.4,<4.0a0
- - readline >=8.2,<9.0a0
+ - openssl >=3.5.5,<4.0a0
+ - readline >=8.3,<9.0a0
- tk >=8.6.13,<8.7.0a0
- tzdata
constrains:
- python_abi 3.11.* *_cp311
license: Python-2.0
purls: []
- size: 30874708
- timestamp: 1761174520369
-- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.10.19-hcd7f573_2_cpython.conda
- build_number: 2
- sha256: 7bac6cc075d1d7897f06fa14c1bc87eb16b9524c6002e0c72b0ed3326af51695
- md5: feb559b139819a7326f992711cb50872
+ size: 30949404
+ timestamp: 1772730362552
+- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.10.20-h1b19095_0_cpython.conda
+ sha256: f0f6fcbb6cfdee5a6b9c03b5b94d2bbe737f3b17a618006c7685cc48992ae667
+ md5: 55ec25b0d09379eb11c32dbe09ee28c4
depends:
- __osx >=11.0
- bzip2 >=1.0.8,<2.0a0
- - libexpat >=2.7.1,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
- libffi >=3.4,<4.0a0
- - liblzma >=5.8.1,<6.0a0
- - libsqlite >=3.50.4,<4.0a0
+ - liblzma >=5.8.2,<6.0a0
+ - libsqlite >=3.51.2,<4.0a0
- libzlib >=1.3.1,<2.0a0
- ncurses >=6.5,<7.0a0
- - openssl >=3.5.4,<4.0a0
- - readline >=8.2,<9.0a0
+ - openssl >=3.5.5,<4.0a0
+ - readline >=8.3,<9.0a0
- tk >=8.6.13,<8.7.0a0
- tzdata
constrains:
- python_abi 3.10.* *_cp310
license: Python-2.0
purls: []
- size: 11674631
- timestamp: 1761173465015
-- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda
- build_number: 2
- sha256: 64a2bc6be8582fae75f1f2da7bdc49afd81c2793f65bb843fc37f53c99734063
- md5: da948e6cd735249ab4cfbb3fdede785e
+ size: 12468674
+ timestamp: 1772730636766
+- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.11.15-h8561d8f_0_cpython.conda
+ sha256: 9a846065863925b2562126a5c6fecd7a972e84aaa4de9e686ad3715ca506acfa
+ md5: 49c7d96c58b969585cf09fb01d74e08e
depends:
- __osx >=11.0
- bzip2 >=1.0.8,<2.0a0
- - libexpat >=2.7.1,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
- libffi >=3.5.2,<3.6.0a0
- - liblzma >=5.8.1,<6.0a0
- - libsqlite >=3.50.4,<4.0a0
+ - liblzma >=5.8.2,<6.0a0
+ - libsqlite >=3.51.2,<4.0a0
- libzlib >=1.3.1,<2.0a0
- ncurses >=6.5,<7.0a0
- - openssl >=3.5.4,<4.0a0
- - readline >=8.2,<9.0a0
+ - openssl >=3.5.5,<4.0a0
+ - readline >=8.3,<9.0a0
- tk >=8.6.13,<8.7.0a0
- tzdata
constrains:
- python_abi 3.11.* *_cp311
license: Python-2.0
purls: []
- size: 14788204
- timestamp: 1761174033541
+ size: 14753109
+ timestamp: 1772730203101
- conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda
sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664
md5: 5b8d21249ff20967101ffa321cab24e8
@@ -4489,6 +4325,20 @@ packages:
- pkg:pypi/python-dateutil?source=hash-mapping
size: 233310
timestamp: 1751104122689
+- conda: https://prefix.dev/conda-forge/noarch/python-discovery-1.3.0-pyhcf101f3_0.conda
+ sha256: ae70eb1c16970f2317e71dd2dee7d3a41abd26a47298ca8d9163a94b6579517b
+ md5: 696db6f25e56c0fafdccb0a7426fffb6
+ depends:
+ - python >=3.10
+ - filelock >=3.15.4
+ - platformdirs <5,>=4.3.6
+ - python
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/python-discovery?source=hash-mapping
+ size: 35030
+ timestamp: 1778013338579
- conda: https://prefix.dev/conda-forge/noarch/python_abi-3.10-8_cp310.conda
build_number: 8
sha256: 7ad76fa396e4bde336872350124c0819032a9e8a0a40590744ff9527b54351c1
@@ -4511,9 +4361,9 @@ packages:
purls: []
size: 7003
timestamp: 1752805919375
-- conda: https://prefix.dev/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_0.conda
- sha256: 7dc5c27c0c23474a879ef5898ed80095d26de7f89f4720855603c324cca19355
- md5: 707c3d23f2476d3bfde8345b4e7d7853
+- conda: https://prefix.dev/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda
+ sha256: c9a6cd2c290d7c3d2b30ea34a0ccda30f770e8ddb2937871f2c404faf60d0050
+ md5: a24add9a3bababee946f3bc1c829acfe
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
@@ -4524,11 +4374,11 @@ packages:
license_family: MIT
purls:
- pkg:pypi/pyyaml?source=hash-mapping
- size: 211606
- timestamp: 1758892088237
-- conda: https://prefix.dev/conda-forge/osx-arm64/pyyaml-6.0.3-py311ha9b3269_0.conda
- sha256: 747c1b94222481a727aeeb912407f862a93a1bb4e704be3a8236768182ac0290
- md5: 109a9c326951cc9ab5df6a06cf5b930a
+ size: 206190
+ timestamp: 1770223702917
+- conda: https://prefix.dev/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda
+ sha256: 984e73d7957460689e10533059de8adb38a308853d298900a37acc58edd84cec
+ md5: e4b908da7cd496b3fa6798c0f60a2a19
depends:
- __osx >=11.0
- python >=3.11,<3.12.0a0
@@ -4539,24 +4389,24 @@ packages:
license_family: MIT
purls:
- pkg:pypi/pyyaml?source=hash-mapping
- size: 195537
- timestamp: 1758892104856
-- conda: https://prefix.dev/conda-forge/linux-64/rav1e-0.7.1-h8fae777_3.conda
- sha256: 6e5e704c1c21f820d760e56082b276deaf2b53cf9b751772761c3088a365f6f4
- md5: 2c42649888aac645608191ffdc80d13a
+ size: 192948
+ timestamp: 1770223655988
+- conda: https://prefix.dev/conda-forge/linux-64/rav1e-0.8.1-h1fbca29_0.conda
+ sha256: cf550bbc8e5ebedb6dba9ccaead3e07bd1cb86b183644a4c853e06e4b3ad5ac7
+ md5: d83958768626b3c8471ce032e28afcd3
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
constrains:
- __glibc >=2.17
license: BSD-2-Clause
license_family: BSD
purls: []
- size: 5176669
- timestamp: 1746622023242
-- conda: https://prefix.dev/conda-forge/osx-arm64/rav1e-0.7.1-h0716509_3.conda
- sha256: 65f862b2b31ef2b557990a82015cbd41e5a66041c2f79b4451dd14b4595d4c04
- md5: 7b37f30516100b86ea522350c8cab44c
+ size: 5595970
+ timestamp: 1772540833621
+- conda: https://prefix.dev/conda-forge/osx-arm64/rav1e-0.8.1-h8246384_0.conda
+ sha256: 925e35b71fe513e0380ecd2fe137e3f4f248bf7ce4bad96946c7c704b7a50d26
+ md5: 4706a8a71474c692482c3f86c2175454
depends:
- __osx >=11.0
constrains:
@@ -4564,8 +4414,8 @@ packages:
license: BSD-2-Clause
license_family: BSD
purls: []
- size: 856271
- timestamp: 1746622200646
+ size: 886953
+ timestamp: 1772541394570
- pypi: https://files.pythonhosted.org/packages/d3/90/5c5960e30acd13eb43a780023761218612d26dd540d90b15edfef730b180/rc_robosim-1.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
name: rc-robosim
version: '1.2'
@@ -4574,27 +4424,29 @@ packages:
name: rc-robosim
version: '1.2'
sha256: 5080167ece949ad83b6e59fa96420ad0a1b5b8abd8c602776b70fa6c127961e7
-- conda: https://prefix.dev/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda
- sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c
- md5: 283b96675859b20a825f8fa30f311446
+- conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda
+ sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002
+ md5: d7d95fc8287ea7bf33e0e7116d2b95ec
depends:
- - libgcc >=13
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
- ncurses >=6.5,<7.0a0
license: GPL-3.0-only
license_family: GPL
purls: []
- size: 282480
- timestamp: 1740379431762
-- conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda
- sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34
- md5: 63ef3f6e6d6d5c589e64f11263dc5676
+ size: 345073
+ timestamp: 1765813471974
+- conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda
+ sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477
+ md5: f8381319127120ce51e081dce4865cf4
depends:
+ - __osx >=11.0
- ncurses >=6.5,<7.0a0
license: GPL-3.0-only
license_family: GPL
purls: []
- size: 252359
- timestamp: 1740379663071
+ size: 313930
+ timestamp: 1765813902568
- conda: https://prefix.dev/conda-forge/linux-64/rhash-1.4.3-hd590300_2.conda
sha256: 475f68cac8981ff2b10c56e53c2f376fc3c805fbc7ec30d22f870cd88f1479ba
md5: 4cabe3858a856bff08d9a0992e413084
@@ -4613,9 +4465,9 @@ packages:
purls: []
size: 176444
timestamp: 1693427792263
-- conda: https://prefix.dev/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda
- sha256: edfb44d0b6468a8dfced728534c755101f06f1a9870a7ad329ec51389f16b086
- md5: a247579d8a59931091b16a1e932bbed6
+- conda: https://prefix.dev/conda-forge/noarch/rich-14.3.4-pyhcf101f3_0.conda
+ sha256: a941b5e002504e1c9b02ea02180d40186b67d03e8e7d668bf746393c266b3951
+ md5: 2e6a922d07244f514863b7e10bf5c923
depends:
- markdown-it-py >=2.2.0
- pygments >=2.13.0,<3.0.0
@@ -4625,29 +4477,29 @@ packages:
license: MIT
license_family: MIT
purls:
- - pkg:pypi/rich?source=compressed-mapping
- size: 200840
- timestamp: 1760026188268
-- conda: https://prefix.dev/conda-forge/linux-64/ruff-0.14.5-h813ae00_0.conda
+ - pkg:pypi/rich?source=hash-mapping
+ size: 208480
+ timestamp: 1775880677603
+- conda: https://prefix.dev/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda
noarch: python
- sha256: 3e425603d7ad058ba9ec21cc482328e5ccb776a458d3e92ee112f2f65631678a
- md5: a4590632bd44d39c8f4f64a1c85ebe49
+ sha256: 98c771464ed93a2733bae460c39cfa9640feb20972d2e651b44e85db296942eb
+ md5: 3c8f229055ad244fa3a97c6c45b77099
depends:
- python
- - __glibc >=2.17,<3.0.a0
- libgcc >=14
+ - __glibc >=2.17,<3.0.a0
constrains:
- __glibc >=2.17
license: MIT
license_family: MIT
purls:
- pkg:pypi/ruff?source=hash-mapping
- size: 11098073
- timestamp: 1763078242921
-- conda: https://prefix.dev/conda-forge/osx-arm64/ruff-0.14.5-h382de68_0.conda
+ size: 9266480
+ timestamp: 1778119386343
+- conda: https://prefix.dev/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda
noarch: python
- sha256: c802549bba4a74e491c1f0c8d1dcf88b1adc151a39d698884734353cca1a4005
- md5: dfb6a5485cf34697a9245832f3f90c09
+ sha256: 91992a557456cbe9980e8ffb0f0d1200f65f741adeefc7ecf568d1935690a7bc
+ md5: 7151a0851207b88c0031f388a7825e9b
depends:
- python
- __osx >=11.0
@@ -4656,12 +4508,12 @@ packages:
license: MIT
license_family: MIT
purls:
- - pkg:pypi/ruff?source=compressed-mapping
- size: 10121479
- timestamp: 1763078323983
-- conda: https://prefix.dev/conda-forge/linux-64/scipy-1.17.0-py311hbe70eeb_1.conda
- sha256: b9582e96d703b2f2f61efc7394c886aefa5ab44983818bfc4a1894afc099561c
- md5: f4dda6316cc4718cbcab7009b5d60c41
+ - pkg:pypi/ruff?source=hash-mapping
+ size: 8485007
+ timestamp: 1778119519471
+- conda: https://prefix.dev/conda-forge/linux-64/scipy-1.17.1-py311hbe70eeb_0.conda
+ sha256: 0063a75e9406357ef8ecc2fb38808a2442b269bfc5a854cf2bcd368060d9a2d7
+ md5: 5ae6d73ab0bebbc892c2d46dc51e90a5
depends:
- __glibc >=2.17,<3.0.a0
- libblas >=3.9.0,<4.0a0
@@ -4679,12 +4531,12 @@ packages:
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/scipy?source=compressed-mapping
- size: 16967163
- timestamp: 1768800888207
-- conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.17.0-py311he9931d0_1.conda
- sha256: d9f37c85cbf689be3672c8264eb81585ad8f6041a2fe545ec978f42e5da0202c
- md5: 9c5c9dbdaf090ba8be3beb34c01495d0
+ - pkg:pypi/scipy?source=hash-mapping
+ size: 17099824
+ timestamp: 1771880736635
+- conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.17.1-py311he9931d0_0.conda
+ sha256: f212138a7eb86cf8bcdb46f003ae552967cd7e48d2c539f7f8dd6e800c74b70d
+ md5: 40606bef4834281d5f746626338829dc
depends:
- __osx >=11.0
- libblas >=3.9.0,<4.0a0
@@ -4702,9 +4554,9 @@ packages:
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/scipy?source=compressed-mapping
- size: 14030449
- timestamp: 1768801949072
+ - pkg:pypi/scipy?source=hash-mapping
+ size: 14107248
+ timestamp: 1771880859933
- conda: https://prefix.dev/conda-forge/linux-64/sdl2-2.32.56-h54a6638_0.conda
sha256: 987ad072939fdd51c92ea8d3544b286bb240aefda329f9b03a51d9b7e777f9de
md5: cdd138897d94dc07d99afe7113a07bec
@@ -4764,109 +4616,112 @@ packages:
purls: []
size: 118041
timestamp: 1716857215118
-- conda: https://prefix.dev/conda-forge/linux-64/sdl2_mixer-2.6.3-h8830914_1.conda
- sha256: c3e99e222b091f26cfd1d6be22c5a2973df9e7caa020262f9d9523f340344a95
- md5: 1a2b60be4d860a0c419a87176c85c3ad
+- conda: https://prefix.dev/conda-forge/linux-64/sdl2_mixer-2.6.3-hb8bcbd3_2.conda
+ sha256: 3a36f7d6349d132c571516514dc8cd771a705e93adef942c8eee38a09875d97a
+ md5: 97fbc8b1372f0000321d568c0e459e3c
depends:
- - fluidsynth >=2.3.4,<2.4.0a0
- - libflac >=1.4.3,<1.5.0a0
- - libgcc-ng >=12
+ - __glibc >=2.17,<3.0.a0
+ - fluidsynth >=2.5.2,<2.6.0a0
+ - libflac >=1.5.0,<1.6.0a0
+ - libgcc >=14
- libmad >=0.15.1b,<0.16.0a0
- - libogg >=1.3.4,<1.4.0a0
- - libstdcxx-ng >=12
+ - libogg >=1.3.5,<1.4.0a0
+ - libstdcxx >=14
- libvorbis >=1.3.7,<1.4.0a0
- - mpg123 >=1.32.1,<1.33.0a0
+ - mpg123 >=1.32.9,<1.33.0a0
- opusfile >=0.12,<0.13.0a0
- - sdl2 >=2.28.3,<3.0a0
+ - sdl2 >=2.32.56,<3.0a0
license: Zlib
purls: []
- size: 202966
- timestamp: 1695761744535
-- conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_mixer-2.6.3-h4fe3bdc_1.conda
- sha256: fcaaf1b589aed11498f5b9735b996fb92ff18e4673a4c804bbfa28eb00264e06
- md5: b92222911d46f08faa583df51191bd7f
+ size: 205164
+ timestamp: 1766614707552
+- conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_mixer-2.6.3-h45fadf6_2.conda
+ sha256: b826d7fa6738d9fcc36bdcb299744246fe82d0f9d9d0d0f999f3111cb7b4d373
+ md5: 890924faa4965ba3edb0cc5cc94db572
depends:
- - fluidsynth >=2.3.4,<2.4.0a0
- - libcxx >=15.0.7
- - libflac >=1.4.3,<1.5.0a0
+ - __osx >=11.0
+ - fluidsynth >=2.5.2,<2.6.0a0
+ - libcxx >=19
+ - libflac >=1.5.0,<1.6.0a0
- libmad >=0.15.1b,<0.16.0a0
- - libogg >=1.3.4,<1.4.0a0
+ - libogg >=1.3.5,<1.4.0a0
- libvorbis >=1.3.7,<1.4.0a0
- - mpg123 >=1.32.1,<1.33.0a0
+ - mpg123 >=1.32.9,<1.33.0a0
- opusfile >=0.12,<0.13.0a0
- - sdl2 >=2.28.3,<3.0a0
+ - sdl2 >=2.32.56,<3.0a0
license: Zlib
purls: []
- size: 188504
- timestamp: 1695762176058
-- conda: https://prefix.dev/conda-forge/linux-64/sdl2_ttf-2.24.0-h287479f_0.conda
- sha256: 431d19b666db6e7a4f09c37c43c83f115176a006b2ac321853ca26bee888c519
- md5: bccd5b74eb55a523dfcc66b857555714
+ size: 187456
+ timestamp: 1766614943278
+- conda: https://prefix.dev/conda-forge/linux-64/sdl2_ttf-2.24.0-hc1d668e_1.conda
+ sha256: 5b72b99576455ba53cc294c8aa1ab389d6302a1072c90f76268257e70343fcfe
+ md5: 0385816e000e885b420cdcfc1b7de3a3
depends:
- __glibc >=2.17,<3.0.a0
- - freetype >=2.12.1,<3.0a0
- - harfbuzz >=10.1.0
+ - freetype >=2.13.3,<3.0a0
+ - harfbuzz >=11.0.0,<12.0a0
- libgcc >=13
- libzlib >=1.3.1,<2.0a0
- - sdl2 >=2.30.10,<3.0a0
+ - sdl2 >=2.32.50,<3.0a0
license: Zlib
purls: []
- size: 61954
- timestamp: 1736117956977
-- conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_ttf-2.24.0-h443c5de_0.conda
- sha256: 5dc56fd1de51dec2f7b63b1a9069ab35ab0494d9cd5af164ab0019cbd9564cf6
- md5: 3cf5cf83deccb663b4e932d3d4b28f57
+ size: 62419
+ timestamp: 1743198496689
+- conda: https://prefix.dev/conda-forge/osx-arm64/sdl2_ttf-2.24.0-h49b19f9_1.conda
+ sha256: 8682907cc92eca66771e4fe951feb97396a1d76b73a6d910f3888d93951a3dc2
+ md5: a9a40538ce016f25b160e80a193f4075
depends:
- __osx >=11.0
- - freetype >=2.12.1,<3.0a0
- - harfbuzz >=10.1.0
- - sdl2 >=2.30.10,<3.0a0
+ - freetype >=2.13.3,<3.0a0
+ - harfbuzz >=11.0.0,<12.0a0
+ - sdl2 >=2.32.50,<3.0a0
license: Zlib
purls: []
- size: 45429
- timestamp: 1736118165229
-- conda: https://prefix.dev/conda-forge/linux-64/sdl3-3.2.26-h68140b3_0.conda
- sha256: 31bfb3db00feb74dab5c3420b6b7529cd9a044fda381c422adc817df09ae17b1
- md5: 8d9c193907f1c9defb46322f06990105
+ size: 45515
+ timestamp: 1743198654173
+- conda: https://prefix.dev/conda-forge/linux-64/sdl3-3.4.8-hdeec2a5_0.conda
+ sha256: bec327fffc08369afe4c1384a5b50cac9b38e7af42ebdf5f89628633bd980dbd
+ md5: 508bad511e617479f0ad60cc49fba903
depends:
- libstdcxx >=14
- libgcc >=14
- __glibc >=2.17,<3.0.a0
- - libgcc >=14
- - xorg-libxscrnsaver >=1.2.4,<2.0a0
- - libvulkan-loader >=1.4.328.1,<2.0a0
- - liburing >=2.12,<2.13.0a0
- - libusb >=1.0.29,<2.0a0
- - libegl >=1.7.0,<2.0a0
- - libunwind >=1.8.3,<1.9.0a0
- - libgl >=1.7.0,<2.0a0
- - dbus >=1.16.2,<2.0a0
- - xorg-libx11 >=1.8.12,<2.0a0
- - libudev1 >=257.9
- pulseaudio-client >=17.0,<17.1.0a0
- - libxkbcommon >=1.12.3,<2.0a0
+ - libgl >=1.7.0,<2.0a0
+ - libunwind >=1.8.3,<1.9.0a0
+ - xorg-libxtst >=1.2.5,<2.0a0
+ - xorg-libxcursor >=1.2.3,<2.0a0
+ - libegl >=1.7.0,<2.0a0
- xorg-libxfixes >=6.0.2,<7.0a0
+ - libudev1 >=257.13
+ - xorg-libxext >=1.3.7,<2.0a0
+ - libusb >=1.0.29,<2.0a0
+ - wayland >=1.25.0,<2.0a0
+ - xorg-libxi >=1.8.2,<2.0a0
+ - libvulkan-loader >=1.4.341.0,<2.0a0
+ - liburing >=2.14,<2.15.0a0
+ - xorg-libxscrnsaver >=1.2.4,<2.0a0
+ - dbus >=1.16.2,<2.0a0
+ - libxkbcommon >=1.13.1,<2.0a0
- libdrm >=2.4.125,<2.5.0a0
- - wayland >=1.24.0,<2.0a0
- - xorg-libxcursor >=1.2.3,<2.0a0
- - xorg-libxext >=1.3.6,<2.0a0
+ - xorg-libx11 >=1.8.13,<2.0a0
license: Zlib
purls: []
- size: 1936312
- timestamp: 1761847993797
-- conda: https://prefix.dev/conda-forge/osx-arm64/sdl3-3.2.26-h919df07_0.conda
- sha256: 18fb9c4c3f4d787151a38e493a8bfde329321700ae8530bda052aba315e7be99
- md5: ed708ea3309264c803198b22b706e1ed
+ size: 2146080
+ timestamp: 1777693555942
+- conda: https://prefix.dev/conda-forge/osx-arm64/sdl3-3.4.8-h6fa9c73_0.conda
+ sha256: 0c15a7f3f46d175c70ca7b032cee3eee9ef602492688a79a6e734f2efc466dad
+ md5: 53311362c6da21e58442bfb959991455
depends:
- - libcxx >=19
- __osx >=11.0
- - libvulkan-loader >=1.4.328.1,<2.0a0
+ - libcxx >=19
+ - libvulkan-loader >=1.4.341.0,<2.0a0
- libusb >=1.0.29,<2.0a0
- dbus >=1.16.2,<2.0a0
license: Zlib
purls: []
- size: 1414256
- timestamp: 1761848029452
+ size: 1561566
+ timestamp: 1777693640800
- conda: https://prefix.dev/conda-forge/linux-64/secretstorage-3.4.1-py311h38be061_0.conda
sha256: 47f28b12e760ae3ce8a1e616c5b56f5e874e0e4a036bdd09516ebf263c19521f
md5: ec955e67147942a68469a46d0bdf0a7b
@@ -4882,17 +4737,17 @@ packages:
- pkg:pypi/secretstorage?source=hash-mapping
size: 32968
timestamp: 1763045430433
-- conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda
- sha256: 972560fcf9657058e3e1f97186cc94389144b46dbdf58c807ce62e83f977e863
- md5: 4de79c071274a53dcaf2a8c749d1499e
+- conda: https://prefix.dev/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda
+ sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1
+ md5: 8e194e7b992f99a5015edbd4ebd38efd
depends:
- - python >=3.9
+ - python >=3.10
license: MIT
license_family: MIT
purls:
- pkg:pypi/setuptools?source=hash-mapping
- size: 748788
- timestamp: 1748804951958
+ size: 639697
+ timestamp: 1773074868565
- conda: https://prefix.dev/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
sha256: 1d6534df8e7924d9087bd388fbac5bd868c5bf8971c36885f9f016da0657d22b
md5: 83ea3a2ddb7a75c1b09cea582aa4f106
@@ -4901,7 +4756,7 @@ packages:
license: MIT
license_family: MIT
purls:
- - pkg:pypi/shellingham?source=compressed-mapping
+ - pkg:pypi/shellingham?source=hash-mapping
size: 15018
timestamp: 1762858315311
- conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
@@ -4936,12 +4791,12 @@ packages:
license: Apache-2.0
license_family: Apache
purls:
- - pkg:pypi/sniffio?source=compressed-mapping
+ - pkg:pypi/sniffio?source=hash-mapping
size: 15698
timestamp: 1762941572482
-- conda: https://prefix.dev/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda
- sha256: 34e2e9c505cd25dba0a9311eb332381b15147cf599d972322a7c197aedfc8ce2
- md5: 9859766c658e78fec9afa4a54891d920
+- conda: https://prefix.dev/conda-forge/linux-64/svt-av1-4.0.1-hecca717_0.conda
+ sha256: 4a1d2005153b9454fc21c9bad1b539df189905be49e851ec62a6212c2e045381
+ md5: 2a2170a3e5c9a354d09e4be718c43235
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
@@ -4949,56 +4804,56 @@ packages:
license: BSD-2-Clause
license_family: BSD
purls: []
- size: 2741200
- timestamp: 1756086702093
-- conda: https://prefix.dev/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda
- sha256: 3b0f4f2a6697f0cdbbe0c0b5f5c7fa8064483d58b4d9674d5babda7f7146af7a
- md5: cb56c114b25f20bd09ef1c66a21136ff
+ size: 2619743
+ timestamp: 1769664536467
+- conda: https://prefix.dev/conda-forge/osx-arm64/svt-av1-4.0.1-h0cb729a_0.conda
+ sha256: bdef3c1c4d2a396ad4f7dc64c5e9a02d4c5a21ff93ed07a33e49574de5d2d18d
+ md5: 8badc3bf16b62272aa2458f138223821
depends:
- __osx >=11.0
- libcxx >=19
license: BSD-2-Clause
license_family: BSD
purls: []
- size: 1474592
- timestamp: 1756086729326
-- conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda
- sha256: 1544760538a40bcd8ace2b1d8ebe3eb5807ac268641f8acdc18c69c5ebfeaf64
- md5: 86bc20552bf46075e3d92b67f089172d
+ size: 1456245
+ timestamp: 1769664727051
+- conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac
+ md5: cffd3bdd58090148f4cfcd831f4b26ab
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
- libzlib >=1.3.1,<2.0a0
constrains:
- xorg-libx11 >=1.8.12,<2.0a0
license: TCL
license_family: BSD
purls: []
- size: 3284905
- timestamp: 1763054914403
-- conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda
- sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7
- md5: a73d54a5abba6543cb2f0af1bfbd6851
+ size: 3301196
+ timestamp: 1769460227866
+- conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3
+ md5: a9d86bc62f39b94c4661716624eb21b0
depends:
- __osx >=11.0
- libzlib >=1.3.1,<2.0a0
license: TCL
license_family: BSD
purls: []
- size: 3125484
- timestamp: 1763055028377
-- conda: https://prefix.dev/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda
- sha256: cb77c660b646c00a48ef942a9e1721ee46e90230c7c570cdeb5a893b5cce9bff
- md5: d2732eb636c264dc9aa4cbee404b1a53
+ size: 3127137
+ timestamp: 1769460817696
+- conda: https://prefix.dev/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
+ sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd
+ md5: b5325cf06a000c5b14970462ff5e4d58
depends:
- python >=3.10
- python
license: MIT
license_family: MIT
purls:
- - pkg:pypi/tomli?source=compressed-mapping
- size: 20973
- timestamp: 1760014679845
+ - pkg:pypi/tomli?source=hash-mapping
+ size: 21561
+ timestamp: 1774492402955
- conda: https://prefix.dev/conda-forge/noarch/tomli-w-1.2.0-pyhd8ed1ab_0.conda
sha256: 304834f2438017921d69f05b3f5a6394b42dc89a90a6128a46acbf8160d377f6
md5: 32e37e8fe9ef45c637ee38ad51377769
@@ -5010,20 +4865,20 @@ packages:
- pkg:pypi/tomli-w?source=hash-mapping
size: 12680
timestamp: 1736962345843
-- conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda
- sha256: f8d3b49c084831a20923f66826f30ecfc55a4cd951e544b7213c692887343222
- md5: 146402bf0f11cbeb8f781fa4309a95d3
+- conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.0-pyha770c72_0.conda
+ sha256: 1cd52f9ccb4854c4d731438afe0e833b6b71edaf5ede661152aa98efb3a7cc70
+ md5: 42ef10a8f7f5d55a2e267c0d5daa6387
depends:
- - python >=3.9
+ - python >=3.10
license: MIT
license_family: MIT
purls:
- - pkg:pypi/tomlkit?source=hash-mapping
- size: 38777
- timestamp: 1749127286558
-- conda: https://prefix.dev/conda-forge/linux-64/tornado-6.5.2-py311h49ec1c0_2.conda
- sha256: 1913516458f92df2a0b415426dce27cc14922415787f4b672a707b233631b1e0
- md5: 8d7a63fc9653ed0bdc253a51d9a5c371
+ - pkg:pypi/tomlkit?source=compressed-mapping
+ size: 41169
+ timestamp: 1778423744478
+- conda: https://prefix.dev/conda-forge/linux-64/tornado-6.5.5-py311h49ec1c0_0.conda
+ sha256: cd8bc08ca661291cfbb05581b79f7e6a6971a072a54a335abcea5460c1230880
+ md5: 73b44a114241e564deb5846e7394bf19
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
@@ -5033,11 +4888,11 @@ packages:
license_family: Apache
purls:
- pkg:pypi/tornado?source=hash-mapping
- size: 869926
- timestamp: 1762506861961
-- conda: https://prefix.dev/conda-forge/osx-arm64/tornado-6.5.2-py311h9408147_2.conda
- sha256: 70d4187bb8ec0016b9928e76eeaea0b2a021f741ea40a6ea2ee4b9c142ddaf24
- md5: e3af56f478d9919acac254ff82ad150c
+ size: 876817
+ timestamp: 1774358035290
+- conda: https://prefix.dev/conda-forge/osx-arm64/tornado-6.5.5-py311hc949640_0.conda
+ sha256: 572923958ec987f3f68e228dfec243c89ab94b50398731215d36e2e040db8703
+ md5: 6f12d9ff025ab1875dff67ad779ca29d
depends:
- __osx >=11.0
- python >=3.11,<3.12.0a0
@@ -5047,19 +4902,19 @@ packages:
license_family: Apache
purls:
- pkg:pypi/tornado?source=hash-mapping
- size: 869348
- timestamp: 1762507255364
-- conda: https://prefix.dev/conda-forge/noarch/trove-classifiers-2025.11.14.15-pyhd8ed1ab_0.conda
- sha256: a99493b5d25272f160bfb4ef2b7878794dfa405ba9922bcee48b6f75675e3a74
- md5: e976edbb0821092956a85f1e7b627a97
+ size: 875022
+ timestamp: 1774358570256
+- conda: https://prefix.dev/conda-forge/noarch/trove-classifiers-2026.5.7.17-pyhd8ed1ab_0.conda
+ sha256: b06931edfab2f6bde64fae1e7323216b74389b5a27242bf2b54b401246efb04d
+ md5: 58ad6f35e45eca7db0fd5da95aafc693
depends:
- python >=3.10
license: Apache-2.0
license_family: Apache
purls:
- pkg:pypi/trove-classifiers?source=hash-mapping
- size: 19522
- timestamp: 1763138400309
+ size: 20076
+ timestamp: 1778229720640
- conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda
sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731
md5: 0caa1af407ecff61170c9437a808404d
@@ -5072,16 +4927,16 @@ packages:
- pkg:pypi/typing-extensions?source=hash-mapping
size: 51692
timestamp: 1756220668932
-- conda: https://prefix.dev/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
- sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192
- md5: 4222072737ccff51314b5ece9c7d6f5a
+- conda: https://prefix.dev/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
+ sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c
+ md5: ad659d0a2b3e47e38d829aa8cad2d610
license: LicenseRef-Public-Domain
purls: []
- size: 122968
- timestamp: 1742727099393
-- conda: https://prefix.dev/conda-forge/linux-64/ukkonen-1.0.1-py311hdf67eae_6.conda
- sha256: a6201979b8619bb9122609eb2189287c33e4a75ad240e4880898941764022782
- md5: 57e703b0057f992687fb9ad154dc48e4
+ size: 119135
+ timestamp: 1767016325805
+- conda: https://prefix.dev/conda-forge/linux-64/ukkonen-1.1.0-py311hdf67eae_0.conda
+ sha256: 31ad759e2d7668f1615021bca0ae2358a284939e3ca3fcb971b3f7aa83c2a6d6
+ md5: 5a715cf5e3dc6cd68717c50e47dd7b6b
depends:
- __glibc >=2.17,<3.0.a0
- cffi
@@ -5093,11 +4948,11 @@ packages:
license_family: MIT
purls:
- pkg:pypi/ukkonen?source=hash-mapping
- size: 14597
- timestamp: 1761594653571
-- conda: https://prefix.dev/conda-forge/osx-arm64/ukkonen-1.0.1-py311h57a9ea7_6.conda
- sha256: b11dee8446a369aaa149b1650dec45105949c9043cc8a5802b292305d48b8718
- md5: 2534d14fa2ebc5a6657bc835bc695557
+ size: 14898
+ timestamp: 1769438724694
+- conda: https://prefix.dev/conda-forge/osx-arm64/ukkonen-1.1.0-py311h572238d_0.conda
+ sha256: 1bf370d4d4698b339d54342b2d653d9ed26f5004af2da3e5556b641fb5e56e1b
+ md5: 2855259ce88c41c1b2297041ec75f540
depends:
- __osx >=11.0
- cffi
@@ -5109,8 +4964,8 @@ packages:
license_family: MIT
purls:
- pkg:pypi/ukkonen?source=hash-mapping
- size: 14449
- timestamp: 1761595344417
+ size: 14760
+ timestamp: 1769438970347
- conda: https://prefix.dev/conda-forge/noarch/userpath-1.9.2-pyhd8ed1ab_0.conda
sha256: 26e53b42f7fa1127e6115a35b91c20e15f75984648b88f115136f27715d4a440
md5: 946e3571aaa55e0870fec0dea13de3bf
@@ -5123,23 +4978,22 @@ packages:
- pkg:pypi/userpath?source=hash-mapping
size: 14292
timestamp: 1735925027874
-- conda: https://prefix.dev/conda-forge/linux-64/uv-0.9.10-h76e24b7_0.conda
- sha256: b926d266bce5f8c9734ecdfeab6739a06ca778771ab1739a549eb77227d8e952
- md5: 502e06bfb394fa379ad7bcea89f51e16
+- conda: https://prefix.dev/conda-forge/linux-64/uv-0.11.12-h29f5ae7_0.conda
+ sha256: 4fc1481bb6ecf5a1a755661483eb99a0ec2054cd02e5893b55f9c80bac9437a8
+ md5: e86a95851fd53465f0657c068cb6c403
depends:
- libgcc >=14
- - libstdcxx >=14
- - libgcc >=14
- __glibc >=2.17,<3.0.a0
+ - libstdcxx >=14
constrains:
- __glibc >=2.17
license: Apache-2.0 OR MIT
purls: []
- size: 17090471
- timestamp: 1763412537330
-- conda: https://prefix.dev/conda-forge/osx-arm64/uv-0.9.10-h1bde295_0.conda
- sha256: e369edaf12de310eda74694233862362381fd50c17afa2709f8ad26ed3f5e74b
- md5: e27feb5e75c6277dfce3a775e8ed3fda
+ size: 19190543
+ timestamp: 1778287747008
+- conda: https://prefix.dev/conda-forge/osx-arm64/uv-0.11.12-h2c65d96_0.conda
+ sha256: e8050d5259b4f77b838ecc573643451593b39970f1b25412267db10919de2ca3
+ md5: a5b3c646ec7fe96f192bb4f5fa8442f3
depends:
- __osx >=11.0
- libcxx >=19
@@ -5147,49 +5001,52 @@ packages:
- __osx >=11.0
license: Apache-2.0 OR MIT
purls: []
- size: 14815480
- timestamp: 1763412481324
-- conda: https://prefix.dev/conda-forge/noarch/virtualenv-20.35.4-pyhd8ed1ab_0.conda
- sha256: 77193c99c6626c58446168d3700f9643d8c0dab1f6deb6b9dd039e6872781bfb
- md5: cfccfd4e8d9de82ed75c8e2c91cab375
+ size: 16562936
+ timestamp: 1778287818707
+- conda: https://prefix.dev/conda-forge/noarch/virtualenv-21.3.1-pyhcf101f3_0.conda
+ sha256: 8888b4725d4166ab54435ba4b6a24e9f089578301a88f8157d012dd38a88ac43
+ md5: dd6a1f3ce9d1cfa8b5b1b32953a80a55
depends:
+ - python >=3.10
- distlib >=0.3.7,<1
- - filelock >=3.12.2,<4
+ - filelock <4,>=3.24.2
+ - importlib-metadata >=6.6
- platformdirs >=3.9.1,<5
- - python >=3.10
+ - python-discovery >=1
- typing_extensions >=4.13.2
+ - python
license: MIT
license_family: MIT
purls:
- - pkg:pypi/virtualenv?source=compressed-mapping
- size: 4401341
- timestamp: 1761726489722
-- conda: https://prefix.dev/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda
- sha256: 3aa04ae8e9521d9b56b562376d944c3e52b69f9d2a0667f77b8953464822e125
- md5: 035da2e4f5770f036ff704fa17aace24
+ - pkg:pypi/virtualenv?source=hash-mapping
+ size: 5154970
+ timestamp: 1777964652524
+- conda: https://prefix.dev/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda
+ sha256: ea374d57a8fcda281a0a89af0ee49a2c2e99cc4ac97cf2e2db7064e74e764bdb
+ md5: 996583ea9c796e5b915f7d7580b51ea6
depends:
- __glibc >=2.17,<3.0.a0
- - libexpat >=2.7.1,<3.0a0
+ - libexpat >=2.7.4,<3.0a0
- libffi >=3.5.2,<3.6.0a0
- libgcc >=14
- libstdcxx >=14
license: MIT
license_family: MIT
purls: []
- size: 329779
- timestamp: 1761174273487
-- conda: https://prefix.dev/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda
- sha256: aa03b49f402959751ccc6e21932d69db96a65a67343765672f7862332aa32834
- md5: 71ae752a748962161b4740eaff510258
+ size: 334139
+ timestamp: 1773959575393
+- conda: https://prefix.dev/conda-forge/linux-64/xkeyboard-config-2.47-hb03c661_0.conda
+ sha256: 19c2bb14bec84b0e995b56b752369775c75f1589314b43733948bb5f471a6915
+ md5: b56e0c8432b56decafae7e78c5f29ba5
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
- - xorg-libx11 >=1.8.12,<2.0a0
+ - xorg-libx11 >=1.8.13,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 396975
- timestamp: 1759543819846
+ size: 399291
+ timestamp: 1772021302485
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda
sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b
md5: fb901ff28063514abb6046c9ec2c4a45
@@ -5214,18 +5071,18 @@ packages:
purls: []
size: 27590
timestamp: 1741896361728
-- conda: https://prefix.dev/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda
- sha256: 51909270b1a6c5474ed3978628b341b4d4472cd22610e5f22b506855a5e20f67
- md5: db038ce880f100acc74dba10302b5630
+- conda: https://prefix.dev/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda
+ sha256: 516d4060139dbb4de49a4dcdc6317a9353fb39ebd47789c14e6fe52de0deee42
+ md5: 861fb6ccbc677bb9a9fb2468430b9c6a
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
- libxcb >=1.17.0,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 835896
- timestamp: 1741901112627
+ size: 839652
+ timestamp: 1770819209719
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda
sha256: 6bc6ab7a90a5d8ac94c7e300cc10beb0500eeba4b99822768ca2f2ef356f731b
md5: b2895afaf55bf96a8c8282a2e47a5de0
@@ -5237,19 +5094,19 @@ packages:
purls: []
size: 15321
timestamp: 1762976464266
-- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda
- sha256: 753f73e990c33366a91fd42cc17a3d19bb9444b9ca5ff983605fa9e953baf57f
- md5: d3c295b50f092ab525ffe3c2aa4b7413
+- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxcomposite-0.4.7-hb03c661_0.conda
+ sha256: 048c103000af9541c919deef03ae7c5e9c570ffb4024b42ecb58dbde402e373a
+ md5: f2ba4192d38b6cef2bb2c25029071d90
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - xorg-libx11 >=1.8.10,<2.0a0
- - xorg-libxfixes >=6.0.1,<7.0a0
+ - libgcc >=14
+ - xorg-libx11 >=1.8.12,<2.0a0
+ - xorg-libxfixes >=6.0.2,<7.0a0
license: MIT
license_family: MIT
purls: []
- size: 13603
- timestamp: 1727884600744
+ size: 14415
+ timestamp: 1770044404696
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda
sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a
md5: 2ccd714aa2242315acaf0a67faea780b
@@ -5327,20 +5184,20 @@ packages:
purls: []
size: 47179
timestamp: 1727799254088
-- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda
- sha256: 1b9141c027f9d84a9ee5eb642a0c19457c788182a5a73c5a9083860ac5c20a8c
- md5: 5e2eb9bf77394fc2e5918beefec9f9ab
+- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxinerama-1.1.6-hecca717_0.conda
+ sha256: 3a9da41aac6dca9d3ff1b53ee18b9d314de88add76bafad9ca2287a494abcd86
+ md5: 93f5d4b5c17c8540479ad65f206fea51
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - libstdcxx >=13
- - xorg-libx11 >=1.8.10,<2.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - xorg-libx11 >=1.8.12,<2.0a0
- xorg-libxext >=1.3.6,<2.0a0
license: MIT
license_family: MIT
purls: []
- size: 13891
- timestamp: 1727908521531
+ size: 14818
+ timestamp: 1769432261050
- conda: https://prefix.dev/conda-forge/linux-64/xorg-libxrandr-1.5.5-hb03c661_0.conda
sha256: 80ed047a5cb30632c3dc5804c7716131d767089f65877813d4ae855ee5c9d343
md5: e192019153591938acf7322b6459d36e
@@ -5407,94 +5264,94 @@ packages:
purls: []
size: 18701
timestamp: 1769434732453
-- conda: https://prefix.dev/conda-forge/linux-64/xorg-xorgproto-2024.1-hb9d3cd8_1.conda
- sha256: 1316680be6edddee0156b86ec1102fc8286f51c1a5440366ed1db596a2dc3731
- md5: 7c21106b851ec72c037b162c216d8f05
+- conda: https://prefix.dev/conda-forge/linux-64/xorg-xorgproto-2025.1-hb03c661_0.conda
+ sha256: 7a8c64938428c2bfd016359f9cb3c44f94acc256c6167dbdade9f2a1f5ca7a36
+ md5: aa8d21be4b461ce612d8f5fb791decae
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
+ - libgcc >=14
license: MIT
license_family: MIT
purls: []
- size: 565425
- timestamp: 1726846388217
-- conda: https://prefix.dev/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda
- sha256: 802725371682ea06053971db5b4fb7fbbcaee9cb1804ec688f55e51d74660617
- md5: 68eae977d7d1196d32b636a026dc015d
+ size: 570010
+ timestamp: 1766154256151
+- conda: https://prefix.dev/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda
+ sha256: 2553fd3ec0a1020b2ca05ca10b0036a596cb0d4bf3645922fcf69dacce0e6679
+ md5: 6a1b6af49a334e4e06b9f103367762bf
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - liblzma 5.8.1 hb9d3cd8_2
- - liblzma-devel 5.8.1 hb9d3cd8_2
- - xz-gpl-tools 5.8.1 hbcc6ac9_2
- - xz-tools 5.8.1 hb9d3cd8_2
+ - libgcc >=14
+ - liblzma 5.8.3 hb03c661_0
+ - liblzma-devel 5.8.3 hb03c661_0
+ - xz-gpl-tools 5.8.3 ha02ee65_0
+ - xz-tools 5.8.3 hb03c661_0
license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later
purls: []
- size: 23987
- timestamp: 1749230104359
-- conda: https://prefix.dev/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda
- sha256: afb747cf017b67cc31d54c6e6c4bd1b1e179fe487a3d23a856232ed7fd0b099b
- md5: 39435c82e5a007ef64cbb153ecc40cfd
+ size: 24360
+ timestamp: 1775825568523
+- conda: https://prefix.dev/conda-forge/osx-arm64/xz-5.8.3-hd0f0c4f_0.conda
+ sha256: d5bb880876336f625523c022aa9bdc6be76a85df721d9e7b33c352f528619185
+ md5: 4df12be699991b97b66a85cc46ea75b5
depends:
- __osx >=11.0
- - liblzma 5.8.1 h39f12f2_2
- - liblzma-devel 5.8.1 h39f12f2_2
- - xz-gpl-tools 5.8.1 h9a6d368_2
- - xz-tools 5.8.1 h39f12f2_2
+ - liblzma 5.8.3 h8088a28_0
+ - liblzma-devel 5.8.3 h8088a28_0
+ - xz-gpl-tools 5.8.3 hd0f0c4f_0
+ - xz-tools 5.8.3 h8088a28_0
license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later
purls: []
- size: 23995
- timestamp: 1749230346887
-- conda: https://prefix.dev/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda
- sha256: 840838dca829ec53f1160f3fca6dbfc43f2388b85f15d3e867e69109b168b87b
- md5: bf627c16aa26231720af037a2709ab09
+ size: 24348
+ timestamp: 1775825911109
+- conda: https://prefix.dev/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda
+ sha256: 8f139666ea18dc8340a44a54056627dd4e89e242e8cd136ab2467d6dc2c192ba
+ md5: 8f5e2c6726c1339287a3c76a2c138ac7
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - liblzma 5.8.1 hb9d3cd8_2
+ - libgcc >=14
+ - liblzma 5.8.3 hb03c661_0
constrains:
- - xz 5.8.1.*
+ - xz 5.8.3.*
license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later
purls: []
- size: 33911
- timestamp: 1749230090353
-- conda: https://prefix.dev/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda
- sha256: a0790cfb48d240e7b655b0d797a00040219cf39e3ee38e2104e548515df4f9c2
- md5: 09b1442c1d49ac7c5f758c44695e77d1
+ size: 34213
+ timestamp: 1775825548743
+- conda: https://prefix.dev/conda-forge/osx-arm64/xz-gpl-tools-5.8.3-hd0f0c4f_0.conda
+ sha256: 0864d53202f618b4c8a5f2c38b7029f14b900b852e99b6248813303f69ccfdee
+ md5: 43d168a8c95a8fcdcc44c2a0a7887653
depends:
- __osx >=11.0
- - liblzma 5.8.1 h39f12f2_2
+ - liblzma 5.8.3 h8088a28_0
constrains:
- - xz 5.8.1.*
+ - xz 5.8.3.*
license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later
purls: []
- size: 34103
- timestamp: 1749230329933
-- conda: https://prefix.dev/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda
- sha256: 58034f3fca491075c14e61568ad8b25de00cb3ae479de3e69be6d7ee5d3ace28
- md5: 1bad2995c8f1c8075c6c331bf96e46fb
+ size: 34224
+ timestamp: 1775825884830
+- conda: https://prefix.dev/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda
+ sha256: 162ebd76803464b8c8ebc7d45df32edf0ec717b3bf369a437ae3b0254f22dc2e
+ md5: b62b615caa60812640f24db3a8d0fc87
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - liblzma 5.8.1 hb9d3cd8_2
+ - libgcc >=14
+ - liblzma 5.8.3 hb03c661_0
constrains:
- - xz 5.8.1.*
+ - xz 5.8.3.*
license: 0BSD AND LGPL-2.1-or-later
purls: []
- size: 96433
- timestamp: 1749230076687
-- conda: https://prefix.dev/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda
- sha256: 9d1232705e3d175f600dc8e344af9182d0341cdaa73d25330591a28532951063
- md5: 37996935aa33138fca43e4b4563b6a28
+ size: 95955
+ timestamp: 1775825530484
+- conda: https://prefix.dev/conda-forge/osx-arm64/xz-tools-5.8.3-h8088a28_0.conda
+ sha256: beb7fb34d5517cce06f5f89710de7108a8ca65ed9c0324269fc0446807bcbd91
+ md5: b8c47eab4e58fe7015a12ceb9d5b114c
depends:
- __osx >=11.0
- - liblzma 5.8.1 h39f12f2_2
+ - liblzma 5.8.3 h8088a28_0
constrains:
- - xz 5.8.1.*
+ - xz 5.8.3.*
license: 0BSD AND LGPL-2.1-or-later
purls: []
- size: 86425
- timestamp: 1749230316106
+ size: 85931
+ timestamp: 1775825857949
- conda: https://prefix.dev/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad
md5: a77f85f77be52ff59391544bfe73390a
@@ -5516,95 +5373,59 @@ packages:
purls: []
size: 83386
timestamp: 1753484079473
-- conda: https://prefix.dev/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda
- sha256: 7560d21e1b021fd40b65bfb72f67945a3fcb83d78ad7ccf37b8b3165ec3b68ad
- md5: df5e78d904988eb55042c0c97446079f
+- conda: https://prefix.dev/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ sha256: 523616c0530d305d2216c2b4a8dfd3872628b60083255b89c5e0d8c42e738cca
+ md5: e1c36c6121a7c9c76f2f148f1e83b983
depends:
- - python >=3.9
+ - python >=3.10
+ - python
license: MIT
license_family: MIT
purls:
- pkg:pypi/zipp?source=hash-mapping
- size: 22963
- timestamp: 1749421737203
-- conda: https://prefix.dev/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda
- sha256: 5d7c0e5f0005f74112a34a7425179f4eb6e73c92f5d109e6af4ddeca407c92ab
- md5: c9f075ab2f33b3bbee9e62d4ad0a6cd8
+ size: 24461
+ timestamp: 1776131454755
+- conda: https://prefix.dev/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
+ sha256: 245c9ee8d688e23661b95e3c6dd7272ca936fabc03d423cdb3cdee1bbcf9f2f2
+ md5: c2a01a08fc991620a74b32420e97868a
depends:
- __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - libzlib 1.3.1 hb9d3cd8_2
+ - libzlib 1.3.2 h25fd6f3_2
license: Zlib
license_family: Other
purls: []
- size: 92286
- timestamp: 1727963153079
-- conda: https://prefix.dev/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda
- sha256: 58f8860756680a4831c1bf4f294e2354d187f2e999791d53b1941834c4b37430
- md5: e3170d898ca6cb48f1bb567afb92f775
+ size: 95931
+ timestamp: 1774072620848
+- conda: https://prefix.dev/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
+ sha256: 8dd2ac25f0ba714263aac5832d46985648f4bfb9b305b5021d702079badc08d2
+ md5: f1c0bce276210bed45a04949cfe8dc20
depends:
- __osx >=11.0
- - libzlib 1.3.1 h8359307_2
+ - libzlib 1.3.2 h8088a28_2
license: Zlib
license_family: Other
purls: []
- size: 77606
- timestamp: 1727963209370
-- conda: https://prefix.dev/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda
- sha256: d534a6518c2d8eccfa6579d75f665261484f0f2f7377b50402446a9433d46234
- md5: ca45bfd4871af957aaa5035593d5efd2
+ size: 81123
+ timestamp: 1774072974535
+- conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
+ sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7
+ md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829
depends:
- - python
- - cffi >=1.11
- - zstd >=1.5.7,<1.5.8.0a0
- - libgcc >=14
- __glibc >=2.17,<3.0.a0
- - zstd >=1.5.7,<1.6.0a0
- - python_abi 3.11.* *_cp311
- license: BSD-3-Clause
- license_family: BSD
- purls:
- - pkg:pypi/zstandard?source=hash-mapping
- size: 466893
- timestamp: 1762512695614
-- conda: https://prefix.dev/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda
- sha256: 2ee455765fe831cca8fe127c56ae99938e353797135fe33140b28abb4fbe1049
- md5: 651594b8f9b9cccc5948287a18903c34
- depends:
- - python
- - cffi >=1.11
- - zstd >=1.5.7,<1.5.8.0a0
- - python 3.11.* *_cpython
- - __osx >=11.0
- - python_abi 3.11.* *_cp311
- - zstd >=1.5.7,<1.6.0a0
- license: BSD-3-Clause
- license_family: BSD
- purls:
- - pkg:pypi/zstandard?source=hash-mapping
- size: 390026
- timestamp: 1762512731928
-- conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda
- sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb
- md5: 6432cb5d4ac0046c3ac0a8a0f95842f9
- depends:
- - __glibc >=2.17,<3.0.a0
- - libgcc >=13
- - libstdcxx >=13
- libzlib >=1.3.1,<2.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 567578
- timestamp: 1742433379869
-- conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda
- sha256: 0d02046f57f7a1a3feae3e9d1aa2113788311f3cf37a3244c71e61a93177ba67
- md5: e6f69c7bcccdefa417f056fa593b40f0
+ size: 601375
+ timestamp: 1764777111296
+- conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
+ sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9
+ md5: ab136e4c34e97f34fb621d2592a393d8
depends:
- __osx >=11.0
- libzlib >=1.3.1,<2.0a0
license: BSD-3-Clause
license_family: BSD
purls: []
- size: 399979
- timestamp: 1742433432699
+ size: 433413
+ timestamp: 1764777166076
diff --git a/pixi.toml b/pixi.toml
index bdb97f60..77a54b98 100644
--- a/pixi.toml
+++ b/pixi.toml
@@ -14,7 +14,7 @@ python = "3.11.*"
numpy = "==2.0.2"
pygame = "==2.6.1"
protobuf = "==3.20.2"
-gymnasium = "==1.0.0"
+gymnasium = "==1.3.0"
pyserial = "==3.5"
pytest = "==8.3.4"
ruff = ">=0.9.8"
@@ -42,7 +42,7 @@ cmake = "<3.27"
libode = ">=0.16.2,<0.17"
[feature.robosim.pypi-dependencies]
-rc-robosim = "<2"
+rc-robosim = ">=1.2,<2"
numpy = "<=2.0.2"
[tasks]
@@ -56,3 +56,6 @@ precommit-uninstall = "pre-commit uninstall"
[environments]
robosim = { features = ["robosim"], no-default-feature = true }
+
+[target.linux-64.tasks]
+main = "setsid python -m main"
diff --git a/utama_core/config/referee_constants.py b/utama_core/config/referee_constants.py
index 3e5333aa..bb70a4fb 100644
--- a/utama_core/config/referee_constants.py
+++ b/utama_core/config/referee_constants.py
@@ -2,6 +2,7 @@
BALL_KEEP_OUT_DISTANCE = 0.8
BALL_PLACEMENT_DONE_DISTANCE = 0.15
+KICKER_READY_DISTANCE = 0.3
OPPONENT_DEFENSE_AREA_KEEP_DISTANCE = 0.25
PENALTY_BEHIND_MARK_DISTANCE = 0.4
PENALTY_MARK_HALF_FIELD_RATIO = 0.5
diff --git a/utama_core/config/robot_params.py b/utama_core/config/robot_params.py
index 4e9729fa..44a1010a 100644
--- a/utama_core/config/robot_params.py
+++ b/utama_core/config/robot_params.py
@@ -16,7 +16,7 @@ class RobotParams:
GRSIM_PARAMS = RobotParams(
MAX_VEL=2,
MAX_ANGULAR_VEL=4,
- MAX_ACCELERATION=8,
+ MAX_ACCELERATION=4,
MAX_ANGULAR_ACCELERATION=50,
KICK_SPD=5,
DRIBBLE_SPD=3,
diff --git a/utama_core/custom_referee/custom_referee.py b/utama_core/custom_referee/custom_referee.py
index c5a7d956..21d07b58 100644
--- a/utama_core/custom_referee/custom_referee.py
+++ b/utama_core/custom_referee/custom_referee.py
@@ -165,9 +165,45 @@ def seed_clock(self, timestamp: float, initial_command: RefereeCommand = Referee
if initial_command != RefereeCommand.HALT:
self._state.set_command(initial_command, timestamp)
- def set_command(self, command: RefereeCommand, timestamp: float) -> None:
- """Manual override — for operator use or test scripting."""
+ def set_command(
+ self,
+ command: RefereeCommand,
+ timestamp: float,
+ designated_position: Optional[tuple[float, float]] = None,
+ next_command: Optional[RefereeCommand] = None,
+ status_message: Optional[str] = None,
+ ) -> None:
+ """Manual override for operator use or test scripting.
+
+ Args:
+ command: Referee command to apply.
+ timestamp: Timestamp to associate with the command transition.
+ designated_position: Optional ball placement/free-kick target to
+ expose through ``RefereeData.designated_position``.
+ next_command: Optional queued command to expose through
+ ``RefereeData.next_command``.
+ status_message: Optional human-readable referee status.
+
+ The optional fields are intentionally thin wrappers around the custom
+ referee state machine so integration tests and scenario runners do not
+ need to reach into private ``_state`` attributes.
+ """
self._state.set_command(command, timestamp)
+ if designated_position is not None:
+ self._state.ball_placement_target = designated_position
+ if next_command is not None:
+ self._state.next_command = next_command
+ if status_message is not None:
+ self._state.status_message = status_message
+
+ def force_command(
+ self,
+ command: "RefereeCommand",
+ timestamp: float,
+ ball_placement_target=None,
+ ) -> None:
+ """God-mode override — bypasses the STOP-first guard."""
+ self._state.force_command(command, timestamp, ball_placement_target)
# ------------------------------------------------------------------
# Properties (read-only access for callers that need to inspect state)
diff --git a/utama_core/custom_referee/gui.py b/utama_core/custom_referee/gui.py
index b957e774..796f133c 100644
--- a/utama_core/custom_referee/gui.py
+++ b/utama_core/custom_referee/gui.py
@@ -229,8 +229,14 @@ def _handle_command(self):
try:
payload = json.loads(body)
cmd = RefereeCommand[payload["command"]]
+ designated = payload.get("designated")
with server_instance._lock:
- server_instance._referee.set_command(cmd, time.time())
+ ref = server_instance._referee
+ if designated is not None and hasattr(ref, "force_command"):
+ target = (float(designated[0]), float(designated[1]))
+ ref.force_command(cmd, time.time(), ball_placement_target=target)
+ else:
+ ref.set_command(cmd, time.time())
self.send_response(204)
self.end_headers()
except (KeyError, ValueError, json.JSONDecodeError) as exc:
@@ -657,6 +663,38 @@ def _build_static_config(profile: "RefereeProfile") -> dict:
}
.pill.on { background: #1a3a1a; color: var(--green); border: 1px solid #2a5a2a; }
.pill.off { background: #3a1a1a; color: #c0605a; border: 1px solid #5a2a2a; }
+
+ /* ── God mode ── */
+ .btn-god { background: #4a0d6e; color: #d49eff; border: 1px solid #7a3dae; }
+ .btn-god.active { background: #7a3dae; color: #fff; border-color: #d49eff; }
+
+ #ctx-menu {
+ display: none;
+ position: fixed;
+ z-index: 999;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ min-width: 180px;
+ box-shadow: 0 4px 16px rgba(0,0,0,.5);
+ overflow: hidden;
+ }
+ #ctx-menu button {
+ display: block;
+ width: 100%;
+ text-align: left;
+ border-radius: 0;
+ border: none;
+ background: none;
+ color: var(--text);
+ padding: 9px 14px;
+ font-size: .78rem;
+ text-transform: none;
+ letter-spacing: 0;
+ font-weight: 400;
+ }
+ #ctx-menu button:hover { background: var(--border); }
+ #ctx-menu .ctx-sep { height: 1px; background: var(--border); margin: 2px 0; }
@@ -728,6 +766,10 @@ def _build_static_config(profile: "RefereeProfile") -> dict:
Profile — loading…
@@ -1004,6 +1052,80 @@ def _build_static_config(profile: "RefereeProfile") -> dict:
return `
`;
}
+// --- God mode ---
+let _godMode = false;
+let _ctxFieldPos = null; // {x, y} in field coords at last right-click
+
+function toggleGod() {
+ _godMode = !_godMode;
+ const btn = document.getElementById('god-btn');
+ btn.classList.toggle('active', _godMode);
+ btn.title = _godMode
+ ? 'God mode ON — right-click on the field canvas to move the ball.'
+ : 'God mode: right-click anywhere on the field to move the ball or issue ball placement.';
+}
+
+function _canvasToField(canvas, clientX, clientY) {
+ if (!_cfg) return null;
+ const rect = canvas.getBoundingClientRect();
+ const cw = canvas.width, ch = canvas.height;
+ const M = 12;
+ const scale = (cw - 2 * M) / (2 * _cfg.half_length);
+ const px = (clientX - rect.left) * (cw / rect.width);
+ const py = (clientY - rect.top) * (ch / rect.height);
+ // Inverse of: toX(fx) = M + (fx + half_length) * scale
+ const fx = (px - M) / scale - _cfg.half_length;
+ // Inverse of: toY(fy) = CH - M - (fy + half_width) * scale
+ const fy = (ch - M - py) / scale - _cfg.half_width;
+ return { x: fx, y: fy };
+}
+
+function _hideCtxMenu() {
+ document.getElementById('ctx-menu').style.display = 'none';
+}
+
+document.addEventListener('click', _hideCtxMenu);
+document.addEventListener('keydown', e => { if (e.key === 'Escape') _hideCtxMenu(); });
+
+document.getElementById('field-canvas').addEventListener('contextmenu', function(e) {
+ if (!_godMode) return;
+ e.preventDefault();
+ _ctxFieldPos = _canvasToField(this, e.clientX, e.clientY);
+ if (!_ctxFieldPos) return;
+ const menu = document.getElementById('ctx-menu');
+ menu.style.display = 'block';
+ const mx = Math.min(e.clientX, window.innerWidth - menu.offsetWidth - 4);
+ const my = Math.min(e.clientY, window.innerHeight - menu.offsetHeight - 4);
+ menu.style.left = mx + 'px';
+ menu.style.top = my + 'px';
+});
+
+function ctxPlaceBallYellow() {
+ _hideCtxMenu();
+ if (!_ctxFieldPos) return;
+ fetch('/command', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ command: 'BALL_PLACEMENT_YELLOW',
+ designated: [_ctxFieldPos.x, _ctxFieldPos.y],
+ }),
+ }).catch(err => console.error('command error:', err));
+}
+
+function ctxPlaceBallBlue() {
+ _hideCtxMenu();
+ if (!_ctxFieldPos) return;
+ fetch('/command', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ command: 'BALL_PLACEMENT_BLUE',
+ designated: [_ctxFieldPos.x, _ctxFieldPos.y],
+ }),
+ }).catch(err => console.error('command error:', err));
+}
+
fetch('/config').then(r => r.json()).then(c => {
document.getElementById('page-title').textContent = 'Custom Referee — ' + c.profile_name;
document.getElementById('cfg-title').textContent = 'Profile: ' + c.profile_name;
diff --git a/utama_core/custom_referee/rules/out_of_bounds_rule.py b/utama_core/custom_referee/rules/out_of_bounds_rule.py
index 3f40adf2..c8e7a98b 100644
--- a/utama_core/custom_referee/rules/out_of_bounds_rule.py
+++ b/utama_core/custom_referee/rules/out_of_bounds_rule.py
@@ -15,7 +15,7 @@
RefereeCommand.FORCE_START,
}
-_INFIELD_OFFSET = 0.1 # metres inside the boundary for free-kick placement
+_INFIELD_OFFSET = 0.25 # metres inside the boundary for a playable free-kick placement
class OutOfBoundsRule(BaseRule):
diff --git a/utama_core/custom_referee/state_machine.py b/utama_core/custom_referee/state_machine.py
index f194bdcd..aa465de7 100644
--- a/utama_core/custom_referee/state_machine.py
+++ b/utama_core/custom_referee/state_machine.py
@@ -1,633 +1,710 @@
-"""GameStateMachine: owns all mutable game state for the CustomReferee."""
-
-from __future__ import annotations
-
-import copy
-import logging
-import math
-from typing import Optional
-
-from utama_core.config.field_params import STANDARD_FIELD_DIMS
-from utama_core.custom_referee.geometry import RefereeGeometry
-from utama_core.custom_referee.profiles.profile_loader import AutoAdvanceConfig
-from utama_core.custom_referee.rules.base_rule import RuleViolation
-from utama_core.entities.data.referee import RefereeData
-from utama_core.entities.game.game_frame import GameFrame
-from utama_core.entities.game.team_info import TeamInfo
-from utama_core.entities.referee.referee_command import RefereeCommand
-from utama_core.entities.referee.stage import Stage
-
-logger = logging.getLogger(__name__)
-
-_TRANSITION_COOLDOWN = 0.3 # seconds — prevents command oscillation
-_BALL_CLEAR_DIST = 0.5 # metres — all robots must be this far from ball before queued restart
-_KICKER_READY_DIST = 0.3 # metres — kicker must be within this distance to trigger free kick start
-_PLACEMENT_DONE_DIST = 0.15 # metres — ball within this dist of target → placement complete
-_AUTO_ADVANCE_DELAY = 2.0 # seconds — readiness must be sustained this long before play starts
-
-
-class GameStateMachine:
- """Owns score, command, and stage. Produces ``RefereeData`` each tick."""
-
- def __init__(
- self,
- half_duration_seconds: float,
- kickoff_team: str,
- n_robots_yellow: int,
- n_robots_blue: int,
- initial_stage: Stage = Stage.NORMAL_FIRST_HALF_PRE,
- force_start_after_goal: bool = False,
- stop_duration_seconds: float = 3.0,
- prepare_duration_seconds: float = 3.0,
- kickoff_timeout_seconds: float = 10.0,
- geometry: Optional[RefereeGeometry] = None,
- auto_advance: Optional[AutoAdvanceConfig] = None,
- ) -> None:
- self.command = RefereeCommand.HALT
- self.command_counter = 0
- self.command_timestamp = 0.0
-
- self.stage = initial_stage
- # Seeded by seed_clock() after the first valid game frame is available.
- self.stage_start_time: Optional[float] = None
- self.stage_duration = half_duration_seconds
-
- self.yellow_team = TeamInfo(
- name="Yellow",
- score=0,
- red_cards=0,
- yellow_card_times=[],
- yellow_cards=0,
- timeouts=4,
- timeout_time=300,
- goalkeeper=0,
- foul_counter=0,
- ball_placement_failures=0,
- can_place_ball=True,
- max_allowed_bots=n_robots_yellow,
- bot_substitution_intent=False,
- bot_substitution_allowed=True,
- bot_substitutions_left=5,
- )
- self.blue_team = TeamInfo(
- name="Blue",
- score=0,
- red_cards=0,
- yellow_card_times=[],
- yellow_cards=0,
- timeouts=4,
- timeout_time=300,
- goalkeeper=0,
- foul_counter=0,
- ball_placement_failures=0,
- can_place_ball=True,
- max_allowed_bots=n_robots_blue,
- bot_substitution_intent=False,
- bot_substitution_allowed=True,
- bot_substitutions_left=5,
- )
-
- self.next_command: Optional[RefereeCommand] = None
- self.ball_placement_target: Optional[tuple[float, float]] = None
- self.status_message: Optional[str] = None
-
- # Kickoff team initialised from profile.
- self._kickoff_team_is_yellow = kickoff_team.lower() == "yellow"
-
- # Arcade auto-advance: after stop_duration_seconds in STOP following a
- # goal, automatically issue FORCE_START instead of waiting for operator.
- self._force_start_after_goal = force_start_after_goal
- self._stop_duration_seconds = stop_duration_seconds
- self._stop_entered_time: float = -math.inf # wall time when STOP was last entered
-
- # Auto-advance timings.
- self._prepare_duration_seconds = prepare_duration_seconds
- self._kickoff_timeout_seconds = kickoff_timeout_seconds
- self._prepare_entered_time: float = -math.inf # wall time when PREPARE_KICKOFF was entered
- self._normal_start_time: float = -math.inf # wall time when NORMAL_START was entered
-
- # Ball position snapshot at NORMAL_START — used to detect if the ball has moved.
- self._ball_pos_at_normal_start: Optional[tuple[float, float]] = None
-
- # Timestamps for sustained-readiness countdown before play-starting advances.
- # Set to math.inf when condition is not yet met; fire when elapsed >= _AUTO_ADVANCE_DELAY.
- self._advance2_ready_since: float = math.inf # PREPARE_* → NORMAL_START
- self._advance3_ready_since: float = math.inf # DIRECT_FREE_* → NORMAL_START
- self._advance4_ready_since: float = math.inf # BALL_PLACEMENT_* → next_command
-
- # Field geometry (used for readiness checks).
- self._geometry: Optional[RefereeGeometry] = geometry
-
- # Cooldown: don't process a new violation within this window.
- self._last_transition_time: float = -math.inf
-
- # Per-transition enable flags (default: all on).
- self._auto_advance = auto_advance if auto_advance is not None else AutoAdvanceConfig()
-
- # ------------------------------------------------------------------
- # Public API
- # ------------------------------------------------------------------
-
- _PREPARE_KICKOFF_COMMANDS = frozenset(
- {
- RefereeCommand.PREPARE_KICKOFF_YELLOW,
- RefereeCommand.PREPARE_KICKOFF_BLUE,
- }
- )
- _DIRECT_FREE_COMMANDS = frozenset(
- {
- RefereeCommand.DIRECT_FREE_YELLOW,
- RefereeCommand.DIRECT_FREE_BLUE,
- }
- )
- _PREPARE_PENALTY_COMMANDS = frozenset(
- {
- RefereeCommand.PREPARE_PENALTY_YELLOW,
- RefereeCommand.PREPARE_PENALTY_BLUE,
- }
- )
- _BALL_PLACEMENT_COMMANDS = frozenset(
- {
- RefereeCommand.BALL_PLACEMENT_YELLOW,
- RefereeCommand.BALL_PLACEMENT_BLUE,
- }
- )
-
- def step(
- self,
- current_time: float,
- violation: Optional[RuleViolation],
- game_frame: Optional["GameFrame"] = None,
- ) -> RefereeData:
- """Process one tick. Apply violation if not in cooldown. Return RefereeData."""
- if self.stage_start_time is None:
- self.stage_start_time = current_time
-
- if violation is not None and self._can_transition(current_time):
- self._apply_violation(violation, current_time)
-
- # ----------------------------------------------------------------
- # Auto-advance 1: STOP → next queued restart
- # Fires when all robots are ≥ _BALL_CLEAR_DIST from the ball.
- # ----------------------------------------------------------------
- if (
- self._auto_advance.stop_to_next_command
- and self.command == RefereeCommand.STOP
- and self.next_command in self._NEEDS_STOP_FIRST
- and game_frame is not None
- and self._all_robots_clear(game_frame)
- ):
- logger.info("All robots clear — auto-advancing STOP → %s", self.next_command.name)
- self.command = self.next_command
- self.command_counter += 1
- self.command_timestamp = current_time
- if self.command in self._BALL_PLACEMENT_COMMANDS:
- self.next_command = RefereeCommand.NORMAL_START
- self._advance4_ready_since = math.inf
- elif self.command in self._DIRECT_FREE_COMMANDS:
- self.next_command = RefereeCommand.NORMAL_START
- self._advance3_ready_since = math.inf
- elif self.command in self._PREPARE_KICKOFF_COMMANDS or self.command in self._PREPARE_PENALTY_COMMANDS:
- self.next_command = RefereeCommand.NORMAL_START
- self._prepare_entered_time = current_time
- self._advance2_ready_since = math.inf
- self._last_transition_time = current_time
-
- # ----------------------------------------------------------------
- # Auto-advance 2a: PREPARE_KICKOFF_* → NORMAL_START
- # Fires after prepare_duration_seconds AND one attacker is inside
- # the centre circle, sustained for _AUTO_ADVANCE_DELAY seconds.
- # ----------------------------------------------------------------
- elif self._auto_advance.prepare_kickoff_to_normal and self.command in self._PREPARE_KICKOFF_COMMANDS:
- ready = (
- (current_time - self._prepare_entered_time) >= self._prepare_duration_seconds
- and game_frame is not None
- and self._kicker_in_centre_circle(self.command, game_frame)
- )
- if ready:
- if self._advance2_ready_since == math.inf:
- self._advance2_ready_since = current_time
- logger.debug("Advance 2 countdown started (%s)", self.command.name)
- elif (current_time - self._advance2_ready_since) >= _AUTO_ADVANCE_DELAY:
- logger.info(
- "Kicker in centre circle — auto-advancing %s → NORMAL_START",
- self.command.name,
- )
- self.command = RefereeCommand.NORMAL_START
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = None
- self.status_message = None
- self._normal_start_time = current_time
- self._ball_pos_at_normal_start = (
- (game_frame.ball.p.x, game_frame.ball.p.y) if game_frame.ball is not None else None
- )
- self._advance2_ready_since = math.inf
- self._last_transition_time = current_time
- else:
- self._advance2_ready_since = math.inf
-
- # ----------------------------------------------------------------
- # Auto-advance 2b: PREPARE_PENALTY_* → NORMAL_START
- # Fires after prepare_duration_seconds when the kicker reaches the
- # penalty mark, sustained for _AUTO_ADVANCE_DELAY seconds.
- # ----------------------------------------------------------------
- elif self._auto_advance.prepare_penalty_to_normal and self.command in self._PREPARE_PENALTY_COMMANDS:
- ready = (
- (current_time - self._prepare_entered_time) >= self._prepare_duration_seconds
- and game_frame is not None
- and self._penalty_kicker_ready(self.command, game_frame)
- )
- if ready:
- if self._advance2_ready_since == math.inf:
- self._advance2_ready_since = current_time
- logger.debug("Advance 2 countdown started (%s)", self.command.name)
- elif (current_time - self._advance2_ready_since) >= _AUTO_ADVANCE_DELAY:
- logger.info(
- "Kicker at penalty mark — auto-advancing %s → NORMAL_START",
- self.command.name,
- )
- self.command = RefereeCommand.NORMAL_START
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = None
- self.status_message = None
- self._normal_start_time = current_time
- self._ball_pos_at_normal_start = (
- (game_frame.ball.p.x, game_frame.ball.p.y) if game_frame.ball is not None else None
- )
- self._advance2_ready_since = math.inf
- self._last_transition_time = current_time
- else:
- self._advance2_ready_since = math.inf
-
- # ----------------------------------------------------------------
- # Auto-advance 3: DIRECT_FREE_* → NORMAL_START
- # Fires when the kicker is within _KICKER_READY_DIST of the ball
- # AND all defending robots are ≥ _BALL_CLEAR_DIST away, sustained
- # for _AUTO_ADVANCE_DELAY seconds.
- # ----------------------------------------------------------------
- elif self._auto_advance.direct_free_to_normal and self.command in self._DIRECT_FREE_COMMANDS:
- ready = game_frame is not None and self._free_kick_ready(self.command, game_frame)
- if ready:
- if self._advance3_ready_since == math.inf:
- self._advance3_ready_since = current_time
- logger.debug("Advance 3 countdown started (%s)", self.command.name)
- elif (current_time - self._advance3_ready_since) >= _AUTO_ADVANCE_DELAY:
- logger.info("Free kick ready — auto-advancing %s → NORMAL_START", self.command.name)
- self.command = RefereeCommand.NORMAL_START
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = None
- self.status_message = None
- self._normal_start_time = current_time
- self._ball_pos_at_normal_start = (
- (game_frame.ball.p.x, game_frame.ball.p.y) if game_frame.ball is not None else None
- )
- self._advance3_ready_since = math.inf
- self._last_transition_time = current_time
- else:
- self._advance3_ready_since = math.inf
-
- # ----------------------------------------------------------------
- # Auto-advance 4: BALL_PLACEMENT_* → next_command
- # Fires when ball reaches within _PLACEMENT_DONE_DIST of target,
- # sustained for _AUTO_ADVANCE_DELAY seconds.
- # ----------------------------------------------------------------
- elif self._auto_advance.ball_placement_to_next and self.command in self._BALL_PLACEMENT_COMMANDS:
- ready = self.next_command is not None and game_frame is not None and self._ball_placement_done(game_frame)
- if ready:
- if self._advance4_ready_since == math.inf:
- self._advance4_ready_since = current_time
- logger.debug("Advance 4 countdown started (%s)", self.command.name)
- elif (current_time - self._advance4_ready_since) >= _AUTO_ADVANCE_DELAY:
- logger.info(
- "Ball placement complete — auto-advancing %s → %s",
- self.command.name,
- self.next_command.name,
- )
- self.command = self.next_command
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = None
- self._advance4_ready_since = math.inf
- self.status_message = None
- self._last_transition_time = current_time
- else:
- self._advance4_ready_since = math.inf
-
- # ----------------------------------------------------------------
- # Auto-advance 5: NORMAL_START → FORCE_START
- # Fires after kickoff_timeout_seconds if the ball hasn't moved ≥5 cm.
- # ----------------------------------------------------------------
- elif (
- self._auto_advance.normal_start_to_force
- and self.command == RefereeCommand.NORMAL_START
- and self._ball_pos_at_normal_start is not None
- and (current_time - self._normal_start_time) >= self._kickoff_timeout_seconds
- and game_frame is not None
- and game_frame.ball is not None
- and not self._ball_has_moved(game_frame)
- ):
- logger.info("Kickoff/free-kick timeout — auto-advancing NORMAL_START → FORCE_START")
- self.command = RefereeCommand.FORCE_START
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = None
- self.status_message = None
- self._ball_pos_at_normal_start = None
- self._last_transition_time = current_time
-
- # ----------------------------------------------------------------
- # Legacy force-start path: STOP → FORCE_START after goal
- # ----------------------------------------------------------------
- elif (
- self._force_start_after_goal
- and self.command == RefereeCommand.STOP
- and self.next_command in self._PREPARE_KICKOFF_COMMANDS
- and (current_time - self._stop_entered_time) >= self._stop_duration_seconds
- ):
- self.command = RefereeCommand.FORCE_START
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = None
- self.status_message = None
- self._last_transition_time = current_time
- logger.info("Auto-advanced STOP → FORCE_START after goal (force-start profile mode)")
-
- return self._generate_referee_data(current_time)
-
- def _all_robots_clear(self, game_frame: "GameFrame") -> bool:
- """Return True if every robot on both teams is ≥ _BALL_CLEAR_DIST from the ball."""
- ball = game_frame.ball
- if ball is None:
- return True
- bx, by = ball.p.x, ball.p.y
- for r in list(game_frame.friendly_robots.values()) + list(game_frame.enemy_robots.values()):
- if math.hypot(r.p.x - bx, r.p.y - by) < _BALL_CLEAR_DIST:
- return False
- return True
-
- def _kicker_in_centre_circle(self, command: RefereeCommand, game_frame: "GameFrame") -> bool:
- """Return True if at least one robot of the attacking team is inside the centre circle."""
- r = self._geometry.center_circle_radius if self._geometry is not None else 0.5 # fallback for standalone use
- kicking_is_yellow = command == RefereeCommand.PREPARE_KICKOFF_YELLOW
- attackers = (
- game_frame.friendly_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.enemy_robots
- )
- return any(math.hypot(robot.p.x, robot.p.y) <= r for robot in attackers.values())
-
- def _penalty_kicker_ready(self, command: RefereeCommand, game_frame: "GameFrame") -> bool:
- """Return True when an attacking robot is within the ready radius of the penalty mark."""
- kicking_is_yellow = command == RefereeCommand.PREPARE_PENALTY_YELLOW
- attackers = (
- game_frame.friendly_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.enemy_robots
- )
- if not attackers:
- return False
-
- half_length = (
- self._geometry.half_length if self._geometry is not None else STANDARD_FIELD_DIMS.full_field_half_length
- )
- yellow_is_right = game_frame.my_team_is_right == game_frame.my_team_is_yellow
- if kicking_is_yellow:
- goal_sign = -1.0 if yellow_is_right else 1.0
- else:
- goal_sign = 1.0 if yellow_is_right else -1.0
- penalty_mark_x = goal_sign * half_length * 0.5
-
- closest = min(math.hypot(robot.p.x - penalty_mark_x, robot.p.y) for robot in attackers.values())
- return closest <= _KICKER_READY_DIST
-
- def _free_kick_ready(self, command: RefereeCommand, game_frame: "GameFrame") -> bool:
- """Return True when a free kick is ready to start:
- - The kicker (closest attacker to ball) is within _KICKER_READY_DIST of the ball.
- - All defending robots are ≥ _BALL_CLEAR_DIST from the ball.
- """
- ball = game_frame.ball
- if ball is None:
- return False
- bx, by = ball.p.x, ball.p.y
-
- kicking_is_yellow = command == RefereeCommand.DIRECT_FREE_YELLOW
- attackers = (
- game_frame.friendly_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.enemy_robots
- )
- defenders = (
- game_frame.enemy_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.friendly_robots
- )
-
- # Check defending robots are all clear.
- if any(math.hypot(r.p.x - bx, r.p.y - by) < _BALL_CLEAR_DIST for r in defenders.values()):
- return False
-
- # Check at least one attacker is close to the ball (kicker in position).
- if not attackers:
- return False
- closest = min(math.hypot(r.p.x - bx, r.p.y - by) for r in attackers.values())
- return closest <= _KICKER_READY_DIST
-
- def _ball_has_moved(self, game_frame: "GameFrame") -> bool:
- """Return True if the ball has moved ≥ 0.05 m since NORMAL_START."""
- if self._ball_pos_at_normal_start is None or game_frame.ball is None:
- return False
- ox, oy = self._ball_pos_at_normal_start
- return math.hypot(game_frame.ball.p.x - ox, game_frame.ball.p.y - oy) >= 0.05
-
- def _ball_placement_done(self, game_frame: "GameFrame") -> bool:
- """Return True when the ball is within _PLACEMENT_DONE_DIST of the placement target."""
- if self.ball_placement_target is None or game_frame.ball is None:
- return False
- tx, ty = self.ball_placement_target
- return math.hypot(game_frame.ball.p.x - tx, game_frame.ball.p.y - ty) <= _PLACEMENT_DONE_DIST
-
- # Commands that require robots to clear the ball before they take effect.
- # In a real match these are always preceded by STOP.
- _NEEDS_STOP_FIRST = frozenset(
- {
- RefereeCommand.PREPARE_KICKOFF_YELLOW,
- RefereeCommand.PREPARE_KICKOFF_BLUE,
- RefereeCommand.DIRECT_FREE_YELLOW,
- RefereeCommand.DIRECT_FREE_BLUE,
- RefereeCommand.PREPARE_PENALTY_YELLOW,
- RefereeCommand.PREPARE_PENALTY_BLUE,
- RefereeCommand.BALL_PLACEMENT_YELLOW,
- RefereeCommand.BALL_PLACEMENT_BLUE,
- }
- )
-
- def seed_clock(self, timestamp: float) -> None:
- """Align all internal timers to *timestamp*.
-
- Resets every timer that was initialised before the real vision clock
- was known (construction time) so that durations are measured from the
- correct timebase. Safe to call only once, immediately after the first
- valid game frame is available.
- """
- if self.stage_start_time is None:
- self.stage_start_time = timestamp
- self.command_timestamp = timestamp
- # Push all "entered-at" sentinels to the new timebase so that
- # cooldowns / durations are not immediately satisfied.
- self._last_transition_time = timestamp - _TRANSITION_COOLDOWN # allow transitions immediately
- self._stop_entered_time = timestamp - self._stop_duration_seconds # won't auto-advance yet
- self._prepare_entered_time = timestamp - self._prepare_duration_seconds # same
-
- def set_command(self, command: RefereeCommand, timestamp: float) -> None:
- """Manual override — for operator use or test scripting.
-
- If *command* is a set-piece command (kickoff, free kick, penalty,
- ball placement) and the game is not already in STOP or HALT, a STOP
- is issued first and the requested command is stored as ``next_command``
- so the operator (or a script) can advance to it after robots have
- cleared the ball. This mirrors real-match game-controller behaviour
- and prevents robots from receiving a PREPARE_KICKOFF while they are
- still within the keep-out zone around the ball.
- """
- _ALREADY_STOPPED = (RefereeCommand.STOP, RefereeCommand.HALT)
-
- if command in self._NEEDS_STOP_FIRST and self.command not in _ALREADY_STOPPED:
- # Insert STOP; park the real command as next_command.
- logger.info("Inserting STOP before %s so robots can clear the ball", command.name)
- self.command = RefereeCommand.STOP
- self.command_counter += 1
- self.command_timestamp = timestamp
- self.next_command = command
- self.status_message = None
- self._stop_entered_time = timestamp
- return
-
- # NORMAL_START while in STOP with a pending set-piece: advance to the
- # set-piece first so robots can form up. Auto-advance will then issue
- # NORMAL_START after prepare_duration_seconds.
- if (
- command == RefereeCommand.NORMAL_START
- and self.command == RefereeCommand.STOP
- and self.next_command in self._NEEDS_STOP_FIRST
- ):
- logger.info(
- "Manually advancing STOP → %s (auto NORMAL_START in %.1f s)",
- self.next_command.name,
- self._prepare_duration_seconds,
- )
- self.command = self.next_command
- self.command_counter += 1
- self.command_timestamp = timestamp
- self.next_command = RefereeCommand.NORMAL_START
- self.status_message = None
- self._prepare_entered_time = timestamp
- return
-
- self.command = command
- self.command_counter += 1
- self.command_timestamp = timestamp
- self.status_message = None
- self._advance2_ready_since = math.inf
- self._advance3_ready_since = math.inf
- self._advance4_ready_since = math.inf
-
- if command in (RefereeCommand.STOP, RefereeCommand.HALT):
- self._stop_entered_time = timestamp
- elif command in (
- RefereeCommand.PREPARE_KICKOFF_YELLOW,
- RefereeCommand.PREPARE_KICKOFF_BLUE,
- RefereeCommand.PREPARE_PENALTY_YELLOW,
- RefereeCommand.PREPARE_PENALTY_BLUE,
- ):
- self._prepare_entered_time = timestamp
-
- # Advance PRE stages to their active counterpart when play begins.
- _PRE_TO_ACTIVE = {
- Stage.NORMAL_FIRST_HALF_PRE: Stage.NORMAL_FIRST_HALF,
- Stage.NORMAL_SECOND_HALF_PRE: Stage.NORMAL_SECOND_HALF,
- Stage.EXTRA_FIRST_HALF_PRE: Stage.EXTRA_FIRST_HALF,
- Stage.EXTRA_SECOND_HALF_PRE: Stage.EXTRA_SECOND_HALF,
- }
- if command in (RefereeCommand.NORMAL_START, RefereeCommand.FORCE_START):
- active = _PRE_TO_ACTIVE.get(self.stage)
- if active is not None:
- self.advance_stage(active, timestamp)
-
- logger.info("Referee command manually set to: %s", command.name)
-
- def advance_stage(self, new_stage: Stage, timestamp: float) -> None:
- """Advance the game stage."""
- logger.info("Stage %s → %s", self.stage.name, new_stage.name)
- self.stage = new_stage
- self.stage_start_time = timestamp
-
- # ------------------------------------------------------------------
- # Internal helpers
- # ------------------------------------------------------------------
-
- def _can_transition(self, current_time: float) -> bool:
- return (current_time - self._last_transition_time) >= _TRANSITION_COOLDOWN
-
- def _apply_violation(self, violation: RuleViolation, current_time: float) -> None:
- """Update state in response to a detected violation."""
- if violation.rule_name == "goal":
- self._handle_goal(violation, current_time)
- else:
- self._handle_foul(violation, current_time)
-
- self._last_transition_time = current_time
-
- def _handle_goal(self, violation: RuleViolation, current_time: float) -> None:
- # Determine scorer from next_command (loser gets the kickoff).
- if violation.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE:
- # Blue gets kickoff → yellow scored.
- self.yellow_team.increment_score()
- logger.info(
- "Goal by Yellow! Score: Yellow %d – Blue %d",
- self.yellow_team.score,
- self.blue_team.score,
- )
- elif violation.next_command == RefereeCommand.PREPARE_KICKOFF_YELLOW:
- # Yellow gets kickoff → blue scored.
- self.blue_team.increment_score()
- logger.info(
- "Goal by Blue! Score: Yellow %d – Blue %d",
- self.yellow_team.score,
- self.blue_team.score,
- )
-
- self.command = RefereeCommand.STOP
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = violation.next_command
- self.ball_placement_target = (0.0, 0.0)
- self.status_message = violation.status_message
- self._stop_entered_time = current_time
-
- def _handle_foul(self, violation: RuleViolation, current_time: float) -> None:
- self.command = violation.suggested_command
- self.command_counter += 1
- self.command_timestamp = current_time
- self.next_command = violation.next_command
- self.ball_placement_target = violation.designated_position
- self.status_message = violation.status_message
- logger.info(
- "Foul detected: %s → %s (next: %s)",
- violation.rule_name,
- violation.suggested_command.name,
- violation.next_command.name if violation.next_command else "None",
- )
-
- def _generate_referee_data(self, current_time: float) -> RefereeData:
- stage_time_left = max(0.0, self.stage_duration - (current_time - self.stage_start_time))
- return RefereeData(
- source_identifier="custom_referee",
- time_sent=current_time,
- time_received=current_time,
- referee_command=self.command,
- referee_command_timestamp=self.command_timestamp,
- stage=self.stage,
- stage_time_left=stage_time_left,
- blue_team=copy.copy(self.blue_team),
- yellow_team=copy.copy(self.yellow_team),
- designated_position=self.ball_placement_target,
- blue_team_on_positive_half=None,
- next_command=self.next_command,
- current_action_time_remaining=None,
- status_message=self.status_message,
- )
+"""GameStateMachine: owns all mutable game state for the CustomReferee."""
+
+from __future__ import annotations
+
+import copy
+import logging
+import math
+from typing import Optional
+
+from utama_core.config.field_params import STANDARD_FIELD_DIMS
+from utama_core.custom_referee.geometry import RefereeGeometry
+from utama_core.custom_referee.profiles.profile_loader import AutoAdvanceConfig
+from utama_core.custom_referee.rules.base_rule import RuleViolation
+from utama_core.entities.data.referee import RefereeData
+from utama_core.entities.game.game_frame import GameFrame
+from utama_core.entities.game.team_info import TeamInfo
+from utama_core.entities.referee.referee_command import RefereeCommand
+from utama_core.entities.referee.stage import Stage
+
+logger = logging.getLogger(__name__)
+
+_TRANSITION_COOLDOWN = 0.3 # seconds — prevents command oscillation
+_BALL_CLEAR_DIST = 0.5 # metres — all robots must be this far from ball before queued restart
+_KICKER_READY_DIST = 0.3 # metres — kicker must be within this distance to trigger free kick start
+_PLACEMENT_DONE_DIST = 0.15 # metres — ball within this dist of target → placement complete
+_AUTO_ADVANCE_DELAY = 2.0 # seconds — readiness must be sustained this long before play starts
+
+
+class GameStateMachine:
+ """Owns score, command, and stage. Produces ``RefereeData`` each tick."""
+
+ def __init__(
+ self,
+ half_duration_seconds: float,
+ kickoff_team: str,
+ n_robots_yellow: int,
+ n_robots_blue: int,
+ initial_stage: Stage = Stage.NORMAL_FIRST_HALF_PRE,
+ force_start_after_goal: bool = False,
+ stop_duration_seconds: float = 3.0,
+ prepare_duration_seconds: float = 3.0,
+ kickoff_timeout_seconds: float = 10.0,
+ geometry: Optional[RefereeGeometry] = None,
+ auto_advance: Optional[AutoAdvanceConfig] = None,
+ ) -> None:
+ self.command = RefereeCommand.HALT
+ self.command_counter = 0
+ self.command_timestamp = 0.0
+
+ self.stage = initial_stage
+ # Seeded by seed_clock() after the first valid game frame is available.
+ self.stage_start_time: Optional[float] = None
+ self.stage_duration = half_duration_seconds
+
+ self.yellow_team = TeamInfo(
+ name="Yellow",
+ score=0,
+ red_cards=0,
+ yellow_card_times=[],
+ yellow_cards=0,
+ timeouts=4,
+ timeout_time=300,
+ goalkeeper=0,
+ foul_counter=0,
+ ball_placement_failures=0,
+ can_place_ball=True,
+ max_allowed_bots=n_robots_yellow,
+ bot_substitution_intent=False,
+ bot_substitution_allowed=True,
+ bot_substitutions_left=5,
+ )
+ self.blue_team = TeamInfo(
+ name="Blue",
+ score=0,
+ red_cards=0,
+ yellow_card_times=[],
+ yellow_cards=0,
+ timeouts=4,
+ timeout_time=300,
+ goalkeeper=0,
+ foul_counter=0,
+ ball_placement_failures=0,
+ can_place_ball=True,
+ max_allowed_bots=n_robots_blue,
+ bot_substitution_intent=False,
+ bot_substitution_allowed=True,
+ bot_substitutions_left=5,
+ )
+
+ self.next_command: Optional[RefereeCommand] = None
+ self.ball_placement_target: Optional[tuple[float, float]] = None
+ self._post_ball_placement_command: Optional[RefereeCommand] = None
+ self.status_message: Optional[str] = None
+
+ # Kickoff team initialised from profile.
+ self._kickoff_team_is_yellow = kickoff_team.lower() == "yellow"
+
+ # Arcade auto-advance: after stop_duration_seconds in STOP following a
+ # goal, automatically issue FORCE_START instead of waiting for operator.
+ self._force_start_after_goal = force_start_after_goal
+ self._stop_duration_seconds = stop_duration_seconds
+ self._stop_entered_time: float = -math.inf # wall time when STOP was last entered
+
+ # Auto-advance timings.
+ self._prepare_duration_seconds = prepare_duration_seconds
+ self._kickoff_timeout_seconds = kickoff_timeout_seconds
+ self._prepare_entered_time: float = -math.inf # wall time when PREPARE_KICKOFF was entered
+ self._normal_start_time: float = -math.inf # wall time when NORMAL_START was entered
+
+ # Ball position snapshot at NORMAL_START — used to detect if the ball has moved.
+ self._ball_pos_at_normal_start: Optional[tuple[float, float]] = None
+
+ # Timestamps for sustained-readiness countdown before play-starting advances.
+ # Set to math.inf when condition is not yet met; fire when elapsed >= _AUTO_ADVANCE_DELAY.
+ self._advance2_ready_since: float = math.inf # PREPARE_* → NORMAL_START
+ self._advance3_ready_since: float = math.inf # DIRECT_FREE_* → NORMAL_START
+ self._advance4_ready_since: float = math.inf # BALL_PLACEMENT_* → next_command
+
+ # Field geometry (used for readiness checks).
+ self._geometry: Optional[RefereeGeometry] = geometry
+
+ # Cooldown: don't process a new violation within this window.
+ self._last_transition_time: float = -math.inf
+
+ # Per-transition enable flags (default: all on).
+ self._auto_advance = auto_advance if auto_advance is not None else AutoAdvanceConfig()
+
+ # ------------------------------------------------------------------
+ # Public API
+ # ------------------------------------------------------------------
+
+ _PREPARE_KICKOFF_COMMANDS = frozenset(
+ {
+ RefereeCommand.PREPARE_KICKOFF_YELLOW,
+ RefereeCommand.PREPARE_KICKOFF_BLUE,
+ }
+ )
+ _DIRECT_FREE_COMMANDS = frozenset(
+ {
+ RefereeCommand.DIRECT_FREE_YELLOW,
+ RefereeCommand.DIRECT_FREE_BLUE,
+ }
+ )
+ _PREPARE_PENALTY_COMMANDS = frozenset(
+ {
+ RefereeCommand.PREPARE_PENALTY_YELLOW,
+ RefereeCommand.PREPARE_PENALTY_BLUE,
+ }
+ )
+ _BALL_PLACEMENT_COMMANDS = frozenset(
+ {
+ RefereeCommand.BALL_PLACEMENT_YELLOW,
+ RefereeCommand.BALL_PLACEMENT_BLUE,
+ }
+ )
+
+ def step(
+ self,
+ current_time: float,
+ violation: Optional[RuleViolation],
+ game_frame: Optional["GameFrame"] = None,
+ ) -> RefereeData:
+ """Process one tick. Apply violation if not in cooldown. Return RefereeData."""
+ if self.stage_start_time is None:
+ self.stage_start_time = current_time
+
+ if violation is not None and self._can_transition(current_time):
+ self._apply_violation(violation, current_time)
+
+ # ----------------------------------------------------------------
+ # Auto-advance 1: STOP → next queued restart
+ # Fires when all robots are ≥ _BALL_CLEAR_DIST from the ball.
+ # ----------------------------------------------------------------
+ if (
+ self._auto_advance.stop_to_next_command
+ and self.command == RefereeCommand.STOP
+ and self.next_command in self._NEEDS_STOP_FIRST
+ and game_frame is not None
+ and self._all_robots_clear(game_frame)
+ ):
+ logger.info("All robots clear — auto-advancing STOP → %s", self.next_command.name)
+ self.command = self.next_command
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ if self.command in self._BALL_PLACEMENT_COMMANDS:
+ self.next_command = self._post_ball_placement_command or RefereeCommand.NORMAL_START
+ self._post_ball_placement_command = None
+ self._advance4_ready_since = math.inf
+ elif self.command in self._DIRECT_FREE_COMMANDS:
+ self.next_command = RefereeCommand.NORMAL_START
+ self._advance3_ready_since = math.inf
+ elif self.command in self._PREPARE_KICKOFF_COMMANDS or self.command in self._PREPARE_PENALTY_COMMANDS:
+ self.next_command = RefereeCommand.NORMAL_START
+ self._prepare_entered_time = current_time
+ self._advance2_ready_since = math.inf
+ self._last_transition_time = current_time
+
+ # ----------------------------------------------------------------
+ # Auto-advance 2a: PREPARE_KICKOFF_* → NORMAL_START
+ # Fires after prepare_duration_seconds AND one attacker is inside
+ # the centre circle, sustained for _AUTO_ADVANCE_DELAY seconds.
+ # ----------------------------------------------------------------
+ elif self._auto_advance.prepare_kickoff_to_normal and self.command in self._PREPARE_KICKOFF_COMMANDS:
+ ready = (
+ (current_time - self._prepare_entered_time) >= self._prepare_duration_seconds
+ and game_frame is not None
+ and self._kicker_in_centre_circle(self.command, game_frame)
+ )
+ if ready:
+ if self._advance2_ready_since == math.inf:
+ self._advance2_ready_since = current_time
+ logger.debug("Advance 2 countdown started (%s)", self.command.name)
+ elif (current_time - self._advance2_ready_since) >= _AUTO_ADVANCE_DELAY:
+ logger.info(
+ "Kicker in centre circle — auto-advancing %s → NORMAL_START",
+ self.command.name,
+ )
+ self.command = RefereeCommand.NORMAL_START
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ self.next_command = None
+ self.status_message = None
+ self._normal_start_time = current_time
+ self._ball_pos_at_normal_start = (
+ (game_frame.ball.p.x, game_frame.ball.p.y) if game_frame.ball is not None else None
+ )
+ self._advance2_ready_since = math.inf
+ self._last_transition_time = current_time
+ else:
+ self._advance2_ready_since = math.inf
+
+ # ----------------------------------------------------------------
+ # Auto-advance 2b: PREPARE_PENALTY_* → NORMAL_START
+ # Fires after prepare_duration_seconds when the kicker reaches the
+ # penalty mark, sustained for _AUTO_ADVANCE_DELAY seconds.
+ # ----------------------------------------------------------------
+ elif self._auto_advance.prepare_penalty_to_normal and self.command in self._PREPARE_PENALTY_COMMANDS:
+ ready = (
+ (current_time - self._prepare_entered_time) >= self._prepare_duration_seconds
+ and game_frame is not None
+ and self._penalty_kicker_ready(self.command, game_frame)
+ )
+ if ready:
+ if self._advance2_ready_since == math.inf:
+ self._advance2_ready_since = current_time
+ logger.debug("Advance 2 countdown started (%s)", self.command.name)
+ elif (current_time - self._advance2_ready_since) >= _AUTO_ADVANCE_DELAY:
+ logger.info(
+ "Kicker at penalty mark — auto-advancing %s → NORMAL_START",
+ self.command.name,
+ )
+ self.command = RefereeCommand.NORMAL_START
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ self.next_command = None
+ self.status_message = None
+ self._normal_start_time = current_time
+ self._ball_pos_at_normal_start = (
+ (game_frame.ball.p.x, game_frame.ball.p.y) if game_frame.ball is not None else None
+ )
+ self._advance2_ready_since = math.inf
+ self._last_transition_time = current_time
+ else:
+ self._advance2_ready_since = math.inf
+
+ # ----------------------------------------------------------------
+ # Auto-advance 3: DIRECT_FREE_* → NORMAL_START
+ # Fires when the kicker is within _KICKER_READY_DIST of the ball
+ # AND all defending robots are ≥ _BALL_CLEAR_DIST away, sustained
+ # for _AUTO_ADVANCE_DELAY seconds.
+ # ----------------------------------------------------------------
+ elif self._auto_advance.direct_free_to_normal and self.command in self._DIRECT_FREE_COMMANDS:
+ ready = game_frame is not None and self._free_kick_ready(self.command, game_frame)
+ if ready:
+ if self._advance3_ready_since == math.inf:
+ self._advance3_ready_since = current_time
+ logger.debug("Advance 3 countdown started (%s)", self.command.name)
+ elif (current_time - self._advance3_ready_since) >= _AUTO_ADVANCE_DELAY:
+ logger.info("Free kick ready — auto-advancing %s → NORMAL_START", self.command.name)
+ self.command = RefereeCommand.NORMAL_START
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ self.next_command = None
+ self.status_message = None
+ self._normal_start_time = current_time
+ self._ball_pos_at_normal_start = (
+ (game_frame.ball.p.x, game_frame.ball.p.y) if game_frame.ball is not None else None
+ )
+ self._advance3_ready_since = math.inf
+ self._last_transition_time = current_time
+ else:
+ self._advance3_ready_since = math.inf
+
+ # ----------------------------------------------------------------
+ # Auto-advance 4: BALL_PLACEMENT_* → next_command
+ # Fires when ball reaches within _PLACEMENT_DONE_DIST of target,
+ # sustained for _AUTO_ADVANCE_DELAY seconds.
+ # ----------------------------------------------------------------
+ elif self._auto_advance.ball_placement_to_next and self.command in self._BALL_PLACEMENT_COMMANDS:
+ ready = self.next_command is not None and game_frame is not None and self._ball_placement_done(game_frame)
+ if ready:
+ if self._advance4_ready_since == math.inf:
+ self._advance4_ready_since = current_time
+ logger.debug("Advance 4 countdown started (%s)", self.command.name)
+ elif (current_time - self._advance4_ready_since) >= _AUTO_ADVANCE_DELAY:
+ logger.info(
+ "Ball placement complete — auto-advancing %s → %s",
+ self.command.name,
+ self.next_command.name,
+ )
+ completed_command = self.next_command
+ self.command = completed_command
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ if completed_command in self._DIRECT_FREE_COMMANDS:
+ self.next_command = RefereeCommand.NORMAL_START
+ self._advance3_ready_since = math.inf
+ elif (
+ completed_command in self._PREPARE_KICKOFF_COMMANDS
+ or completed_command in self._PREPARE_PENALTY_COMMANDS
+ ):
+ self.next_command = RefereeCommand.NORMAL_START
+ self._prepare_entered_time = current_time
+ self._advance2_ready_since = math.inf
+ else:
+ self.next_command = None
+ self._advance4_ready_since = math.inf
+ self.status_message = None
+ self._last_transition_time = current_time
+ else:
+ self._advance4_ready_since = math.inf
+
+ # ----------------------------------------------------------------
+ # Auto-advance 5: NORMAL_START → FORCE_START
+ # Fires after kickoff_timeout_seconds if the ball hasn't moved ≥5 cm.
+ # ----------------------------------------------------------------
+ elif (
+ self._auto_advance.normal_start_to_force
+ and self.command == RefereeCommand.NORMAL_START
+ and self._ball_pos_at_normal_start is not None
+ and (current_time - self._normal_start_time) >= self._kickoff_timeout_seconds
+ and game_frame is not None
+ and game_frame.ball is not None
+ and not self._ball_has_moved(game_frame)
+ ):
+ logger.info("Kickoff/free-kick timeout — auto-advancing NORMAL_START → FORCE_START")
+ self.command = RefereeCommand.FORCE_START
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ self.next_command = None
+ self.status_message = None
+ self._ball_pos_at_normal_start = None
+ self._last_transition_time = current_time
+
+ # ----------------------------------------------------------------
+ # Legacy force-start path: STOP → FORCE_START after goal
+ # ----------------------------------------------------------------
+ elif (
+ self._force_start_after_goal
+ and self.command == RefereeCommand.STOP
+ and self.next_command in self._PREPARE_KICKOFF_COMMANDS
+ and (current_time - self._stop_entered_time) >= self._stop_duration_seconds
+ ):
+ self.command = RefereeCommand.FORCE_START
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ self.next_command = None
+ self.status_message = None
+ self._last_transition_time = current_time
+ logger.info("Auto-advanced STOP → FORCE_START after goal (force-start profile mode)")
+
+ return self._generate_referee_data(current_time)
+
+ def _all_robots_clear(self, game_frame: "GameFrame") -> bool:
+ """Return True if every robot on both teams is ≥ _BALL_CLEAR_DIST from the ball."""
+ ball = game_frame.ball
+ if ball is None:
+ return True
+ bx, by = ball.p.x, ball.p.y
+ for r in list(game_frame.friendly_robots.values()) + list(game_frame.enemy_robots.values()):
+ if math.hypot(r.p.x - bx, r.p.y - by) < _BALL_CLEAR_DIST:
+ return False
+ return True
+
+ def _kicker_in_centre_circle(self, command: RefereeCommand, game_frame: "GameFrame") -> bool:
+ """Return True if at least one robot of the attacking team is inside the centre circle."""
+ r = self._geometry.center_circle_radius if self._geometry is not None else 0.5 # fallback for standalone use
+ kicking_is_yellow = command == RefereeCommand.PREPARE_KICKOFF_YELLOW
+ attackers = (
+ game_frame.friendly_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.enemy_robots
+ )
+ return any(math.hypot(robot.p.x, robot.p.y) <= r for robot in attackers.values())
+
+ def _penalty_kicker_ready(self, command: RefereeCommand, game_frame: "GameFrame") -> bool:
+ """Return True when an attacking robot is within the ready radius of the penalty mark."""
+ kicking_is_yellow = command == RefereeCommand.PREPARE_PENALTY_YELLOW
+ attackers = (
+ game_frame.friendly_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.enemy_robots
+ )
+ if not attackers:
+ return False
+
+ half_length = (
+ self._geometry.half_length if self._geometry is not None else STANDARD_FIELD_DIMS.full_field_half_length
+ )
+ yellow_is_right = game_frame.my_team_is_right == game_frame.my_team_is_yellow
+ if kicking_is_yellow:
+ goal_sign = -1.0 if yellow_is_right else 1.0
+ else:
+ goal_sign = 1.0 if yellow_is_right else -1.0
+ penalty_mark_x = goal_sign * half_length * 0.5
+
+ closest = min(math.hypot(robot.p.x - penalty_mark_x, robot.p.y) for robot in attackers.values())
+ return closest <= _KICKER_READY_DIST
+
+ def _free_kick_ready(self, command: RefereeCommand, game_frame: "GameFrame") -> bool:
+ """Return True when a free kick is ready to start:
+ - The kicker (closest attacker to ball) is within _KICKER_READY_DIST of the ball.
+ - All defending robots are ≥ _BALL_CLEAR_DIST from the ball.
+ """
+ ball = game_frame.ball
+ if ball is None:
+ return False
+ bx, by = ball.p.x, ball.p.y
+
+ kicking_is_yellow = command == RefereeCommand.DIRECT_FREE_YELLOW
+ attackers = (
+ game_frame.friendly_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.enemy_robots
+ )
+ defenders = (
+ game_frame.enemy_robots if kicking_is_yellow == game_frame.my_team_is_yellow else game_frame.friendly_robots
+ )
+
+ # Check defending robots are all clear.
+ if any(math.hypot(r.p.x - bx, r.p.y - by) < _BALL_CLEAR_DIST for r in defenders.values()):
+ return False
+
+ # Check at least one attacker is close to the ball (kicker in position).
+ if not attackers:
+ return False
+ closest = min(math.hypot(r.p.x - bx, r.p.y - by) for r in attackers.values())
+ return closest <= _KICKER_READY_DIST
+
+ def _ball_has_moved(self, game_frame: "GameFrame") -> bool:
+ """Return True if the ball has moved ≥ 0.05 m since NORMAL_START."""
+ if self._ball_pos_at_normal_start is None or game_frame.ball is None:
+ return False
+ ox, oy = self._ball_pos_at_normal_start
+ return math.hypot(game_frame.ball.p.x - ox, game_frame.ball.p.y - oy) >= 0.05
+
+ def _ball_placement_done(self, game_frame: "GameFrame") -> bool:
+ """Return True when the ball is within _PLACEMENT_DONE_DIST of the placement target."""
+ if self.ball_placement_target is None or game_frame.ball is None:
+ return False
+ tx, ty = self.ball_placement_target
+ return math.hypot(game_frame.ball.p.x - tx, game_frame.ball.p.y - ty) <= _PLACEMENT_DONE_DIST
+
+ # Commands that require robots to clear the ball before they take effect.
+ # In a real match these are always preceded by STOP.
+ _NEEDS_STOP_FIRST = frozenset(
+ {
+ RefereeCommand.PREPARE_KICKOFF_YELLOW,
+ RefereeCommand.PREPARE_KICKOFF_BLUE,
+ RefereeCommand.DIRECT_FREE_YELLOW,
+ RefereeCommand.DIRECT_FREE_BLUE,
+ RefereeCommand.PREPARE_PENALTY_YELLOW,
+ RefereeCommand.PREPARE_PENALTY_BLUE,
+ RefereeCommand.BALL_PLACEMENT_YELLOW,
+ RefereeCommand.BALL_PLACEMENT_BLUE,
+ }
+ )
+
+ @staticmethod
+ def _ball_placement_command_for(restart_command: Optional[RefereeCommand]) -> Optional[RefereeCommand]:
+ """Return the ball-placement command for the team that owns a restart."""
+ if restart_command in (
+ RefereeCommand.PREPARE_KICKOFF_YELLOW,
+ RefereeCommand.PREPARE_PENALTY_YELLOW,
+ RefereeCommand.DIRECT_FREE_YELLOW,
+ RefereeCommand.INDIRECT_FREE_YELLOW,
+ ):
+ return RefereeCommand.BALL_PLACEMENT_YELLOW
+ if restart_command in (
+ RefereeCommand.PREPARE_KICKOFF_BLUE,
+ RefereeCommand.PREPARE_PENALTY_BLUE,
+ RefereeCommand.DIRECT_FREE_BLUE,
+ RefereeCommand.INDIRECT_FREE_BLUE,
+ ):
+ return RefereeCommand.BALL_PLACEMENT_BLUE
+ return None
+
+ def seed_clock(self, timestamp: float) -> None:
+ """Align all internal timers to *timestamp*.
+
+ Resets every timer that was initialised before the real vision clock
+ was known (construction time) so that durations are measured from the
+ correct timebase. Safe to call only once, immediately after the first
+ valid game frame is available.
+ """
+ if self.stage_start_time is None:
+ self.stage_start_time = timestamp
+ self.command_timestamp = timestamp
+ # Push all "entered-at" sentinels to the new timebase so that
+ # cooldowns / durations are not immediately satisfied.
+ self._last_transition_time = timestamp - _TRANSITION_COOLDOWN # allow transitions immediately
+ self._stop_entered_time = timestamp - self._stop_duration_seconds # won't auto-advance yet
+ self._prepare_entered_time = timestamp - self._prepare_duration_seconds # same
+
+ def set_command(self, command: RefereeCommand, timestamp: float) -> None:
+ """Manual override — for operator use or test scripting.
+
+ If *command* is a set-piece command (kickoff, free kick, penalty,
+ ball placement) and the game is not already in STOP or HALT, a STOP
+ is issued first and the requested command is stored as ``next_command``
+ so the operator (or a script) can advance to it after robots have
+ cleared the ball. This mirrors real-match game-controller behaviour
+ and prevents robots from receiving a PREPARE_KICKOFF while they are
+ still within the keep-out zone around the ball.
+ """
+ _ALREADY_STOPPED = (RefereeCommand.STOP, RefereeCommand.HALT)
+
+ if command in self._NEEDS_STOP_FIRST and self.command not in _ALREADY_STOPPED:
+ # Insert STOP; park the real command as next_command.
+ logger.info("Inserting STOP before %s so robots can clear the ball", command.name)
+ self.command = RefereeCommand.STOP
+ self.command_counter += 1
+ self.command_timestamp = timestamp
+ self.next_command = command
+ self._post_ball_placement_command = None
+ self.status_message = None
+ self._stop_entered_time = timestamp
+ return
+
+ # NORMAL_START while in STOP with a pending set-piece: advance to the
+ # set-piece first so robots can form up. Auto-advance will then issue
+ # NORMAL_START after prepare_duration_seconds.
+ if (
+ command == RefereeCommand.NORMAL_START
+ and self.command == RefereeCommand.STOP
+ and self.next_command in self._NEEDS_STOP_FIRST
+ ):
+ logger.info(
+ "Manually advancing STOP → %s (auto NORMAL_START in %.1f s)",
+ self.next_command.name,
+ self._prepare_duration_seconds,
+ )
+ self.command = self.next_command
+ self.command_counter += 1
+ self.command_timestamp = timestamp
+ if self.command in self._BALL_PLACEMENT_COMMANDS:
+ self.next_command = self._post_ball_placement_command or RefereeCommand.NORMAL_START
+ self._post_ball_placement_command = None
+ self._advance4_ready_since = math.inf
+ else:
+ self.next_command = RefereeCommand.NORMAL_START
+ self.status_message = None
+ self._prepare_entered_time = timestamp
+ return
+
+ self.command = command
+ self.command_counter += 1
+ self.command_timestamp = timestamp
+ self._post_ball_placement_command = None
+ self.status_message = None
+ self._advance2_ready_since = math.inf
+ self._advance3_ready_since = math.inf
+ self._advance4_ready_since = math.inf
+
+ if command in (RefereeCommand.STOP, RefereeCommand.HALT):
+ self._stop_entered_time = timestamp
+ elif command in (
+ RefereeCommand.PREPARE_KICKOFF_YELLOW,
+ RefereeCommand.PREPARE_KICKOFF_BLUE,
+ RefereeCommand.PREPARE_PENALTY_YELLOW,
+ RefereeCommand.PREPARE_PENALTY_BLUE,
+ ):
+ self._prepare_entered_time = timestamp
+
+ # Advance PRE stages to their active counterpart when play begins.
+ _PRE_TO_ACTIVE = {
+ Stage.NORMAL_FIRST_HALF_PRE: Stage.NORMAL_FIRST_HALF,
+ Stage.NORMAL_SECOND_HALF_PRE: Stage.NORMAL_SECOND_HALF,
+ Stage.EXTRA_FIRST_HALF_PRE: Stage.EXTRA_FIRST_HALF,
+ Stage.EXTRA_SECOND_HALF_PRE: Stage.EXTRA_SECOND_HALF,
+ }
+ if command in (RefereeCommand.NORMAL_START, RefereeCommand.FORCE_START):
+ active = _PRE_TO_ACTIVE.get(self.stage)
+ if active is not None:
+ self.advance_stage(active, timestamp)
+
+ logger.info("Referee command manually set to: %s", command.name)
+
+ def force_command(
+ self,
+ command: RefereeCommand,
+ timestamp: float,
+ ball_placement_target: Optional[tuple[float, float]] = None,
+ ) -> None:
+ """Directly set the command, bypassing the STOP-first guard.
+
+ For god-mode / test use only — skips the normal safety interlock that
+ inserts STOP before set-piece commands.
+ """
+ self.command = command
+ self.command_counter += 1
+ self.command_timestamp = timestamp
+ self._post_ball_placement_command = None
+ self.status_message = None
+ self._advance2_ready_since = math.inf
+ self._advance3_ready_since = math.inf
+ self._advance4_ready_since = math.inf
+ if ball_placement_target is not None:
+ self.ball_placement_target = ball_placement_target
+ if command in self._BALL_PLACEMENT_COMMANDS:
+ # Auto-advance 4 requires next_command to be set.
+ self.next_command = RefereeCommand.NORMAL_START
+ else:
+ self.next_command = None
+ logger.info("Referee command force-set to: %s", command.name)
+
+ def advance_stage(self, new_stage: Stage, timestamp: float) -> None:
+ """Advance the game stage."""
+ logger.info("Stage %s → %s", self.stage.name, new_stage.name)
+ self.stage = new_stage
+ self.stage_start_time = timestamp
+
+ # ------------------------------------------------------------------
+ # Internal helpers
+ # ------------------------------------------------------------------
+
+ def _can_transition(self, current_time: float) -> bool:
+ return (current_time - self._last_transition_time) >= _TRANSITION_COOLDOWN
+
+ def _apply_violation(self, violation: RuleViolation, current_time: float) -> None:
+ """Update state in response to a detected violation."""
+ if violation.rule_name == "goal":
+ self._handle_goal(violation, current_time)
+ else:
+ self._handle_foul(violation, current_time)
+
+ self._last_transition_time = current_time
+
+ def _handle_goal(self, violation: RuleViolation, current_time: float) -> None:
+ # Determine scorer from next_command (loser gets the kickoff).
+ if violation.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE:
+ # Blue gets kickoff → yellow scored.
+ self.yellow_team.increment_score()
+ logger.info(
+ "Goal by Yellow! Score: Yellow %d – Blue %d",
+ self.yellow_team.score,
+ self.blue_team.score,
+ )
+ elif violation.next_command == RefereeCommand.PREPARE_KICKOFF_YELLOW:
+ # Yellow gets kickoff → blue scored.
+ self.blue_team.increment_score()
+ logger.info(
+ "Goal by Blue! Score: Yellow %d – Blue %d",
+ self.yellow_team.score,
+ self.blue_team.score,
+ )
+
+ self.command = RefereeCommand.STOP
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ self.next_command = self._ball_placement_command_for(violation.next_command) or violation.next_command
+ self._post_ball_placement_command = (
+ violation.next_command if self.next_command in self._BALL_PLACEMENT_COMMANDS else None
+ )
+ self.ball_placement_target = (0.0, 0.0)
+ self.status_message = violation.status_message
+ self._stop_entered_time = current_time
+
+ def _handle_foul(self, violation: RuleViolation, current_time: float) -> None:
+ self.command = violation.suggested_command
+ self.command_counter += 1
+ self.command_timestamp = current_time
+ placement_command = (
+ self._ball_placement_command_for(violation.next_command)
+ if violation.designated_position is not None
+ else None
+ )
+ self.next_command = placement_command or violation.next_command
+ self._post_ball_placement_command = violation.next_command if placement_command is not None else None
+ self.ball_placement_target = violation.designated_position
+ self.status_message = violation.status_message
+ logger.info(
+ "Foul detected: %s → %s (next: %s)",
+ violation.rule_name,
+ violation.suggested_command.name,
+ violation.next_command.name if violation.next_command else "None",
+ )
+
+ def _generate_referee_data(self, current_time: float) -> RefereeData:
+ stage_time_left = max(0.0, self.stage_duration - (current_time - self.stage_start_time))
+ return RefereeData(
+ source_identifier="custom_referee",
+ time_sent=current_time,
+ time_received=current_time,
+ referee_command=self.command,
+ referee_command_timestamp=self.command_timestamp,
+ stage=self.stage,
+ stage_time_left=stage_time_left,
+ blue_team=copy.copy(self.blue_team),
+ yellow_team=copy.copy(self.yellow_team),
+ designated_position=self.ball_placement_target,
+ blue_team_on_positive_half=None,
+ next_command=self.next_command,
+ current_action_time_remaining=None,
+ status_message=self.status_message,
+ )
diff --git a/utama_core/data_processing/predictors/position.py b/utama_core/data_processing/predictors/position.py
index 3ce5ae65..8a1162b4 100644
--- a/utama_core/data_processing/predictors/position.py
+++ b/utama_core/data_processing/predictors/position.py
@@ -3,94 +3,24 @@
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.game import Game
-### Incomplete Code Snippet ###
-# def predict_frame_after(self, t: float) -> VisionData:
-# yellow_pos = [
-# self.predict_object_pos_after(t, RobotEntity(Colour.YELLOW, i))
-# for i in range(len(self.get_robots_pos(True)))
-# ]
-# blue_pos = [
-# self.predict_object_pos_after(t, RobotEntity(Colour.BLUE, i))
-# for i in range(len(self.get_robots_pos(False)))
-# ]
-# ball_pos = self.predict_object_pos_after(t, Ball)
-# if ball_pos is None or None in yellow_pos or None in blue_pos:
-# return None
-# else:
-# return VisionData(
-# self._records[-1].ts + t,
-# list(map(lambda pos: VisionRobotData(pos[0], pos[1], 0), yellow_pos)),
-# list(map(lambda pos: VisionRobotData(pos[0], pos[1], 0), blue_pos)),
-# [VisionBallData(ball_pos[0], ball_pos[1], 0, 1)], # TODO : Support z axis
-# )
+def predict_ball_pos_at_x(game: Game, x: float) -> Optional[Vector2D]:
+ """Predict where the ball will cross a given x-coordinate.
-# ### General Object Position Prediction ###
-# def predict_object_pos_after(self, t: float, object: GameObject) -> Optional[tuple]:
-# # If t is after the object has stopped we return the position at which object stopped.
-# sx = 0
-# sy = 0
-
-# acceleration = self.get_object_acceleration(object)
-
-# if acceleration is None:
-# return None
-
-# ax, ay = acceleration
-# vels = self.get_object_velocity(object)
-
-# if vels is None:
-# ux, uy = None, None
-# else:
-# ux, uy = vels
-
-# if object is Ball:
-# ball = self.get_ball_pos()
-# start_x, start_y = ball[0].x, ball[0].y
-# else:
-# posn = self._get_object_position_at_frame(len(self._records) - 1, object)
-# start_x, start_y = posn.x, posn.y
-
-# if ax and ux:
-# sx = self._calculate_displacement(ux, ax, t)
-
-# if ay and uy:
-# sy = self._calculate_displacement(uy, ay, t)
-
-# return (
-# start_x + sx,
-# start_y + sy,
-# ) # TODO: Doesn't take into account spin / angular vel
-
-# def _calculate_displacement(self, u, a, t) -> float:
-# if a == 0: # Handle zero acceleration case
-# return u * t
-# else:
-# stop_time = -u / a
-# if stop_time < 0:
-# stop_time = float("inf")
-# effective_time = min(t, stop_time)
-# displacement = (u * effective_time) + (0.5 * a * effective_time**2)
-# logger.debug(
-# f"Displacement: {displacement} for time: {effective_time}, stop time: {stop_time}"
-# )
-# return displacement
-
-
-def predict_ball_pos_at_x(game: Game, x: float) -> Optional[tuple]:
+ Returns None if the ball is not moving toward ``x`` (stationary along x,
+ or moving away). Callers should fall back to their own positioning logic
+ (e.g. goalkeep uses shadow-based ``stop_y``).
+ """
vel = game.ball.v.to_2d()
- if not vel or not vel[0] or not vel[1]:
- return None
+ if abs(vel.x) < 1e-12:
+ return None # Ball not moving along x — cannot predict arrival
- ux, uy = vel
- pos = game.ball.p
- bx = pos.x
- by = pos.y
+ pos = game.ball.p.to_2d()
+ t = (x - pos.x) / vel.x
- if uy == 0:
- return (bx, by)
+ if t < 0:
+ return None # Ball moving away from target x
- t = (x - bx) / ux
- y = by + uy * t
+ y = pos.y + vel.y * t
return Vector2D(x, y)
diff --git a/utama_core/motion_planning/src/controllers/fastpathplanning.py b/utama_core/motion_planning/src/controllers/fastpathplanning.py
index 2807f21b..d4f07342 100644
--- a/utama_core/motion_planning/src/controllers/fastpathplanning.py
+++ b/utama_core/motion_planning/src/controllers/fastpathplanning.py
@@ -1,12 +1,6 @@
-from abc import ABC, abstractmethod
-
-import numpy as np
-
from utama_core.config.enums import Mode
-from utama_core.config.physical_constants import ROBOT_RADIUS
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.game import Game
-from utama_core.entities.game.field import Field
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.motion_planning.src.fastpathplanning.planner import FastPathPlanner
from utama_core.motion_planning.src.pid.pid import get_pids
@@ -15,10 +9,9 @@
class FastPathPlanningController(MotionController):
def __init__(self, mode: Mode, rsim_env: SSLStandardEnv | None = None):
- self.mode = mode
- self.rsim_env: SSLStandardEnv | None = rsim_env
+ super().__init__(mode, rsim_env)
self.pid_oren, self.pid_trans = get_pids(mode)
- self.fpp = FastPathPlanner(env=self.rsim_env)
+ self.fpp = FastPathPlanner(env=rsim_env)
def calculate(
self,
diff --git a/utama_core/motion_planning/src/fastpathplanning/planner.py b/utama_core/motion_planning/src/fastpathplanning/planner.py
index 84079c17..0ec7424b 100644
--- a/utama_core/motion_planning/src/fastpathplanning/planner.py
+++ b/utama_core/motion_planning/src/fastpathplanning/planner.py
@@ -21,7 +21,7 @@
class FastPathPlanner:
- def __init__(self, env: SSLStandardEnv):
+ def __init__(self, env: SSLStandardEnv | None):
self._env = env
self.config = config
self.OBSTACLE_CLEARANCE = self.config.OBSTACLE_CLEARANCE
@@ -62,7 +62,7 @@ def _get_obstacles(
obstacle_list.append(obstacle_segment)
- # DRAWING: Show the projected velocity line in Red
+ # DRAWING: Show the projected velocity line in Red when an RSim renderer is available.
if self._env is not None:
self._env.draw_line(obstacle_segment, color="Red")
@@ -271,7 +271,7 @@ def _path_to(
# 4. Plan geometric path
final_trajectory, _ = self.check_segment((our_pos, safe_target), obstacles, 0, safe_target, field_bounds)
- # 5. Draw the resulting safe path segments
+ # 5. Draw the resulting safe path segments when an RSim renderer is available.
if self._env is not None:
for i in final_trajectory:
self._env.draw_line(i)
diff --git a/utama_core/rsoccer_simulator/src/Simulators/robosim/robosim_wrapper.py b/utama_core/rsoccer_simulator/src/Simulators/robosim/robosim_wrapper.py
index becd4787..321cb560 100644
--- a/utama_core/rsoccer_simulator/src/Simulators/robosim/robosim_wrapper.py
+++ b/utama_core/rsoccer_simulator/src/Simulators/robosim/robosim_wrapper.py
@@ -50,6 +50,7 @@ def __init__(self, sim_type, n_blue, n_yellow, field_type, time_step_ms):
text=True,
bufsize=1,
env=env,
+ cwd=project_root,
)
def step(self, commands: np.ndarray):
diff --git a/utama_core/rsoccer_simulator/src/ssl/ssl_gym_base.py b/utama_core/rsoccer_simulator/src/ssl/ssl_gym_base.py
index dec6ad79..ff579d12 100644
--- a/utama_core/rsoccer_simulator/src/ssl/ssl_gym_base.py
+++ b/utama_core/rsoccer_simulator/src/ssl/ssl_gym_base.py
@@ -4,6 +4,7 @@
# - To create your wrapper from env to communcation, use inherit from this class!
"""
+import random
from typing import List, Optional
import numpy as np
@@ -99,6 +100,10 @@ def step(self, action):
return observation, reward, done, False, {}
def reset(self, *, seed=None, options=None):
+ if seed is not None:
+ random.seed(seed)
+ np.random.seed(seed)
+
self.steps = 0
self.last_frame = None
self.sent_commands = None
diff --git a/utama_core/run/game_gater.py b/utama_core/run/game_gater.py
index 0634805f..96174001 100644
--- a/utama_core/run/game_gater.py
+++ b/utama_core/run/game_gater.py
@@ -119,20 +119,4 @@ def _add_frame(my_game_frame: GameFrame, opp_game_frame: GameFrame) -> Tuple[Gam
if len(my_game_frame.enemy_robots) > exp_enemy:
raise ValueError(f"Too many enemy robots: {len(my_game_frame.enemy_robots)} > {exp_enemy}")
- # Validate that the vision→cmd mapping keys exactly match the observed robot IDs.
- # Done here (after first valid frame) because actual vision IDs are non-contiguous
- # in some deployments and are not known until a real frame arrives.
- if my_vision_to_cmd_mapping is not None:
- GameGater._validate_mapping_covers_game_frame(
- my_vision_to_cmd_mapping,
- set(my_game_frame.friendly_robots.keys()),
- "friendly",
- )
- if opp_vision_to_cmd_mapping is not None and opp_game_frame is not None:
- GameGater._validate_mapping_covers_game_frame(
- opp_vision_to_cmd_mapping,
- set(opp_game_frame.friendly_robots.keys()),
- "opponent",
- )
-
return my_game_frame, opp_game_frame
diff --git a/utama_core/run/strategy_runner.py b/utama_core/run/strategy_runner.py
index a882a2c4..ae1c761a 100644
--- a/utama_core/run/strategy_runner.py
+++ b/utama_core/run/strategy_runner.py
@@ -232,8 +232,10 @@ def __init__(
# Derive per-color roster allowlists from the validated mappings (real mode only).
# Any robot ID seen by vision that is not in the allowlist is silently dropped so that
# stray detections from robots not in play never pollute the game state.
+ # Blue filtering is only applied when there is an opponent strategy — in single-team
+ # mode blue robots are tracked as enemies and must not be filtered out.
_allowed_yellow = frozenset(self.yellow_vision_to_cmd_mapping) or None
- _allowed_blue = frozenset(self.blue_vision_to_cmd_mapping) or None
+ _allowed_blue = (frozenset(self.blue_vision_to_cmd_mapping) or None) if opp_strategy is not None else None
self.my, self.opp = self._setup_sides_data(
strategy,
@@ -395,8 +397,16 @@ def _check_no_cmd_duplicate_if_transmission_sharing(
seen.add(v)
def _handle_sigint(self, sig, frame):
+ if self._stop_event.is_set():
+ signal.default_int_handler(sig, frame)
self._stop_event.set()
- signal.default_int_handler(sig, frame)
+ self._stop_fps_live()
+ print("\nStopping gracefully. Press Ctrl+C again to force quit.", flush=True)
+
+ def _stop_fps_live(self):
+ if self._fps_live:
+ self._fps_live.stop()
+ self._fps_live = None
def _load_mode(self, mode_str: str) -> Mode:
"""Convert a mode string to a Mode enum value.
@@ -988,8 +998,7 @@ def close(self, stop_command_repeat: int = 20):
self.replay_writer.close()
if self.rsim_env:
self.rsim_env.close()
- if self._fps_live:
- self._fps_live.stop()
+ self._stop_fps_live()
def run_test(
self,
@@ -1074,7 +1083,7 @@ def run(self):
"""Run the main loop, stepping the game until interrupted.
If an RSim environment is present, it ensures rendering is on. The loop
- continues until a KeyboardInterrupt is received, after which resources
+ continues until interrupted via SIGINT stop event, after which resources
(such as replay writer and rsim env) are closed.
"""
signal.signal(signal.SIGINT, self._handle_sigint)
@@ -1101,6 +1110,16 @@ def run(self):
finally:
self.close()
+ def step_once(self):
+ """Advance the runner by one strategy/simulation tick.
+
+ This is a public wrapper around the internal single-step loop for
+ deterministic harnesses and scenario runners that need to apply events
+ or assertions between ticks without taking ownership of the runner's
+ game-loop internals.
+ """
+ self._run_step()
+
def _run_step(self):
"""Perform one tick of the overall game loop.
diff --git a/utama_core/skills/src/defend_parameter.py b/utama_core/skills/src/defend_parameter.py
index bd472e84..995ec1d1 100644
--- a/utama_core/skills/src/defend_parameter.py
+++ b/utama_core/skills/src/defend_parameter.py
@@ -1,14 +1,112 @@
-import math
from typing import Optional
-import numpy as np
-
+from utama_core.config.physical_constants import BALL_RADIUS, ROBOT_RADIUS
from utama_core.data_processing.predictors.position import predict_ball_pos_at_x
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.game import Game
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.rsoccer_simulator.src.ssl.envs.standard_ssl import SSLStandardEnv
from utama_core.skills.src.go_to_point import go_to_point
+from utama_core.skills.src.utils.defense_utils import (
+ clamp_y,
+ intersection_with_x_line,
+ single_defender_stop_y,
+)
+
+
+def _defender_target_y(ball_pos, keeper_x, goal_frame_y, edge_offset, defender_x, defense_half_width):
+ """Compute the defender y-position for a given goal_frame_y assignment."""
+ offset = edge_offset if goal_frame_y >= 0 else -edge_offset
+ goal_point_y = goal_frame_y + offset
+
+ dx = keeper_x - ball_pos.x
+ if abs(dx) > 1e-12:
+ t = (defender_x - ball_pos.x) / dx
+ t = max(0.0, min(1.0, t))
+ target_y = ball_pos.y + t * (goal_point_y - ball_pos.y)
+ else:
+ target_y = goal_frame_y
+
+ return max(-defense_half_width, min(defense_half_width, target_y))
+
+
+def _shadow_width(ball_pos, defender_pos, keeper_x, post_limit, edge_offset):
+ """Width of the shadow interval a defender casts on the keeper line."""
+ _, yy_top = intersection_with_x_line(
+ (ball_pos.x, ball_pos.y),
+ (defender_pos.x, defender_pos.y + edge_offset),
+ keeper_x,
+ post_limit,
+ )
+ _, yy_bottom = intersection_with_x_line(
+ (ball_pos.x, ball_pos.y),
+ (defender_pos.x, defender_pos.y - edge_offset),
+ keeper_x,
+ post_limit,
+ )
+ return abs(yy_top - yy_bottom)
+
+
+def _choose_defender_side(
+ game,
+ ball_pos,
+ keeper_x,
+ post_limit,
+ edge_offset,
+ defender_x,
+ defense_half_width,
+ robot_id,
+):
+ """For a single defender (2-robot team), dynamically pick the better post side."""
+ # Keeper reference: predicted intercept > current keeper y > centre
+ keeper_ref = 0.0
+ try:
+ predicted = predict_ball_pos_at_x(game, keeper_x)
+ if predicted is not None:
+ keeper_ref = clamp_y(predicted.y, post_limit)
+ else:
+ keeper_ref = clamp_y(game.friendly_robots[0].p.y, post_limit)
+ except (IndexError, KeyError, AttributeError):
+ pass
+
+ candidates = [post_limit, -post_limit]
+ best_side = post_limit if robot_id == 1 else -post_limit
+ best_score = None
+
+ for side in candidates:
+ target_y = _defender_target_y(
+ ball_pos,
+ keeper_x,
+ side,
+ edge_offset,
+ defender_x,
+ defense_half_width,
+ )
+ defender_pos = Vector2D(defender_x, target_y)
+ stop_y = single_defender_stop_y(
+ ball_pos,
+ defender_pos,
+ keeper_x,
+ post_limit,
+ edge_offset,
+ )
+ dist = abs(stop_y - keeper_ref)
+ shadow_w = _shadow_width(
+ ball_pos,
+ defender_pos,
+ keeper_x,
+ post_limit,
+ edge_offset,
+ )
+ # Lower is better: closest keeper stop_y, then largest shadow, then id convention
+ id_tiebreak = 0 if (robot_id == 1) == (side > 0) else 1
+ score = (dist, -shadow_w, id_tiebreak)
+
+ if best_score is None or score < best_score:
+ best_score = score
+ best_side = side
+
+ return best_side
def defend_parameter(
@@ -16,93 +114,44 @@ def defend_parameter(
motion_controller: MotionController,
robot_id: int,
env: Optional[SSLStandardEnv] = None,
+ goal_frame_y: Optional[float] = None,
):
- defenseing_friendly = game.friendly_robots[robot_id]
- vel = game.ball.v.to_2d()
- if len(game.friendly_robots) > 2:
- if game.ball.p.y >= -0.5 and robot_id == 1:
- target_pos = [3.0 if game.my_team_is_right else -3.0, -1.2]
- return go_to_point(
- game,
- motion_controller,
- robot_id,
- Vector2D(target_pos[0], target_pos[1]),
- )
- elif game.ball.p.y < -0.5 and robot_id == 2:
- target_pos = [3.0 if game.my_team_is_right else -3.0, 1.2]
- return go_to_point(
+ ball_pos = game.ball.p.to_2d()
+
+ goal_x = game.field.my_goal_line[0][0]
+ goal_half_width = game.field.half_goal_width
+ defense_area = game.field.my_defense_area
+ defense_front_x = float(defense_area[1][0])
+ defender_x = defense_front_x + (ROBOT_RADIUS if not game.my_team_is_right else -ROBOT_RADIUS)
+ defense_half_width = abs(float(defense_area[0][1]))
+
+ keeper_x = goal_x + (ROBOT_RADIUS if not game.my_team_is_right else -ROBOT_RADIUS)
+ post_limit = goal_half_width - ROBOT_RADIUS
+ edge_offset = BALL_RADIUS + ROBOT_RADIUS
+
+ if goal_frame_y is None:
+ if len(game.friendly_robots) == 2:
+ goal_frame_y = _choose_defender_side(
game,
- motion_controller,
+ ball_pos,
+ keeper_x,
+ post_limit,
+ edge_offset,
+ defender_x,
+ defense_half_width,
robot_id,
- Vector2D(target_pos[0], target_pos[1]),
)
- if robot_id == 1:
- goal_frame = 0.5
- else:
- goal_frame = -0.5
-
- def positions_to_defend_parameter(x2, y2):
- x1, y1 = game.ball.p.x, game.ball.p.y
- x3, y3 = defenseing_friendly.p.x, defenseing_friendly.p.y
- t = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / ((x2 - x1) ** 2 + (y2 - y1) ** 2)
- x4 = x1 + t * (x2 - x1)
- y4 = y1 + t * (y2 - y1)
-
- def cal_xy5(xa, ya, xb, yb, w, x4, y4):
- x5 = xa - w
- dx = xb - xa
- if abs(dx) < 1e-12:
- return x4, y4
- t = (x5 - xa) / dx
- y5 = ya + t * (yb - ya)
- return x5, y5
-
- def distance(x1, y1, x2, y2):
- return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
-
- if abs(x2 - x4) < 1 and -1 < y4 < 1:
- hori_x, hori_y = cal_xy5(x2, y2, x1, y1, 1.0, x4, y4)
- ver_x, ver_y = cal_xy5(y2, x2, y1, x1, 2.0, x4, y4)
- if distance(x4, y4, hori_x, hori_y) < distance(x4, y4, ver_x, ver_y):
- x4, y4 = hori_x, hori_y
- else:
- x4, y4 = ver_y, ver_x
-
- return x3, y3, x4, y4
-
- ball_y_at_baseline = predict_ball_pos_at_x(game, 4.5 if game.my_team_is_right else -4.5)
- ball_y_at_robo = predict_ball_pos_at_x(game, defenseing_friendly.p.x)
- if (
- vel[0] ** 2 + vel[1] ** 2 > 0.05
- and (ball_y_at_baseline is not None and ball_y_at_baseline[1] < 0.5 and ball_y_at_baseline[1] > -0.5)
- and (ball_y_at_robo is not None and abs(ball_y_at_robo[1] - defenseing_friendly.p.y) > 0.1)
- ):
- x2, y2 = 4.5 if game.my_team_is_right else -4.5, (goal_frame + 0.2 if robot_id == 1 else goal_frame - 0.2)
- x3, y3, x4, y4 = positions_to_defend_parameter(x2, y2)
- target_pos = np.array([x4, y4])
-
- else:
- robot_rad = 0.09
- x2, y2 = 4.5 if game.my_team_is_right else -4.5, -goal_frame
- x3, y3, x4, y4 = positions_to_defend_parameter(x2, y2)
- vec_to_target = np.array(
- [
- x4 - x3,
- y4 + y3,
- ]
- )
- dist_to_target = np.linalg.norm(vec_to_target)
-
- if dist_to_target > 0:
- vec_dir = vec_to_target / dist_to_target
else:
- vec_dir = np.array([0.0, 0.0])
- target_pos = np.array([x4, y4]) - vec_dir * robot_rad
-
- return go_to_point(
- game,
- motion_controller,
- robot_id,
- Vector2D(target_pos[0], target_pos[1]),
- dribbling=True,
+ goal_frame_y = post_limit if robot_id == 1 else -post_limit
+
+ target_y = _defender_target_y(
+ ball_pos,
+ keeper_x,
+ goal_frame_y,
+ edge_offset,
+ defender_x,
+ defense_half_width,
)
+ target = Vector2D(defender_x, target_y)
+
+ return go_to_point(game, motion_controller, robot_id, target, dribbling=True)
diff --git a/utama_core/skills/src/go_to_ball.py b/utama_core/skills/src/go_to_ball.py
index 51b68336..b9ad93bd 100644
--- a/utama_core/skills/src/go_to_ball.py
+++ b/utama_core/skills/src/go_to_ball.py
@@ -1,3 +1,5 @@
+import math
+
from utama_core.entities.data.command import RobotCommand
from utama_core.entities.game import Game
from utama_core.motion_planning.src.common.motion_controller import MotionController
@@ -14,7 +16,8 @@ def go_to_ball(
ball = game.ball.p.to_2d()
robot = game.friendly_robots[robot_id].p
- target_oren = robot.angle_to(ball)
+ # Kicker/dribbler is on the back of the robot; approach with back facing ball.
+ target_oren = (robot.angle_to(ball) + math.pi) % (2 * math.pi) - math.pi
if dribble_when_near:
distance = robot.distance_to(ball)
diff --git a/utama_core/skills/src/goalkeep.py b/utama_core/skills/src/goalkeep.py
index 6b5ac57e..ce182c8e 100644
--- a/utama_core/skills/src/goalkeep.py
+++ b/utama_core/skills/src/goalkeep.py
@@ -1,7 +1,5 @@
from typing import Optional
-import numpy as np
-
from utama_core.config.physical_constants import BALL_RADIUS, ROBOT_RADIUS
from utama_core.data_processing.predictors.position import predict_ball_pos_at_x
from utama_core.entities.data.vector import Vector2D
@@ -9,7 +7,11 @@
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.rsoccer_simulator.src.ssl.envs.standard_ssl import SSLStandardEnv
from utama_core.skills.src.go_to_point import go_to_point
-from utama_core.skills.src.utils.move_utils import face_ball, move
+from utama_core.skills.src.utils.defense_utils import (
+ clamp_y,
+ intersection_with_x_line,
+ single_defender_stop_y,
+)
# TODO: instead of checking number of friendly, should check roles
@@ -20,110 +22,78 @@ def goalkeep(
robot_id: int,
env: Optional[SSLStandardEnv] = None,
):
- EDGE_OFFSET = BALL_RADIUS + ROBOT_RADIUS
+ if game.ball is None:
+ return None
+
+ edge_offset = BALL_RADIUS + ROBOT_RADIUS
goal_x = game.field.my_goal_line[0][0]
- half_goal_width = game.field.half_goal_width
- target = predict_ball_pos_at_x(game, goal_x)
+ keeper_x = goal_x + (ROBOT_RADIUS if not game.my_team_is_right else -ROBOT_RADIUS)
+ goal_half_width = game.field.half_goal_width
+ post_limit = goal_half_width - ROBOT_RADIUS
+ ball_pos = game.ball.p.to_2d()
+ target = predict_ball_pos_at_x(game, keeper_x)
stop_y = 0.0
- def intersection_with_vertical_line(a, b):
- xa, ya = a
- xb, yb = b
-
- if xb == xa:
- return a # Line is vertical, return the point itself
-
- k = (yb - ya) / (xb - xa)
- y_intersect = ya + k * (goal_x - xa)
- if y_intersect < -half_goal_width:
- return (goal_x, -half_goal_width)
- elif y_intersect > half_goal_width:
- return (goal_x, half_goal_width)
- return (goal_x, y_intersect)
-
- if len(game.friendly_robots) == 2:
+ if len(game.friendly_robots) == 1:
+ stop_y = clamp_y(ball_pos.y, post_limit)
+ elif len(game.friendly_robots) == 2:
try:
# Check if defender is between ball and goal (side-aware)
- defender_between = (game.my_team_is_right and game.friendly_robots[1].p.x > game.ball.p.x) or (
- not game.my_team_is_right and game.friendly_robots[1].p.x < game.ball.p.x
+ defender_between = (game.my_team_is_right and game.friendly_robots[1].p.x > ball_pos.x) or (
+ not game.my_team_is_right and game.friendly_robots[1].p.x < ball_pos.x
)
if defender_between:
- # 1. Project BOTH edges to find the defender's shadow on the goal line
- _, yy_top = intersection_with_vertical_line(
- (game.ball.p.x, game.ball.p.y),
- (
- game.friendly_robots[1].p.x,
- game.friendly_robots[1].p.y + EDGE_OFFSET,
- ),
- )
- _, yy_bottom = intersection_with_vertical_line(
- (game.ball.p.x, game.ball.p.y),
- (
- game.friendly_robots[1].p.x,
- game.friendly_robots[1].p.y - EDGE_OFFSET,
- ),
+ stop_y = single_defender_stop_y(
+ ball_pos,
+ game.friendly_robots[1].p,
+ keeper_x,
+ post_limit,
+ edge_offset,
)
-
- # 2. Calculate the size of the gaps (using max(0, ...) to ignore negative space if shadow is outside the goal)
- top_gap_size = max(0, half_goal_width - yy_top)
- bottom_gap_size = max(0, yy_bottom - (-half_goal_width))
-
- # 3. Position the goalie in the middle of the LARGEST gap
- if top_gap_size > bottom_gap_size:
- stop_y = (yy_top + half_goal_width) / 2
- else:
- stop_y = (yy_bottom - half_goal_width) / 2
except (IndexError, KeyError):
# If robot with ID 1 is not available, keep default stop_y
pass
elif len(game.friendly_robots) >= 3:
try:
# Check if both defenders are between ball and goal (side-aware)
- defender1_between = (game.my_team_is_right and game.friendly_robots[1].p.x > game.ball.p.x) or (
- not game.my_team_is_right and game.friendly_robots[1].p.x < game.ball.p.x
+ defender1_between = (game.my_team_is_right and game.friendly_robots[1].p.x > ball_pos.x) or (
+ not game.my_team_is_right and game.friendly_robots[1].p.x < ball_pos.x
)
- defender2_between = (game.my_team_is_right and game.friendly_robots[2].p.x > game.ball.p.x) or (
- not game.my_team_is_right and game.friendly_robots[2].p.x < game.ball.p.x
+ defender2_between = (game.my_team_is_right and game.friendly_robots[2].p.x > ball_pos.x) or (
+ not game.my_team_is_right and game.friendly_robots[2].p.x < ball_pos.x
)
if defender1_between and defender2_between:
- _, yy1 = intersection_with_vertical_line(
- (game.ball.p.x, game.ball.p.y),
- (
- game.friendly_robots[1].p.x,
- game.friendly_robots[1].p.y + EDGE_OFFSET,
- ),
+ _, yy1 = intersection_with_x_line(
+ (ball_pos.x, ball_pos.y),
+ (game.friendly_robots[1].p.x, game.friendly_robots[1].p.y + edge_offset),
+ keeper_x,
+ post_limit,
)
- _, yy2 = intersection_with_vertical_line(
- (game.ball.p.x, game.ball.p.y),
- (
- game.friendly_robots[2].p.x,
- game.friendly_robots[2].p.y - EDGE_OFFSET,
- ),
+ _, yy2 = intersection_with_x_line(
+ (ball_pos.x, ball_pos.y),
+ (game.friendly_robots[2].p.x, game.friendly_robots[2].p.y - edge_offset),
+ keeper_x,
+ post_limit,
)
stop_y = (yy1 + yy2) / 2
except (IndexError, KeyError):
# If robots with IDs 1 or 2 are not available, keep existing stop_y
pass
- if not target or abs(target[1]) > half_goal_width:
- target = Vector2D(goal_x, stop_y)
-
- # shooters_data = find_likely_enemy_shooter(game.enemy_robots, game.ball)
-
- if target:
- cmd = go_to_point(
- game,
- motion_controller,
- robot_id,
- target,
- dribbling=True,
- )
+ if target is None:
+ target = Vector2D(keeper_x, stop_y)
+ elif abs(target.y) > goal_half_width:
+ # Ball heading toward goal but predicted wide -- clamp to nearest post
+ # instead of snapping to stop_y, so the keeper stays reactive to the shot
+ clamped_y = max(-post_limit, min(post_limit, target.y))
+ target = Vector2D(keeper_x, clamped_y)
else:
- cmd = move(
- game,
- motion_controller,
- robot_id,
- Vector2D(None, None), # No specific target
- face_ball(game.friendly_robots[robot_id].p, game.ball.p),
- )
- return cmd
+ target = Vector2D(keeper_x, target.y)
+
+ return go_to_point(
+ game,
+ motion_controller,
+ robot_id,
+ target,
+ dribbling=True,
+ )
diff --git a/utama_core/skills/src/utils/defense_utils.py b/utama_core/skills/src/utils/defense_utils.py
index c9d654a0..ef98ed4a 100644
--- a/utama_core/skills/src/utils/defense_utils.py
+++ b/utama_core/skills/src/utils/defense_utils.py
@@ -1,4 +1,4 @@
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, List, NamedTuple, Optional, Tuple
import numpy as np
@@ -8,6 +8,37 @@
EPS = 1e-5
+CURVE_MARGIN = 0.1
+CURVE_SHAPE_R = 2.1
+
+
+class DefenseGeometry(NamedTuple):
+ goal_x: float
+ goal_half_width: float
+ defense_front_x: float
+ defense_depth: float
+ defense_half_width: float
+
+
+def _defense_geometry(game: Game) -> DefenseGeometry:
+ """Derive all field-shaped defense values from ``game.field``."""
+ goal_line = game.field.my_goal_line
+ goal_x = float(goal_line[0][0])
+ goal_half_width = abs(float(goal_line[0][1]))
+
+ defense_area = game.field.my_defense_area
+ defense_front_x = float(defense_area[1][0])
+ defense_depth = abs(goal_x - defense_front_x)
+ defense_half_width = abs(float(defense_area[0][1]))
+
+ return DefenseGeometry(
+ goal_x=goal_x,
+ goal_half_width=goal_half_width,
+ defense_front_x=defense_front_x,
+ defense_depth=defense_depth,
+ defense_half_width=defense_half_width,
+ )
+
def align_defenders(
game: Game,
@@ -24,8 +55,8 @@ def align_defenders(
# Start by getting the current position of the defenders
defender_pos = calculate_defense_area(game, defender_parametric_pos)
- # logger.debug(f"DEFENDER {dx} {dy}")
- goal_centre_x = game.field.my_goal_line[0][0]
+ geo = _defense_geometry(game)
+ goal_centre_x = geo.goal_x
if attacker_orientation is None or attacker_orientation == 0:
# In case there is no ball velocity or attackers, use centre of goal
@@ -33,7 +64,10 @@ def align_defenders(
else:
predicted_goal_position = Vector2D(
goal_centre_x,
- clamp_to_goal_width(predict_goal_y_location(game, attacker_position, attacker_orientation)),
+ clamp_to_goal_width(
+ predict_goal_y_location(game, attacker_position, attacker_orientation),
+ geo.goal_half_width,
+ ),
)
if env:
@@ -85,21 +119,16 @@ def calculate_defense_area(game: Game, t: float) -> Vector2D:
cos_t = np.cos(t)
sin_t = np.sin(t)
- a, r = 1.1, 2.1
+ geo = _defense_geometry(game)
+ front_extent = geo.defense_depth + CURVE_MARGIN
+ vertical_extent = geo.defense_half_width + CURVE_MARGIN
+ r = CURVE_SHAPE_R
rp = Vector2D(
- a * ((1 - r) * (abs(cos_t) * cos_t) + r * cos_t),
- a * ((1 - r) * (abs(sin_t) * sin_t) + r * sin_t),
+ front_extent * ((1 - r) * (abs(cos_t) * cos_t) + r * cos_t),
+ vertical_extent * ((1 - r) * (abs(sin_t) * sin_t) + r * sin_t),
)
- # This slows everything down sooo much that everything breaks (ask Fred if still confused)
- # goal_centre_x, _ = game.field.my_goal_line[0]
-
- if game.my_team_is_right:
- goal_centre_x = 4.5
- else:
- goal_centre_x = -4.5
-
- return make_relative_to_goal_centre(goal_centre_x, rp)
+ return make_relative_to_goal_centre(geo.goal_x, rp)
def make_relative_to_goal_centre(goal_centre_x: float, p: Vector2D) -> Vector2D:
@@ -166,8 +195,8 @@ def step_curve(t: float, direction: int):
return direction * STEP_SIZE + t
-def clamp_to_goal_width(y: float) -> float:
- return max(min(y, 0.5), -0.5)
+def clamp_to_goal_width(y: float, goal_half_width: float) -> float:
+ return max(min(y, goal_half_width), -goal_half_width)
def clamp_to_parametric(t: float) -> float:
@@ -204,3 +233,69 @@ def velocity_to_orientation(p: Tuple[float, float]) -> float:
if res < 0:
res += 2 * np.pi
return res
+
+
+# ---------------------------------------------------------------------------
+# Shared shadow-geometry helpers (used by both goalkeep and defend_parameter)
+# ---------------------------------------------------------------------------
+
+
+def clamp_y(y: float, limit: float) -> float:
+ """Clamp *y* to ``[-limit, limit]``."""
+ return max(-limit, min(limit, y))
+
+
+def intersection_with_x_line(a, b, target_x: float, y_limit: float):
+ """Where line *a* -> *b* crosses ``x = target_x``, y clamped to +/-*y_limit*.
+
+ Returns ``(target_x, clamped_y)``. If the line is vertical (dx ~ 0),
+ returns the y of *b* clamped. If the crossing is behind *a* (t < 0),
+ returns the y of *a* clamped.
+ """
+ xa, ya = a
+ xb, yb = b
+ dx = xb - xa
+
+ if abs(dx) < 1e-12:
+ return (target_x, clamp_y(yb, y_limit))
+
+ t = (target_x - xa) / dx
+ if t < 0:
+ return (target_x, clamp_y(ya, y_limit))
+
+ y_intersect = ya + t * (yb - ya)
+ return (target_x, clamp_y(y_intersect, y_limit))
+
+
+def single_defender_stop_y(
+ ball_pos,
+ defender_pos,
+ keeper_x: float,
+ post_limit: float,
+ edge_offset: float,
+) -> float:
+ """Keeper y-position given a single defender's shadow on the keeper line.
+
+ Projects both edges of the defender to find the shadow interval, then
+ positions the keeper in the centre of the largest uncovered gap.
+ """
+ _, yy_top = intersection_with_x_line(
+ (ball_pos.x, ball_pos.y),
+ (defender_pos.x, defender_pos.y + edge_offset),
+ keeper_x,
+ post_limit,
+ )
+ _, yy_bottom = intersection_with_x_line(
+ (ball_pos.x, ball_pos.y),
+ (defender_pos.x, defender_pos.y - edge_offset),
+ keeper_x,
+ post_limit,
+ )
+
+ top_gap_size = max(0, post_limit - yy_top)
+ bottom_gap_size = max(0, yy_bottom - (-post_limit))
+
+ if top_gap_size > bottom_gap_size:
+ return (yy_top + post_limit) / 2
+
+ return (yy_bottom - post_limit) / 2
diff --git a/utama_core/strategy/common/__init__.py b/utama_core/strategy/common/__init__.py
index c8290df5..4ae22305 100644
--- a/utama_core/strategy/common/__init__.py
+++ b/utama_core/strategy/common/__init__.py
@@ -4,3 +4,10 @@
SpaceRequirements,
)
from utama_core.strategy.common.base_blackboard import BaseBlackboard
+from utama_core.strategy.common.blackboard_contract import (
+ BlackboardKeySpec,
+ declared_contract,
+ register_blackboard_contract,
+ resolve_runtime_key,
+ validate_blackboard_contracts,
+)
diff --git a/utama_core/strategy/common/abstract_behaviour.py b/utama_core/strategy/common/abstract_behaviour.py
index 1f14b5ee..128f02b5 100644
--- a/utama_core/strategy/common/abstract_behaviour.py
+++ b/utama_core/strategy/common/abstract_behaviour.py
@@ -4,6 +4,7 @@
from utama_core.config.settings import BLACKBOARD_NAMESPACE_MAP
from utama_core.strategy.common.base_blackboard import BaseBlackboard
+from utama_core.strategy.common.blackboard_contract import register_blackboard_contract
class AbstractBehaviour(py_trees.behaviour.Behaviour):
@@ -83,6 +84,7 @@ def setup(self, **kwargs: Any) -> None:
key="tactic",
access=py_trees.common.Access.READ,
)
+ register_blackboard_contract(self)
self.setup_()
# prevent overriding of setup method
diff --git a/utama_core/strategy/common/abstract_strategy.py b/utama_core/strategy/common/abstract_strategy.py
index 091acdfc..bdf1d7ea 100644
--- a/utama_core/strategy/common/abstract_strategy.py
+++ b/utama_core/strategy/common/abstract_strategy.py
@@ -12,6 +12,7 @@
from utama_core.entities.data.command import RobotCommand
from utama_core.entities.game import Game
from utama_core.entities.game.field import Field, FieldBounds
+from utama_core.entities.referee.referee_command import RefereeCommand
from utama_core.global_utils.math_utils import (
assert_contains,
assert_valid_bounding_box,
@@ -19,6 +20,7 @@
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.rsoccer_simulator.src.ssl.ssl_gym_base import SSLBaseEnv
from utama_core.skills.src.utils.move_utils import empty_command
+from utama_core.strategy.common.abstract_behaviour import AbstractBehaviour
from utama_core.strategy.common.base_blackboard import BaseBlackboard
from utama_core.team_controller.src.controllers.common.robot_controller_abstract import (
AbstractRobotController,
@@ -73,6 +75,55 @@ class SpaceRequirements:
min_width: float
+_REFEREE_STOPPAGE_COMMANDS = frozenset(
+ {
+ RefereeCommand.HALT,
+ RefereeCommand.STOP,
+ RefereeCommand.PREPARE_KICKOFF_YELLOW,
+ RefereeCommand.PREPARE_KICKOFF_BLUE,
+ RefereeCommand.PREPARE_PENALTY_YELLOW,
+ RefereeCommand.PREPARE_PENALTY_BLUE,
+ RefereeCommand.DIRECT_FREE_YELLOW,
+ RefereeCommand.DIRECT_FREE_BLUE,
+ RefereeCommand.INDIRECT_FREE_YELLOW,
+ RefereeCommand.INDIRECT_FREE_BLUE,
+ RefereeCommand.TIMEOUT_YELLOW,
+ RefereeCommand.TIMEOUT_BLUE,
+ RefereeCommand.GOAL_YELLOW,
+ RefereeCommand.GOAL_BLUE,
+ RefereeCommand.BALL_PLACEMENT_YELLOW,
+ RefereeCommand.BALL_PLACEMENT_BLUE,
+ }
+)
+
+
+class _ResetStrategyOnRefereeStoppage(AbstractBehaviour):
+ """Invalidate a strategy subtree once for each new referee stoppage token."""
+
+ def __init__(self, target: py_trees.behaviour.Behaviour, name: str = "ResetStrategyOnRefereeStoppage"):
+ super().__init__(name=name)
+ self.target = target
+ self._last_reset_token: tuple[RefereeCommand, float | None] | None = None
+
+ def update(self) -> py_trees.common.Status:
+ game = self.blackboard.game
+ referee = getattr(game, "referee", None)
+ if referee is None:
+ return py_trees.common.Status.SUCCESS
+
+ command = getattr(referee, "referee_command", None)
+ if command not in _REFEREE_STOPPAGE_COMMANDS:
+ return py_trees.common.Status.SUCCESS
+
+ token = (command, getattr(referee, "referee_command_timestamp", None))
+ if token != self._last_reset_token:
+ if self.target.status != py_trees.common.Status.INVALID:
+ self.target.stop(py_trees.common.Status.INVALID)
+ self._last_reset_token = token
+
+ return py_trees.common.Status.SUCCESS
+
+
@dataclass
class AbstractStrategy(ABC):
"""
@@ -95,10 +146,19 @@ def __init__(self):
strategy_subtree = self.create_behaviour_tree()
# Wrap the user's strategy tree with the referee override layer (Option B).
- # The root Selector checks referee commands first; if none match (e.g. NORMAL_START
- # or FORCE_START), it falls through to the strategy subtree.
+ # The reset guard invalidates any running strategy memory as soon as a
+ # stoppage command arrives. The root Selector checks referee commands next;
+ # if none match (e.g. NORMAL_START or FORCE_START), it falls through to the
+ # freshly reset strategy subtree.
root = py_trees.composites.Selector(name="Root", memory=False)
- root.add_children([build_referee_override_tree(), strategy_subtree])
+ referee_reset_and_override = py_trees.composites.Sequence(name="RefereeResetAndOverride", memory=False)
+ referee_reset_and_override.add_children(
+ [
+ _ResetStrategyOnRefereeStoppage(strategy_subtree),
+ build_referee_override_tree(),
+ ]
+ )
+ root.add_children([referee_reset_and_override, strategy_subtree])
self.behaviour_tree = py_trees.trees.BehaviourTree(root)
@@ -203,6 +263,7 @@ def setup_strategy_blackboard(self, is_opp_strat: bool):
Setups the blackboard based on if is_opp_strat.
"""
+ self._is_opp_strat = is_opp_strat
self.blackboard = self._setup_blackboard(is_opp_strat)
def setup_behaviour_tree(self, is_opp_strat: bool):
diff --git a/utama_core/strategy/common/blackboard_contract.py b/utama_core/strategy/common/blackboard_contract.py
new file mode 100644
index 00000000..0ff85cef
--- /dev/null
+++ b/utama_core/strategy/common/blackboard_contract.py
@@ -0,0 +1,242 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Iterable
+
+import py_trees
+from py_trees.blackboard import Blackboard
+
+
+@dataclass(frozen=True)
+class BlackboardKeySpec:
+ """Declared blackboard contract for a behaviour constructor key."""
+
+ name: str
+ access: str | py_trees.common.Access
+ key_attr: str | None = None
+ key: str | None = None
+ required: bool = True
+ type_name: str = "unknown"
+ meaning: str = ""
+ default: str | None = None
+ bounds: tuple[float | None, float | None] | None = None
+
+ def runtime_key(self, node: Any | None = None) -> str | None:
+ if self.key is not None:
+ return self.key
+ if node is not None and self.key_attr is not None:
+ if hasattr(node, self.key_attr):
+ value = getattr(node, self.key_attr)
+ return None if value is None else str(value)
+ return self.name
+
+ def to_dict(self, node: Any | None = None, *, resolve: bool = False) -> dict[str, Any]:
+ key = self.runtime_key(node)
+ payload = {
+ "name": self.name,
+ "key": key,
+ "key_attr": self.key_attr,
+ "access": _access_name(self.access),
+ "required": self.required,
+ "type": self.type_name,
+ "meaning": self.meaning,
+ "default": self.default,
+ "bounds": self.bounds,
+ }
+ if resolve and node is not None and key is not None:
+ payload["resolved_key"] = resolve_runtime_key(node, key)
+ return payload
+
+
+def declared_contract(target: Any, *, resolve: bool = False) -> list[dict[str, Any]]:
+ return [spec.to_dict(target, resolve=resolve) for spec in _contract_specs(target)]
+
+
+def resolve_runtime_key(node: Any, key: str) -> str:
+ blackboards = getattr(node, "blackboards", ())
+ for client in blackboards:
+ try:
+ return client.absolute_name(key)
+ except KeyError:
+ continue
+ client = getattr(node, "blackboard", None)
+ namespace = getattr(client, "namespace", None) if client is not None else None
+ return Blackboard.absolute_name(namespace, key)
+
+
+def register_blackboard_contract(node: Any) -> None:
+ """Register a behaviour's declared blackboard keys on its py_trees client."""
+
+ specs = _contract_specs(node)
+ if not specs:
+ return
+
+ blackboard = getattr(node, "blackboard", None)
+ if blackboard is None:
+ raise RuntimeError(f"{_node_label(node)} has blackboard contracts but no attached blackboard client")
+
+ for spec in specs:
+ key = _registration_key(node, spec)
+ if key is None:
+ continue
+ blackboard.register_key(key=key, access=_normalise_access(spec.access, node, spec))
+
+
+def validate_blackboard_contracts(
+ root: Any,
+ *,
+ seeded_keys: Iterable[str] | None = None,
+ include_base_keys: bool = True,
+) -> dict[str, Any]:
+ """Validate declared read contracts against known writers and seeded inputs.
+
+ This deliberately validates the declared contract graph, not py_trees runtime
+ required keys. Many strategy keys are written by earlier behaviour nodes.
+ """
+
+ available_keys = {_comparison_key(key) for key in (seeded_keys or ())}
+ if include_base_keys:
+ from utama_core.strategy.common.base_blackboard import BaseBlackboard
+
+ available_keys.update(BaseBlackboard.base_keys())
+
+ reads: list[dict[str, Any]] = []
+ writes: set[str] = set()
+ errors: list[dict[str, Any]] = []
+
+ for node in _iter_nodes(root):
+ for spec in _contract_specs(node):
+ try:
+ key = _registration_key(node, spec)
+ if key is None:
+ continue
+ access = _normalise_access(spec.access, node, spec)
+ except (TypeError, ValueError) as exc:
+ errors.append(_validation_error("malformed_contract", node, spec, None, str(exc)))
+ continue
+
+ item = {
+ "node": getattr(node, "name", node.__class__.__name__),
+ "class_name": node.__class__.__name__,
+ "name": spec.name,
+ "key": key,
+ "access": access.name,
+ }
+ if access == py_trees.common.Access.READ:
+ reads.append(item)
+ elif access in {py_trees.common.Access.WRITE, py_trees.common.Access.EXCLUSIVE_WRITE}:
+ writes.add(_comparison_key(key))
+
+ available_keys.update(writes)
+ for item in reads:
+ if _comparison_key(item["key"]) not in available_keys:
+ errors.append(
+ {
+ "code": "missing_writer",
+ **item,
+ "message": f"Required READ key {item['key']!r} has no declared writer or seeded input.",
+ }
+ )
+
+ return {
+ "status": "valid" if not errors else "invalid",
+ "errors": errors,
+ "read_keys": sorted({_comparison_key(item["key"]) for item in reads}),
+ "write_keys": sorted(writes),
+ "available_keys": sorted(available_keys),
+ }
+
+
+def _contract_specs(target: Any) -> tuple[BlackboardKeySpec, ...]:
+ specs = getattr(target, "BLACKBOARD_CONTRACT", ())
+ if specs is None:
+ return ()
+ if not isinstance(specs, tuple):
+ raise TypeError(f"{_node_label(target)} BLACKBOARD_CONTRACT must be a tuple")
+ for spec in specs:
+ if not isinstance(spec, BlackboardKeySpec):
+ raise TypeError(f"{_node_label(target)} BLACKBOARD_CONTRACT entries must be BlackboardKeySpec")
+ return specs
+
+
+def _registration_key(node: Any, spec: BlackboardKeySpec) -> str | None:
+ if spec.key is not None:
+ key = spec.key
+ elif spec.key_attr is not None:
+ if not hasattr(node, spec.key_attr):
+ if spec.required:
+ raise ValueError(
+ f"{_node_label(node)} contract {spec.name!r} references missing attribute {spec.key_attr!r}"
+ )
+ return None
+ value = getattr(node, spec.key_attr)
+ if value is None:
+ if spec.required:
+ raise ValueError(f"{_node_label(node)} contract {spec.name!r} resolved to None")
+ return None
+ key = str(value)
+ else:
+ key = spec.name
+
+ if key == "":
+ if spec.required:
+ raise ValueError(f"{_node_label(node)} contract {spec.name!r} resolved to an empty key")
+ return None
+ return key
+
+
+def _normalise_access(access: Any, node: Any, spec: BlackboardKeySpec) -> py_trees.common.Access:
+ if isinstance(access, py_trees.common.Access):
+ return access
+ if isinstance(access, str):
+ try:
+ return py_trees.common.Access[access]
+ except KeyError as exc:
+ valid = ", ".join(item.name for item in py_trees.common.Access)
+ raise ValueError(
+ f"{_node_label(node)} contract {spec.name!r} has invalid access {access!r}; expected one of {valid}"
+ ) from exc
+ raise TypeError(f"{_node_label(node)} contract {spec.name!r} access must be a string or py_trees Access")
+
+
+def _iter_nodes(root: Any) -> Iterable[Any]:
+ root = getattr(root, "root", root)
+ if hasattr(root, "iterate"):
+ yield from root.iterate()
+ else:
+ yield root
+
+
+def _validation_error(
+ code: str,
+ node: Any,
+ spec: BlackboardKeySpec,
+ key: str | None,
+ message: str,
+) -> dict[str, Any]:
+ return {
+ "code": code,
+ "node": getattr(node, "name", node.__class__.__name__),
+ "class_name": node.__class__.__name__,
+ "name": spec.name,
+ "key": key,
+ "access": _access_name(spec.access),
+ "message": message,
+ }
+
+
+def _comparison_key(key: str) -> str:
+ key = str(key)
+ if key.startswith("/"):
+ return key.rsplit("/", 1)[-1]
+ return key
+
+
+def _node_label(node: Any) -> str:
+ return getattr(node, "name", getattr(node, "__name__", node.__class__.__name__))
+
+
+def _access_name(access: Any) -> str:
+ if hasattr(access, "name"):
+ return access.name
+ return str(access)
diff --git a/utama_core/strategy/examples/__init__.py b/utama_core/strategy/examples/__init__.py
index a59b9b55..d85dcc67 100644
--- a/utama_core/strategy/examples/__init__.py
+++ b/utama_core/strategy/examples/__init__.py
@@ -1,4 +1,11 @@
+from utama_core.strategy.examples.ball_placement_and_kick_strategy import (
+ BallPlacementAndKickStrategy,
+ KickAfterDirectFreeStep,
+)
from utama_core.strategy.examples.defense_strategy import DefenceStrategy
+from utama_core.strategy.examples.deliberate_out_of_bounds_strategy import (
+ DeliberateOutOfBoundsStrategy,
+)
from utama_core.strategy.examples.go_to_ball_ex import GoToBallExampleStrategy
from utama_core.strategy.examples.motion_planning.multi_robot_navigation_strategy import (
MultiRobotNavigationStrategy,
diff --git a/utama_core/strategy/examples/ball_placement_and_kick_strategy.py b/utama_core/strategy/examples/ball_placement_and_kick_strategy.py
new file mode 100644
index 00000000..d26e3270
--- /dev/null
+++ b/utama_core/strategy/examples/ball_placement_and_kick_strategy.py
@@ -0,0 +1,116 @@
+from typing import Optional
+
+import py_trees
+
+from utama_core.entities.data.command import RobotCommand
+from utama_core.entities.referee.referee_command import RefereeCommand
+from utama_core.strategy.common import AbstractBehaviour
+from utama_core.strategy.examples.ball_placement_strategy import BallPlacementStrategy
+
+_DIRECT_FREE_COMMANDS = frozenset(
+ {
+ RefereeCommand.DIRECT_FREE_YELLOW,
+ RefereeCommand.DIRECT_FREE_BLUE,
+ }
+)
+
+
+class _CommandTracker:
+ """Mutable container updated by BallPlacementAndKickStrategy.step() every tick.
+
+ Because KickAfterDirectFreeStep is not ticked while the referee-override tree
+ handles DIRECT_FREE_* commands, it cannot observe that command transition
+ directly. The strategy's step() method samples the referee command before
+ each tree tick and records it here so the behaviour can read it on the next
+ NORMAL_START tick.
+ """
+
+ def __init__(self):
+ self.prev_command: Optional[RefereeCommand] = None
+
+
+class KickAfterDirectFreeStep(AbstractBehaviour):
+ """Kick once on each NORMAL_START that follows a direct-free command."""
+
+ _KICK_DISTANCE = 0.22
+
+ def __init__(self, name: str, tracker: _CommandTracker):
+ super().__init__(name=name)
+ self._tracker = tracker
+
+ def setup_(self):
+ self._active_restart_timestamp: float | None = None
+ self._kick_sent_for_timestamp: float | None = None
+
+ def update(self) -> py_trees.common.Status:
+ game = self.blackboard.game
+ ref = game.referee
+
+ if ref is None:
+ return py_trees.common.Status.RUNNING
+
+ current_command = ref.referee_command
+
+ if current_command != RefereeCommand.NORMAL_START:
+ return py_trees.common.Status.RUNNING
+
+ # Only kick if we transitioned here from a direct free (not from a kickoff
+ # seed / force-start at game start). _tracker.prev_command is maintained
+ # by the strategy's step() so it reflects commands seen during ticks when
+ # this node was overridden by the RefereeOverride subtree.
+ if self._tracker.prev_command not in _DIRECT_FREE_COMMANDS:
+ return py_trees.common.Status.RUNNING
+
+ restart_timestamp = ref.referee_command_timestamp
+ if restart_timestamp != self._active_restart_timestamp:
+ self._active_restart_timestamp = restart_timestamp
+ self._kick_sent_for_timestamp = None
+
+ if self._kick_sent_for_timestamp == restart_timestamp:
+ return py_trees.common.Status.RUNNING
+
+ if game.ball is None or not game.friendly_robots:
+ return py_trees.common.Status.RUNNING
+
+ kicker_id = min(
+ game.friendly_robots,
+ key=lambda rid: game.friendly_robots[rid].p.distance_to(game.ball.p),
+ )
+ kicker = game.friendly_robots[kicker_id]
+ if kicker.p.distance_to(game.ball.p) > self._KICK_DISTANCE:
+ return py_trees.common.Status.RUNNING
+
+ self.blackboard.cmd_map[kicker_id] = RobotCommand(
+ local_forward_vel=0,
+ local_left_vel=0,
+ angular_vel=0,
+ kick=1,
+ chip=0,
+ dribble=0,
+ )
+ self._kick_sent_for_timestamp = restart_timestamp
+
+ return py_trees.common.Status.RUNNING
+
+
+class BallPlacementAndKickStrategy(BallPlacementStrategy):
+ """Use referee ball placement/direct-free setup, then kick once on NORMAL_START."""
+
+ def __init__(self):
+ self._tracker = _CommandTracker()
+ super().__init__()
+
+ def create_behaviour_tree(self) -> py_trees.behaviour.Behaviour:
+ root = py_trees.composites.Sequence(name="PlacementRestartKickRoot", memory=False)
+ root.add_child(KickAfterDirectFreeStep(name="KickAfterDirectFree", tracker=self._tracker))
+ return root
+
+ def step(self):
+ # Sample the referee command *before* the tree tick so that transitions
+ # through DIRECT_FREE_* (which are handled by the RefereeOverride subtree
+ # and never reach KickAfterDirectFreeStep) are still recorded in the tracker.
+ game = self.blackboard.game
+ ref = game.referee
+ if ref is not None:
+ self._tracker.prev_command = ref.referee_command
+ super().step()
diff --git a/utama_core/strategy/examples/ball_placement_strategy.py b/utama_core/strategy/examples/ball_placement_strategy.py
new file mode 100644
index 00000000..f22611f5
--- /dev/null
+++ b/utama_core/strategy/examples/ball_placement_strategy.py
@@ -0,0 +1,182 @@
+"""ball_placement_strategy.py — Skeleton for the ball placement feature.
+
+Your task
+---------
+Implement ``BallPlacementStep.update()`` so that, when the referee issues
+BALL_PLACEMENT_YELLOW (our team places), one robot picks up the ball with its
+dribbler and carries it to ``game.referee.designated_position``, while the
+other robot waits clear of the ball.
+
+The referee integration (deciding *when* to use this behaviour) is already
+wired up in the referee override tree — you do not need to touch the referee
+config. Your work is entirely inside this file's strategy and behaviour tree.
+
+How ball placement works
+------------------------
+1. Referee issues BALL_PLACEMENT_YELLOW with a ``designated_position`` (x, y).
+2. One robot (the "placer") drives to the ball with its dribbler on to capture it.
+3. Once it has the ball (``robot.has_ball`` is True), it carries it to
+ ``designated_position`` and stops.
+4. When the ball is within ``BALL_PLACEMENT_DONE_DISTANCE`` of the target the
+ referee auto-advances to DIRECT_FREE_YELLOW → NORMAL_START.
+5. All other robots must stay at least ``BALL_KEEP_OUT_DISTANCE`` from the ball.
+
+Useful references
+-----------------
+- ``game.referee.designated_position`` — (x, y) tuple, the placement target.
+- ``game.ball.p`` — current ball position (Vector2D-like).
+- ``robot.has_ball`` — True when the robot's IR dribbler sensor
+ detects the ball.
+- ``move(game, motion_controller, robot_id, target, orientation, dribbling=True)``
+ — send a motion command; pass ``dribbling=True`` to activate the dribbler.
+- ``empty_command(False)`` — stop a robot in place.
+- ``BALL_PLACEMENT_DONE_DISTANCE`` — 0.15 m; ball must be this close to target.
+- ``BALL_KEEP_OUT_DISTANCE`` — 0.8 m; clearance for non-placer robots.
+
+Running the demo
+----------------
+ pixi run python demo_ball_placement.py
+
+The RSim window opens and the browser GUI at http://localhost:8080 lets you
+issue BALL_PLACEMENT_YELLOW commands from the "Manual Commands" panel and watch
+your strategy respond.
+
+Running the tests
+-----------------
+ pixi run pytest utama_core/tests/strategy_runner/test_ball_placement_rsim.py -v
+
+There are three tests that must pass before the feature is considered complete:
+ 1. placer robot drives toward the ball after the command is issued.
+ 2. placer carries the ball toward the designated position (dribbler on).
+ 3. non-placer robot stays outside the keep-out radius throughout.
+"""
+
+import math
+from typing import Optional
+
+import py_trees
+
+from utama_core.config.referee_constants import (
+ BALL_KEEP_OUT_DISTANCE,
+ BALL_PLACEMENT_DONE_DISTANCE,
+)
+from utama_core.entities.data.vector import Vector2D
+from utama_core.skills.src.utils.move_utils import empty_command, move, turn_on_spot
+from utama_core.strategy.common import AbstractBehaviour, AbstractStrategy
+from utama_core.strategy.referee.actions import _clear_to_legal_positions
+
+
+class BallPlacementStep(AbstractBehaviour):
+ """Move one robot to place the ball at the referee's designated position.
+
+ Implementation guide
+ --------------------
+ The ``update()`` method is called every simulation tick while the referee
+ command is BALL_PLACEMENT_YELLOW. It must:
+
+ 1. Read ``game.referee.designated_position`` — the (x, y) target tuple.
+ 2. If the ball is already at the target (within ``BALL_PLACEMENT_DONE_DISTANCE``),
+ stop all robots and return RUNNING (the referee will advance the state).
+ 3. Pick the robot closest to the ball as the placer.
+ 4. If the placer does not yet have the ball (``robot.has_ball`` is False),
+ drive it to the ball with the dribbler on.
+ 5. Once the placer has the ball, drive it to ``designated_position`` with
+ the dribbler still on.
+ 6. Keep all non-placer robots at least ``BALL_KEEP_OUT_DISTANCE`` away from
+ the ball by calling ``_clear_to_legal_positions``.
+
+ Return ``py_trees.common.Status.RUNNING`` on every tick (the referee
+ override tree holds here until the command changes).
+
+ Tips
+ ----
+ - Use ``Vector2D(target[0], target[1])`` to convert the tuple to a Vector2D.
+ - Orientation: ``robot.p.angle_to(target_pos)`` gives the heading angle.
+ - Call ``move(..., dribbling=True)`` to activate the dribbler.
+ - Write commands to ``self.blackboard.cmd_map[robot_id]`` for each robot.
+ """
+
+ def update(self) -> py_trees.common.Status:
+ game = self.blackboard.game
+ ref = game.referee
+ motion_controller = self.blackboard.motion_controller
+
+ if ref is None or game.ball is None or not game.friendly_robots:
+ return self._stop_all()
+
+ target = ref.designated_position
+ if target is None:
+ return self._stop_all()
+
+ target_pos = Vector2D(target[0], target[1])
+ ball = game.ball
+
+ if ball.p.distance_to(target_pos) <= BALL_PLACEMENT_DONE_DISTANCE:
+ return self._stop_all()
+
+ placer_id = min(
+ game.friendly_robots,
+ key=lambda rid: game.friendly_robots[rid].p.distance_to(ball.p),
+ )
+ placer = game.friendly_robots[placer_id]
+
+ _FACE_READY_ANGLE = 0.2
+
+ if placer.has_ball:
+ orientation = placer.p.angle_to(target_pos)
+ face_error = math.atan2(
+ math.sin(orientation - placer.orientation), math.cos(orientation - placer.orientation)
+ )
+ if abs(face_error) > _FACE_READY_ANGLE:
+ self.blackboard.cmd_map[placer_id] = turn_on_spot(
+ game, motion_controller, placer_id, orientation, dribbling=True
+ )
+ else:
+ self.blackboard.cmd_map[placer_id] = move(
+ game, motion_controller, placer_id, target_pos, orientation, dribbling=True
+ )
+ else:
+ target_for_move = Vector2D(ball.p.x, ball.p.y)
+ orientation = placer.p.angle_to(target_for_move)
+ self.blackboard.cmd_map[placer_id] = move(
+ game, motion_controller, placer_id, target_for_move, orientation, dribbling=True
+ )
+
+ _clear_to_legal_positions(
+ self.blackboard,
+ ball_keep_dist=BALL_KEEP_OUT_DISTANCE,
+ exempt_robot_ids={placer_id},
+ )
+ return py_trees.common.Status.RUNNING
+
+ def _stop_all(self) -> py_trees.common.Status:
+ game = self.blackboard.game
+ for robot_id in game.friendly_robots:
+ self.blackboard.cmd_map[robot_id] = empty_command(False)
+ return py_trees.common.Status.RUNNING
+
+
+class BallPlacementStrategy(AbstractStrategy):
+ """2v2 ball placement strategy for the Exhibition Road field.
+
+ The referee override tree (built into AbstractStrategy) automatically handles
+ HALT, STOP, kickoff, and penalty commands — you only need to implement what
+ happens during BALL_PLACEMENT_YELLOW (i.e., in ``BallPlacementStep``).
+
+ During NORMAL_START / FORCE_START the ``IdleRoot`` sequence at the bottom of
+ the tree runs — replace it with your regular game strategy once placement works.
+ """
+
+ def create_behaviour_tree(self) -> py_trees.behaviour.Behaviour:
+ root = py_trees.composites.Sequence(name="BallPlacementRoot", memory=False)
+ root.add_child(BallPlacementStep(name="PlaceBall"))
+ return root
+
+ def assert_exp_robots(self, n_runtime_friendly: int, n_runtime_enemy: int) -> bool:
+ return n_runtime_friendly >= 1
+
+ def assert_exp_goals(self, includes_my_goal_line: bool, includes_opp_goal_line: bool) -> bool:
+ return True
+
+ def get_min_bounding_req(self) -> Optional[object]:
+ return None
diff --git a/utama_core/strategy/examples/deliberate_out_of_bounds_strategy.py b/utama_core/strategy/examples/deliberate_out_of_bounds_strategy.py
new file mode 100644
index 00000000..db696d52
--- /dev/null
+++ b/utama_core/strategy/examples/deliberate_out_of_bounds_strategy.py
@@ -0,0 +1,215 @@
+"""Opponent strategy that deliberately creates out-of-bounds ball-placement events."""
+
+import math
+
+import py_trees
+
+from utama_core.config.field_params import FieldDimensions
+from utama_core.config.physical_constants import ROBOT_RADIUS
+from utama_core.entities.data.command import RobotCommand
+from utama_core.entities.data.vector import Vector2D
+from utama_core.skills.src.utils.move_utils import empty_command, move
+from utama_core.strategy.common import AbstractBehaviour, AbstractStrategy
+
+
+def _angle_delta(target: float, current: float) -> float:
+ return math.atan2(math.sin(target - current), math.cos(target - current))
+
+
+class DeliberateOutOfBoundsStep(AbstractBehaviour):
+ """Line up behind the ball, shove once toward a boundary, then stop."""
+
+ _TARGETS = ("top", "bottom", "left", "right", "yellow_goal")
+ _BOUNDARY_EPSILON = 0.04
+ _GOAL_EPSILON = 0.06
+ _GOAL_MOUTH_CLEARANCE = 0.28
+ _APPROACH_BACKOFF = ROBOT_RADIUS + 0.10
+ _APPROACH_TOLERANCE = 0.08
+ _ALIGN_TOLERANCE_RAD = 0.14
+ _PUSH_SPEED = 1.9
+ _PUSH_SECONDS = 0.34
+ _STOP_SECONDS = 0.35
+
+ def __init__(self, field_dims: FieldDimensions, name: str = "DeliberateOutOfBoundsStep"):
+ super().__init__(name=name)
+ self._field_dims = field_dims
+ self._target_index = 0
+ self._phase = "approach"
+ self._phase_started_at = 0.0
+ self._waiting_for_restart = False
+
+ def update(self) -> py_trees.common.Status:
+ game = self.blackboard.game
+ motion_controller = self.blackboard.motion_controller
+
+ if game.ball is None or not game.friendly_robots:
+ return self._stop_all()
+
+ ball_pos = Vector2D(game.ball.p.x, game.ball.p.y)
+ half_length = self._field_dims.full_field_half_length
+ half_width = self._field_dims.full_field_half_width
+
+ if self._waiting_for_restart:
+ if abs(ball_pos.x) < half_length - 0.25 and abs(ball_pos.y) < half_width - 0.25:
+ self._waiting_for_restart = False
+ self._phase = "approach"
+ else:
+ return self._stop_all()
+
+ target_name = self._TARGETS[self._target_index]
+ if self._has_reached_target(ball_pos, target_name, game, half_length, half_width):
+ if self._target_index < len(self._TARGETS) - 1:
+ self._target_index += 1
+ self._waiting_for_restart = True
+ self._phase = "approach"
+ return self._stop_all()
+
+ target_point = self._target_point(target_name, ball_pos, game, half_length, half_width)
+ direction = target_point - ball_pos
+ if direction.mag() == 0.0:
+ return self._stop_all()
+ direction = Vector2D(direction.x / direction.mag(), direction.y / direction.mag())
+
+ pusher_id = min(
+ game.friendly_robots,
+ key=lambda rid: game.friendly_robots[rid].p.distance_to(ball_pos),
+ )
+ pusher = game.friendly_robots[pusher_id]
+ setup_point = Vector2D(
+ ball_pos.x - direction.x * self._APPROACH_BACKOFF,
+ ball_pos.y - direction.y * self._APPROACH_BACKOFF,
+ )
+ push_oren = math.atan2(direction.y, direction.x)
+
+ for robot_id in game.friendly_robots:
+ if robot_id != pusher_id:
+ self.blackboard.cmd_map[robot_id] = empty_command(False)
+ continue
+
+ if self._phase == "approach":
+ if pusher.p.distance_to(setup_point) <= self._APPROACH_TOLERANCE:
+ self._phase = "align"
+ self._phase_started_at = game.ts
+ self.blackboard.cmd_map[robot_id] = move(
+ game,
+ motion_controller,
+ robot_id,
+ setup_point,
+ push_oren,
+ dribbling=False,
+ )
+ continue
+
+ if self._phase == "align":
+ aligned = abs(_angle_delta(push_oren, pusher.orientation)) <= self._ALIGN_TOLERANCE_RAD
+ near_setup = pusher.p.distance_to(setup_point) <= self._APPROACH_TOLERANCE * 1.8
+ if aligned and near_setup:
+ self._phase = "push"
+ self._phase_started_at = game.ts
+ else:
+ self.blackboard.cmd_map[robot_id] = move(
+ game,
+ motion_controller,
+ robot_id,
+ setup_point,
+ push_oren,
+ dribbling=False,
+ )
+ continue
+
+ if self._phase == "push":
+ if game.ts - self._phase_started_at <= self._PUSH_SECONDS:
+ self.blackboard.cmd_map[robot_id] = RobotCommand(
+ local_forward_vel=self._PUSH_SPEED,
+ local_left_vel=0,
+ angular_vel=0,
+ kick=0,
+ chip=0,
+ dribble=0,
+ )
+ continue
+ self._phase = "stop"
+ self._phase_started_at = game.ts
+
+ if self._phase == "stop":
+ self.blackboard.cmd_map[robot_id] = empty_command(False)
+ if game.ts - self._phase_started_at >= self._STOP_SECONDS:
+ self._phase = "approach"
+
+ return py_trees.common.Status.RUNNING
+
+ def _target_point(
+ self,
+ target_name: str,
+ ball_pos: Vector2D,
+ game,
+ half_length: float,
+ half_width: float,
+ ) -> Vector2D:
+ if target_name == "top":
+ return Vector2D(ball_pos.x, half_width + self._BOUNDARY_EPSILON)
+ if target_name == "bottom":
+ return Vector2D(ball_pos.x, -half_width - self._BOUNDARY_EPSILON)
+ if target_name == "left":
+ return Vector2D(-half_length - self._BOUNDARY_EPSILON, self._safe_touchline_y(ball_pos.y, game))
+ if target_name == "right":
+ return Vector2D(half_length + self._BOUNDARY_EPSILON, self._safe_touchline_y(ball_pos.y, game))
+
+ yellow_is_right = game.my_team_is_right == game.my_team_is_yellow
+ yellow_goal_x = half_length + self._GOAL_EPSILON if yellow_is_right else -half_length - self._GOAL_EPSILON
+ return Vector2D(yellow_goal_x, 0.0)
+
+ def _safe_touchline_y(self, current_y: float, game) -> float:
+ min_abs_y = game.field.half_goal_width + self._GOAL_MOUTH_CLEARANCE
+ sign = 1.0 if current_y >= 0.0 else -1.0
+ return sign * max(abs(current_y), min_abs_y)
+
+ def _has_reached_target(
+ self,
+ ball_pos: Vector2D,
+ target_name: str,
+ game,
+ half_length: float,
+ half_width: float,
+ ) -> bool:
+ if target_name == "top":
+ return ball_pos.y >= half_width - self._BOUNDARY_EPSILON
+ if target_name == "bottom":
+ return ball_pos.y <= -half_width + self._BOUNDARY_EPSILON
+ if target_name == "left":
+ return ball_pos.x <= -half_length + self._BOUNDARY_EPSILON and abs(ball_pos.y) > game.field.half_goal_width
+ if target_name == "right":
+ return ball_pos.x >= half_length - self._BOUNDARY_EPSILON and abs(ball_pos.y) > game.field.half_goal_width
+
+ yellow_is_right = game.my_team_is_right == game.my_team_is_yellow
+ if yellow_is_right:
+ return ball_pos.x >= half_length - self._GOAL_EPSILON and abs(ball_pos.y) < game.field.half_goal_width
+ return ball_pos.x <= -half_length + self._GOAL_EPSILON and abs(ball_pos.y) < game.field.half_goal_width
+
+ def _stop_all(self) -> py_trees.common.Status:
+ game = self.blackboard.game
+ for robot_id in game.friendly_robots:
+ self.blackboard.cmd_map[robot_id] = empty_command(False)
+ return py_trees.common.Status.RUNNING
+
+
+class DeliberateOutOfBoundsStrategy(AbstractStrategy):
+ """Opponent strategy that pushes the ball out once per side, then scores on yellow."""
+
+ def __init__(self, field_dims: FieldDimensions) -> None:
+ self._field_dims = field_dims
+ super().__init__()
+
+ def assert_exp_robots(self, n_runtime_friendly: int, n_runtime_enemy: int) -> bool:
+ return n_runtime_friendly >= 1
+
+ def assert_exp_goals(self, includes_my_goal_line: bool, includes_opp_goal_line: bool) -> bool:
+ return True
+
+ def get_min_bounding_req(self):
+ return None
+
+ def create_behaviour_tree(self) -> py_trees.behaviour.Behaviour:
+ root = py_trees.composites.Sequence(name="DeliberateOutOfBoundsRoot", memory=False)
+ root.add_child(DeliberateOutOfBoundsStep(self._field_dims))
+ return root
diff --git a/utama_core/strategy/referee/actions.py b/utama_core/strategy/referee/actions.py
index facc9093..09b19df8 100644
--- a/utama_core/strategy/referee/actions.py
+++ b/utama_core/strategy/referee/actions.py
@@ -27,7 +27,7 @@
)
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.referee.referee_command import RefereeCommand
-from utama_core.skills.src.utils.move_utils import empty_command, move
+from utama_core.skills.src.utils.move_utils import empty_command, move, turn_on_spot
from utama_core.strategy.common.abstract_behaviour import AbstractBehaviour
@@ -301,6 +301,16 @@ class BallPlacementOursStep(AbstractBehaviour):
designated position. All other robots clear away from the ball.
"""
+ _RELEASE_DELAY_SECONDS = 0.25
+
+ def setup_(self):
+ self._release_started_at: float | None = None
+ self._placer_id: int | None = None
+
+ def _reset_release(self) -> None:
+ self._release_started_at = None
+ self._placer_id = None
+
def update(self) -> py_trees.common.Status:
game = self.blackboard.game
ref = game.referee
@@ -309,37 +319,67 @@ def update(self) -> py_trees.common.Status:
# Determine which team is ours
our_team = ref.yellow_team if game.my_team_is_yellow else ref.blue_team
if getattr(our_team, "can_place_ball", None) is False:
+ self._reset_release()
return _all_stop(self.blackboard)
target = ref.designated_position
if target is None:
+ self._reset_release()
return _all_stop(self.blackboard)
target_pos = Vector2D(target[0], target[1])
ball = game.ball
if ball is None:
+ self._reset_release()
return _all_stop(self.blackboard)
if ball.p.distance_to(target_pos) <= BALL_PLACEMENT_DONE_DISTANCE:
- return _all_stop(self.blackboard)
+ if self._release_started_at is None:
+ self._release_started_at = game.ts
+
+ placer_id = self._placer_id
+ if placer_id not in game.friendly_robots:
+ placer_id = min(
+ game.friendly_robots,
+ key=lambda rid: game.friendly_robots[rid].p.distance_to(ball.p),
+ )
+
+ hold_dribbler = game.ts - self._release_started_at < self._RELEASE_DELAY_SECONDS
+ for robot_id in game.friendly_robots:
+ self.blackboard.cmd_map[robot_id] = empty_command(hold_dribbler and robot_id == placer_id)
+ return py_trees.common.Status.RUNNING
+
+ self._release_started_at = None
# Pick the placer: robot closest to the ball
placer_id = min(
game.friendly_robots,
key=lambda rid: game.friendly_robots[rid].p.distance_to(ball.p),
)
+ self._placer_id = placer_id
+
+ _FACE_READY_ANGLE = 0.2
for robot_id in game.friendly_robots:
if robot_id == placer_id:
robot = game.friendly_robots[robot_id]
if robot.has_ball:
- target_for_move = target_pos
+ oren = robot.p.angle_to(target_pos)
+ face_error = math.atan2(math.sin(oren - robot.orientation), math.cos(oren - robot.orientation))
+ if abs(face_error) > _FACE_READY_ANGLE:
+ self.blackboard.cmd_map[robot_id] = turn_on_spot(
+ game, motion_controller, robot_id, oren, dribbling=True
+ )
+ else:
+ self.blackboard.cmd_map[robot_id] = move(
+ game, motion_controller, robot_id, target_pos, oren, dribbling=True
+ )
else:
target_for_move = Vector2D(ball.p.x, ball.p.y)
- oren = robot.p.angle_to(target_for_move)
- self.blackboard.cmd_map[robot_id] = move(
- game, motion_controller, robot_id, target_for_move, oren, dribbling=True
- )
+ oren = robot.p.angle_to(target_for_move)
+ self.blackboard.cmd_map[robot_id] = move(
+ game, motion_controller, robot_id, target_for_move, oren, dribbling=True
+ )
return _clear_to_legal_positions(
self.blackboard,
ball_keep_dist=BALL_KEEP_OUT_DISTANCE,
@@ -547,8 +587,26 @@ class DirectFreeOursStep(AbstractBehaviour):
All other robots stop in place.
"""
- # How far infield from the ball the approach point is placed
- _APPROACH_OFFSET = 0.15
+ # How close the robot center should get behind the ball before turning/kicking.
+ _APPROACH_OFFSET = ROBOT_RADIUS + 0.03
+ _APPROACH_READY_DISTANCE = 0.04
+ _KICK_READY_DISTANCE = 0.16
+ _FACE_READY_ANGLE = 0.18
+
+ @staticmethod
+ def _angle_error(current: float, target: float) -> float:
+ return math.atan2(math.sin(target - current), math.cos(target - current))
+
+ def _nearest_enemy_to_ball(self, game, ball_pos: Vector2D):
+ if not game.enemy_robots:
+ return None, None
+ enemy_id = min(game.enemy_robots, key=lambda rid: game.enemy_robots[rid].p.distance_to(ball_pos))
+ return enemy_id, game.enemy_robots[enemy_id]
+
+ def _kick_target_enemy(self, game, ball_pos: Vector2D):
+ if 1 in game.enemy_robots:
+ return 1, game.enemy_robots[1]
+ return self._nearest_enemy_to_ball(game, ball_pos)
def update(self) -> py_trees.common.Status:
game = self.blackboard.game
@@ -564,22 +622,33 @@ def update(self) -> py_trees.common.Status:
if robot_id == kicker_id and ball:
robot = game.friendly_robots[robot_id]
ball_pos = Vector2D(ball.p.x, ball.p.y)
+ distance_to_ball = robot.p.distance_to(ball_pos)
+ _, enemy = self._kick_target_enemy(game, ball_pos)
- # Compute an approach point offset inward from the nearest boundary,
- # so the robot never drives through the ball toward the edge.
- half_length = _field_half_length(game)
- half_width = _field_half_width(game)
- # Inward direction: push away from whichever boundary is closest
- offset_x = 0.0
- offset_y = 0.0
- if abs(ball_pos.x) > half_length - 0.5:
- offset_x = self._APPROACH_OFFSET * (-1.0 if ball_pos.x > 0 else 1.0)
- if abs(ball_pos.y) > half_width - 0.5:
- offset_y = self._APPROACH_OFFSET * (-1.0 if ball_pos.y > 0 else 1.0)
-
- approach = Vector2D(ball_pos.x + offset_x, ball_pos.y + offset_y)
- oren = robot.p.angle_to(approach)
- self.blackboard.cmd_map[robot_id] = move(game, motion_controller, robot_id, approach, oren)
+ if enemy is None:
+ target_oren = robot.p.angle_to(ball_pos)
+ else:
+ target_oren = ball_pos.angle_to(enemy.p)
+
+ kick_dir = Vector2D(math.cos(target_oren), math.sin(target_oren))
+ approach = Vector2D(
+ ball_pos.x - kick_dir.x * self._APPROACH_OFFSET,
+ ball_pos.y - kick_dir.y * self._APPROACH_OFFSET,
+ )
+ approach = _clamp_to_field(approach, game)
+ distance_to_approach = robot.p.distance_to(approach)
+ face_error = self._angle_error(robot.orientation, target_oren)
+
+ if distance_to_approach > self._APPROACH_READY_DISTANCE:
+ self.blackboard.cmd_map[robot_id] = move(game, motion_controller, robot_id, approach, target_oren)
+ elif abs(face_error) > self._FACE_READY_ANGLE:
+ self.blackboard.cmd_map[robot_id] = turn_on_spot(
+ game, motion_controller, robot_id, target_oren, dribbling=False
+ )
+ elif distance_to_ball > self._KICK_READY_DISTANCE:
+ self.blackboard.cmd_map[robot_id] = move(game, motion_controller, robot_id, approach, target_oren)
+ else:
+ self.blackboard.cmd_map[robot_id] = empty_command(False)
else:
self.blackboard.cmd_map[robot_id] = empty_command(False)
diff --git a/utama_core/team_controller/src/controllers/sim/rsim_controller.py b/utama_core/team_controller/src/controllers/sim/rsim_controller.py
index e6ab263f..8879cdec 100644
--- a/utama_core/team_controller/src/controllers/sim/rsim_controller.py
+++ b/utama_core/team_controller/src/controllers/sim/rsim_controller.py
@@ -43,3 +43,8 @@ def _get_teleport_location(self, robot_id: int, is_team_yellow: bool, add: bool)
@property
def env(self):
return self._env
+
+ @property
+ def latest_sent_commands(self):
+ """Most recent low-level rSim commands, if a frame has been stepped."""
+ return self._env.sent_commands
diff --git a/utama_core/team_controller/src/controllers/sim/rsim_robot_controller.py b/utama_core/team_controller/src/controllers/sim/rsim_robot_controller.py
index bb1504bd..fb2f75fd 100644
--- a/utama_core/team_controller/src/controllers/sim/rsim_robot_controller.py
+++ b/utama_core/team_controller/src/controllers/sim/rsim_robot_controller.py
@@ -143,6 +143,11 @@ def is_team_yellow(self):
def env(self):
return self._env
+ @property
+ def latest_sent_commands(self):
+ """Most recent low-level rSim commands, if a frame has been stepped."""
+ return self._env.sent_commands
+
@property
def robots_info(self):
return self._robots_info
diff --git a/utama_core/tests/abstract_strategy/test_assertions.py b/utama_core/tests/abstract_strategy/test_assertions.py
index 85bcf729..c37bffeb 100644
--- a/utama_core/tests/abstract_strategy/test_assertions.py
+++ b/utama_core/tests/abstract_strategy/test_assertions.py
@@ -1,14 +1,20 @@
from types import SimpleNamespace
+import py_trees
import pytest
from utama_core.config.field_params import STANDARD_FIELD_DIMS
from utama_core.entities.game.field import Field, FieldBounds
+from utama_core.entities.referee.referee_command import RefereeCommand
from utama_core.strategy.common import (
AbstractBehaviour,
AbstractStrategy,
SpaceRequirements,
)
+from utama_core.strategy.common.abstract_strategy import (
+ _REFEREE_STOPPAGE_COMMANDS,
+ _ResetStrategyOnRefereeStoppage,
+)
# Dummy blackboard helper
@@ -118,3 +124,119 @@ def test_space_requirements_rejected_when_field_too_small():
game = make_dummy_game(actual_field)
with pytest.raises(ValueError, match="too small for strategy"):
strategy.assert_field_requirements(game)
+
+
+def test_strategy_subtree_resets_once_when_referee_enters_stoppage():
+ strategy = ResetRecorderStrategy()
+ strategy.setup_strategy_blackboard(is_opp_strat=False)
+ strategy.setup_behaviour_tree(is_opp_strat=False)
+ strategy.blackboard.game = make_referee_game(RefereeCommand.FORCE_START, timestamp=0.0)
+ strategy.blackboard.cmd_map = {}
+
+ strategy.behaviour_tree.tick()
+ assert strategy.recorder.initialise_count == 1
+ assert strategy.recorder.status == py_trees.common.Status.RUNNING
+
+ strategy.blackboard.game.referee = make_referee_data(RefereeCommand.HALT, timestamp=1.0)
+ strategy.behaviour_tree.tick()
+
+ assert strategy.recorder.invalidated_count == 1
+ assert strategy.recorder.status == py_trees.common.Status.INVALID
+
+ strategy.behaviour_tree.tick()
+ assert strategy.recorder.invalidated_count == 1
+
+ strategy.blackboard.game.referee = make_referee_data(RefereeCommand.FORCE_START, timestamp=2.0)
+ strategy.behaviour_tree.tick()
+
+ assert strategy.recorder.initialise_count == 2
+ assert strategy.recorder.status == py_trees.common.Status.RUNNING
+
+
+def test_active_referee_start_commands_do_not_reset_strategy_subtree():
+ strategy = ResetRecorderStrategy()
+ strategy.setup_strategy_blackboard(is_opp_strat=False)
+ strategy.setup_behaviour_tree(is_opp_strat=False)
+ strategy.blackboard.game = make_referee_game(RefereeCommand.NORMAL_START, timestamp=0.0)
+ strategy.blackboard.cmd_map = {}
+
+ strategy.behaviour_tree.tick()
+ strategy.blackboard.game.referee = make_referee_data(RefereeCommand.FORCE_START, timestamp=1.0)
+ strategy.behaviour_tree.tick()
+
+ assert strategy.recorder.initialise_count == 1
+ assert strategy.recorder.invalidated_count == 0
+
+
+@pytest.mark.parametrize("command", sorted(_REFEREE_STOPPAGE_COMMANDS, key=lambda item: item.value))
+def test_reset_guard_invalidates_for_every_stoppage_command(command):
+ target = RunningRecorder(name="Target")
+ target.status = py_trees.common.Status.RUNNING
+ guard = _ResetStrategyOnRefereeStoppage(target=target)
+ guard.blackboard = SimpleNamespace(game=make_referee_game(command, timestamp=1.0))
+
+ assert guard.update() == py_trees.common.Status.SUCCESS
+ assert target.invalidated_count == 1
+
+
+def test_reset_guard_ignores_active_start_commands():
+ target = RunningRecorder(name="Target")
+ target.status = py_trees.common.Status.RUNNING
+ guard = _ResetStrategyOnRefereeStoppage(target=target)
+ guard.blackboard = SimpleNamespace(game=make_referee_game(RefereeCommand.NORMAL_START, timestamp=1.0))
+
+ assert guard.update() == py_trees.common.Status.SUCCESS
+ assert target.invalidated_count == 0
+
+
+class ResetRecorderStrategy(AbstractStrategy):
+ exp_ball = True
+
+ def __init__(self):
+ self.recorder = RunningRecorder(name="Recorder")
+ super().__init__()
+
+ def create_behaviour_tree(self):
+ return self.recorder
+
+ def assert_exp_robots(self, n_runtime_friendly: int, n_runtime_enemy: int):
+ return True
+
+ def assert_exp_goals(self, includes_my_goal_line: bool, includes_opp_goal_line: bool):
+ return True
+
+ def get_min_bounding_req(self):
+ return None
+
+
+class RunningRecorder(py_trees.behaviour.Behaviour):
+ def __init__(self, name):
+ super().__init__(name=name)
+ self.initialise_count = 0
+ self.invalidated_count = 0
+
+ def initialise(self):
+ self.initialise_count += 1
+
+ def update(self):
+ return py_trees.common.Status.RUNNING
+
+ def terminate(self, new_status):
+ if new_status == py_trees.common.Status.INVALID:
+ self.invalidated_count += 1
+
+
+def make_referee_game(command, timestamp):
+ return SimpleNamespace(
+ referee=make_referee_data(command, timestamp),
+ friendly_robots={},
+ my_team_is_yellow=True,
+ my_team_is_right=False,
+ )
+
+
+def make_referee_data(command, timestamp):
+ return SimpleNamespace(
+ referee_command=command,
+ referee_command_timestamp=timestamp,
+ )
diff --git a/utama_core/tests/common/test_blackboard_contract.py b/utama_core/tests/common/test_blackboard_contract.py
new file mode 100644
index 00000000..8eebc0f7
--- /dev/null
+++ b/utama_core/tests/common/test_blackboard_contract.py
@@ -0,0 +1,123 @@
+from __future__ import annotations
+
+import py_trees
+import pytest
+from py_trees.blackboard import Blackboard
+
+from utama_core.strategy.common import AbstractBehaviour
+from utama_core.strategy.common.blackboard_contract import (
+ BlackboardKeySpec,
+ declared_contract,
+ register_blackboard_contract,
+ validate_blackboard_contracts,
+)
+
+
+class ContractOnlyBehaviour(AbstractBehaviour):
+ BLACKBOARD_CONTRACT = (
+ BlackboardKeySpec(
+ name="robot_id",
+ key_attr="robot_id_key",
+ access="READ",
+ type_name="int",
+ ),
+ BlackboardKeySpec(
+ name="target_orientation",
+ key_attr="target_orientation_key",
+ access=py_trees.common.Access.WRITE,
+ type_name="float",
+ ),
+ BlackboardKeySpec(
+ name="optional_target",
+ key_attr="optional_target_key",
+ access="READ",
+ required=False,
+ ),
+ )
+
+ def __init__(self):
+ super().__init__(name="ContractOnly")
+ self.robot_id_key = "robot_id"
+ self.target_orientation_key = "target_orientation"
+ self.optional_target_key = None
+
+ def setup_(self):
+ pass
+
+ def update(self):
+ return py_trees.common.Status.SUCCESS
+
+
+def test_runtime_key_resolves_direct_key_key_attr_optional_and_class_level_docs():
+ node = ContractOnlyBehaviour()
+
+ assert BlackboardKeySpec(name="direct", key="actual", access="READ").runtime_key(node) == "actual"
+ assert BlackboardKeySpec(name="robot_id", key_attr="robot_id_key", access="READ").runtime_key(node) == "robot_id"
+ assert (
+ BlackboardKeySpec(name="optional", key_attr="optional_target_key", access="READ", required=False).runtime_key(
+ node
+ )
+ is None
+ )
+ assert BlackboardKeySpec(name="robot_id", key_attr="robot_id_key", access="READ").runtime_key() == "robot_id"
+
+
+def test_declared_contract_is_doc_friendly_without_instance():
+ contracts = declared_contract(ContractOnlyBehaviour)
+
+ assert contracts[0]["key"] == "robot_id"
+ assert contracts[0]["access"] == "READ"
+ assert contracts[1]["access"] == "WRITE"
+
+
+def test_setup_auto_registers_contract_keys():
+ Blackboard.clear()
+ node = ContractOnlyBehaviour()
+
+ node.setup(is_opp_strat=False)
+
+ assert "/My/robot_id" in node.blackboard.read
+ assert "/My/target_orientation" in node.blackboard.write
+ assert "/My/optional_target" not in node.blackboard.read
+ Blackboard.clear()
+
+
+def test_register_blackboard_contract_rejects_missing_required_attr():
+ Blackboard.clear()
+ node = ContractOnlyBehaviour()
+ node.BLACKBOARD_CONTRACT = (BlackboardKeySpec(name="missing", key_attr="missing_key", access="READ"),)
+ node.blackboard = py_trees.blackboard.Client(name="Test", namespace="/My")
+
+ with pytest.raises(ValueError, match="missing attribute 'missing_key'"):
+ register_blackboard_contract(node)
+ Blackboard.clear()
+
+
+def test_register_blackboard_contract_rejects_invalid_access():
+ Blackboard.clear()
+ node = ContractOnlyBehaviour()
+ node.BLACKBOARD_CONTRACT = (BlackboardKeySpec(name="robot_id", key_attr="robot_id_key", access="MUTATE"),)
+ node.blackboard = py_trees.blackboard.Client(name="Test", namespace="/My")
+
+ with pytest.raises(ValueError, match="invalid access 'MUTATE'"):
+ register_blackboard_contract(node)
+ Blackboard.clear()
+
+
+def test_validate_blackboard_contracts_reports_missing_read_writer():
+ node = ContractOnlyBehaviour()
+
+ validation = validate_blackboard_contracts(node)
+
+ assert validation["status"] == "invalid"
+ assert {error["code"] for error in validation["errors"]} == {"missing_writer"}
+ assert {error["key"] for error in validation["errors"]} == {"robot_id"}
+
+
+def test_validate_blackboard_contracts_accepts_seeded_inputs():
+ node = ContractOnlyBehaviour()
+
+ validation = validate_blackboard_contracts(node, seeded_keys={"robot_id"})
+
+ assert validation["status"] == "valid"
+ assert validation["errors"] == []
diff --git a/utama_core/tests/custom_referee/test_custom_referee.py b/utama_core/tests/custom_referee/test_custom_referee.py
index 1fb17557..9819cdfd 100644
--- a/utama_core/tests/custom_referee/test_custom_referee.py
+++ b/utama_core/tests/custom_referee/test_custom_referee.py
@@ -1,581 +1,582 @@
-"""Unit tests for the CustomReferee system."""
-
-from __future__ import annotations
-
-import math
-from unittest.mock import MagicMock
-
-import pytest
-
-from utama_core.config.field_params import STANDARD_FIELD_DIMS
-from utama_core.custom_referee.custom_referee import CustomReferee
-from utama_core.custom_referee.geometry import RefereeGeometry
-from utama_core.custom_referee.profiles.profile_loader import load_profile
-from utama_core.custom_referee.rules.defense_area_rule import DefenseAreaRule
-from utama_core.custom_referee.rules.goal_rule import GoalRule
-from utama_core.custom_referee.rules.keep_out_rule import KeepOutRule
-from utama_core.custom_referee.rules.out_of_bounds_rule import OutOfBoundsRule
-from utama_core.custom_referee.state_machine import GameStateMachine
-from utama_core.entities.data.referee import RefereeData
-from utama_core.entities.data.vector import Vector2D, Vector3D
-from utama_core.entities.game.ball import Ball
-from utama_core.entities.game.game_frame import GameFrame
-from utama_core.entities.game.robot import Robot
-from utama_core.entities.referee.referee_command import RefereeCommand
-from utama_core.entities.referee.stage import Stage
-
-# ---------------------------------------------------------------------------
-# Helpers
-# ---------------------------------------------------------------------------
-
-GEO = RefereeGeometry.from_field_dims(STANDARD_FIELD_DIMS)
-
-
-def _ball(x: float, y: float, z: float = 0.0) -> Ball:
- return Ball(p=Vector3D(x, y, z), v=Vector3D(0, 0, 0), a=Vector3D(0, 0, 0))
-
-
-def _robot(robot_id: int, x: float, y: float, is_friendly: bool, has_ball: bool = False) -> Robot:
- return Robot(
- id=robot_id,
- is_friendly=is_friendly,
- has_ball=has_ball,
- p=Vector2D(x, y),
- v=Vector2D(0, 0),
- a=Vector2D(0, 0),
- orientation=0.0,
- )
-
-
-def _frame(
- ball: Ball,
- friendly_robots: dict | None = None,
- enemy_robots: dict | None = None,
- my_team_is_yellow: bool = True,
- my_team_is_right: bool = False,
- ts: float = 10.0,
-) -> GameFrame:
- return GameFrame(
- ts=ts,
- my_team_is_yellow=my_team_is_yellow,
- my_team_is_right=my_team_is_right,
- friendly_robots=friendly_robots or {},
- enemy_robots=enemy_robots or {},
- ball=ball,
- referee=None,
- )
-
-
-def _state_machine() -> GameStateMachine:
- sm = GameStateMachine(
- half_duration_seconds=300.0,
- kickoff_team="yellow",
- n_robots_yellow=3,
- n_robots_blue=3,
- )
- sm.seed_clock(0.0)
- sm.set_command(RefereeCommand.NORMAL_START, 0.0)
- return sm
-
-
-# ---------------------------------------------------------------------------
-# GoalRule
-# ---------------------------------------------------------------------------
-
-
-class TestGoalRule:
- def test_right_goal_blue_scores_when_yellow_is_right(self):
- # Yellow defends right goal → ball in right goal → blue scored → yellow kicks off.
- rule = GoalRule(cooldown_seconds=1.0)
- frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True)
- violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
- assert violation.rule_name == "goal"
- assert violation.status_message == "Goal by Blue"
- assert violation.next_command == RefereeCommand.PREPARE_KICKOFF_YELLOW
-
- def test_left_goal_yellow_scores_when_yellow_is_right(self):
- # Blue defends left goal → ball in left goal → yellow scored → blue kicks off.
- rule = GoalRule(cooldown_seconds=1.0)
- frame = _frame(ball=_ball(-5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True)
- violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
- assert violation.status_message == "Goal by Yellow"
- assert violation.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE
-
- def test_right_goal_yellow_scores_when_yellow_is_left(self):
- # Blue defends right goal → ball in right goal → yellow scored → blue kicks off.
- rule = GoalRule(cooldown_seconds=1.0)
- frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=False)
- violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
- assert violation.status_message == "Goal by Yellow"
- assert violation.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE
-
- def test_no_goal_wide_shot(self):
- rule = GoalRule()
- frame = _frame(ball=_ball(5.0, 1.0)) # y=1.0 > half_goal_width=0.5
- assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
-
- def test_goal_respects_cooldown(self):
- rule = GoalRule(cooldown_seconds=2.0)
- frame1 = _frame(ball=_ball(5.0, 0.0), my_team_is_right=True, ts=10.0)
- v1 = rule.check(frame1, GEO, RefereeCommand.NORMAL_START)
- assert v1 is not None
-
- # Second detection within cooldown window — must be suppressed.
- frame2 = _frame(ball=_ball(5.0, 0.0), my_team_is_right=True, ts=10.5)
- v2 = rule.check(frame2, GEO, RefereeCommand.NORMAL_START)
- assert v2 is None
-
- # After cooldown expires — should fire again.
- frame3 = _frame(ball=_ball(5.0, 0.0), my_team_is_right=True, ts=13.0)
- v3 = rule.check(frame3, GEO, RefereeCommand.NORMAL_START)
- assert v3 is not None
-
- def test_no_detection_during_stop(self):
- rule = GoalRule()
- frame = _frame(ball=_ball(5.0, 0.0))
- assert rule.check(frame, GEO, RefereeCommand.STOP) is None
-
-
-# ---------------------------------------------------------------------------
-# OutOfBoundsRule
-# ---------------------------------------------------------------------------
-
-
-class TestOutOfBoundsRule:
- def test_ball_out_top(self):
- rule = OutOfBoundsRule()
- frame = _frame(ball=_ball(0.0, 3.5), my_team_is_yellow=True)
- violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
- assert violation.rule_name == "out_of_bounds"
-
- def test_ball_out_right_side(self):
- rule = OutOfBoundsRule()
- frame = _frame(ball=_ball(5.0, 1.0)) # wide — not in goal (y=1.0 > 0.5)
- violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
-
- def test_ball_in_field_no_violation(self):
- rule = OutOfBoundsRule()
- frame = _frame(ball=_ball(0.0, 0.0))
- assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
-
- def test_ball_in_goal_no_out_of_bounds(self):
- rule = OutOfBoundsRule()
- frame = _frame(ball=_ball(5.0, 0.0)) # in right goal
- # GoalRule handles this; OutOfBoundsRule must not also fire.
- assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
-
- def test_free_kick_assigned_to_non_touching_team_friendly_yellow(self):
- """Friendly (yellow) last touched → enemy (blue) gets free kick."""
- rule = OutOfBoundsRule()
- friendly = {0: _robot(0, 4.4, 2.9, is_friendly=True, has_ball=True)}
- frame_before = _frame(ball=_ball(4.4, 2.9), friendly_robots=friendly, my_team_is_yellow=True, ts=9.9)
- rule.check(frame_before, GEO, RefereeCommand.NORMAL_START)
-
- frame_out = _frame(ball=_ball(0.0, 3.5), my_team_is_yellow=True, ts=10.0)
- violation = rule.check(frame_out, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
- assert violation.next_command == RefereeCommand.DIRECT_FREE_BLUE
-
- def test_designated_position_is_infield(self):
- rule = OutOfBoundsRule()
- frame = _frame(ball=_ball(0.0, 3.5))
- violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert violation is not None
- px, py = violation.designated_position
- assert abs(py) < GEO.half_width # placed infield
-
-
-# ---------------------------------------------------------------------------
-# DefenseAreaRule
-# ---------------------------------------------------------------------------
-
-
-class TestDefenseAreaRule:
- def _frame_with_attacker_in_defense(self, my_team_is_right: bool = False) -> GameFrame:
- # Enemy robot inside my (left) defense area.
- enemy = {0: _robot(0, -4.3, 0.5, is_friendly=False)}
- return _frame(
- ball=_ball(0, 0),
- enemy_robots=enemy,
- my_team_is_right=my_team_is_right,
- )
-
- def test_fires_during_normal_start(self):
- rule = DefenseAreaRule()
- frame = self._frame_with_attacker_in_defense(my_team_is_right=False)
- v = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert v is not None
- assert v.rule_name == "defense_area"
-
- def test_does_not_fire_during_stop(self):
- rule = DefenseAreaRule()
- frame = self._frame_with_attacker_in_defense()
- assert rule.check(frame, GEO, RefereeCommand.STOP) is None
-
- def test_does_not_fire_during_force_start_no_actually_fires(self):
- rule = DefenseAreaRule()
- frame = self._frame_with_attacker_in_defense(my_team_is_right=False)
- v = rule.check(frame, GEO, RefereeCommand.FORCE_START)
- assert v is not None
-
- def test_too_many_defenders(self):
- rule = DefenseAreaRule(max_defenders=1)
- # Two friendly robots in own (left) defense area.
- friendly = {
- 0: _robot(0, -4.3, 0.0, is_friendly=True),
- 1: _robot(1, -4.3, 0.5, is_friendly=True),
- }
- frame = _frame(ball=_ball(0, 0), friendly_robots=friendly, my_team_is_right=False, my_team_is_yellow=True)
- v = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
- assert v is not None
- assert v.next_command == RefereeCommand.DIRECT_FREE_BLUE
-
-
-# ---------------------------------------------------------------------------
-# KeepOutRule
-# ---------------------------------------------------------------------------
-
-
-class TestKeepOutRule:
- def test_no_trigger_before_persistence_threshold(self):
- rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=5)
- friendly = {0: _robot(0, 0.2, 0.0, is_friendly=True)}
- # Enemy is kicking → check friendly.
- for _ in range(4):
- frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
- v = rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
- assert v is None
-
- def test_triggers_after_persistence_threshold(self):
- rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=5)
- friendly = {0: _robot(0, 0.2, 0.0, is_friendly=True)}
- v = None
- for _ in range(5):
- frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
- v = rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
- assert v is not None
- assert v.rule_name == "keep_out"
-
- def test_resets_on_non_violation_frame(self):
- rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=5)
- friendly = {0: _robot(0, 0.2, 0.0, is_friendly=True)}
- for _ in range(4):
- frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
- rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
-
- # Robot moves away — count resets.
- far_frame = _frame(
- ball=_ball(0.0, 0.0),
- friendly_robots={0: _robot(0, 2.0, 0.0, is_friendly=True)},
- )
- rule.check(far_frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
-
- # Needs another full persistence run to trigger.
- v = None
- for _ in range(5):
- frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
- v = rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
- assert v is not None
-
- def test_inactive_during_normal_start(self):
- rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=1)
- friendly = {0: _robot(0, 0.1, 0.0, is_friendly=True)}
- frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
- assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
-
-
-# ---------------------------------------------------------------------------
-# GameStateMachine
-# ---------------------------------------------------------------------------
-
-
-class TestGameStateMachine:
- def test_goal_increments_yellow_score(self):
- from utama_core.custom_referee.rules.base_rule import RuleViolation
-
- sm = _state_machine()
- violation = RuleViolation(
- rule_name="goal",
- suggested_command=RefereeCommand.STOP,
- next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
- status_message="Goal by Yellow",
- )
- data = sm.step(current_time=10.0, violation=violation)
- assert sm.yellow_team.score == 1
- assert sm.blue_team.score == 0
- assert data.referee_command == RefereeCommand.STOP
- assert data.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE
-
- def test_goal_increments_blue_score(self):
- from utama_core.custom_referee.rules.base_rule import RuleViolation
-
- sm = _state_machine()
- violation = RuleViolation(
- rule_name="goal",
- suggested_command=RefereeCommand.STOP,
- next_command=RefereeCommand.PREPARE_KICKOFF_YELLOW,
- status_message="Goal by Blue",
- )
- sm.step(current_time=10.0, violation=violation)
- assert sm.blue_team.score == 1
- assert sm.yellow_team.score == 0
-
- def test_transition_cooldown_suppresses_duplicate(self):
- from utama_core.custom_referee.rules.base_rule import RuleViolation
-
- sm = _state_machine()
- violation = RuleViolation(
- rule_name="goal",
- suggested_command=RefereeCommand.STOP,
- next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
- status_message="Goal",
- )
- sm.step(current_time=10.0, violation=violation)
- assert sm.yellow_team.score == 1
-
- # Second goal within cooldown window — must be suppressed.
- sm.step(current_time=10.1, violation=violation)
- assert sm.yellow_team.score == 1 # still 1
-
- def test_goal_sets_designated_position_to_centre(self):
- from utama_core.custom_referee.rules.base_rule import RuleViolation
-
- sm = _state_machine()
- violation = RuleViolation(
- rule_name="goal",
- suggested_command=RefereeCommand.STOP,
- next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
- status_message="Goal by Yellow",
- )
- data = sm.step(current_time=10.0, violation=violation)
- assert data.designated_position == (0.0, 0.0)
-
- def test_goal_status_message_is_propagated_into_referee_data(self):
- from utama_core.custom_referee.rules.base_rule import RuleViolation
-
- sm = _state_machine()
- violation = RuleViolation(
- rule_name="goal",
- suggested_command=RefereeCommand.STOP,
- next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
- status_message="Goal by Yellow",
- )
- data = sm.step(current_time=10.0, violation=violation)
- assert data.status_message == "Goal by Yellow"
-
- def test_manual_command_clears_status_message(self):
- from utama_core.custom_referee.rules.base_rule import RuleViolation
-
- sm = _state_machine()
- violation = RuleViolation(
- rule_name="goal",
- suggested_command=RefereeCommand.STOP,
- next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
- status_message="Goal by Yellow",
- )
- sm.step(current_time=10.0, violation=violation)
-
- sm.set_command(RefereeCommand.NORMAL_START, timestamp=11.0)
- data = sm.step(current_time=11.0, violation=None)
- assert data.status_message is None
-
- def test_manual_set_command(self):
- sm = _state_machine()
- sm.set_command(RefereeCommand.NORMAL_START, timestamp=5.0)
- assert sm.command == RefereeCommand.NORMAL_START
-
- def test_manual_ball_placement_auto_advances_from_stop_and_then_to_normal_start(self):
- sm = _state_machine()
- sm.set_command(RefereeCommand.BALL_PLACEMENT_YELLOW, timestamp=1.0)
- sm.ball_placement_target = (0.0, 0.0)
-
- clear_frame = _frame(ball=_ball(2.0, 0.0), ts=10.0)
- data = sm.step(current_time=10.0, violation=None, game_frame=clear_frame)
+"""Unit tests for the CustomReferee system."""
+
+from __future__ import annotations
+
+import math
+from unittest.mock import MagicMock
+
+import pytest
+
+from utama_core.config.field_params import STANDARD_FIELD_DIMS
+from utama_core.custom_referee.custom_referee import CustomReferee
+from utama_core.custom_referee.geometry import RefereeGeometry
+from utama_core.custom_referee.profiles.profile_loader import load_profile
+from utama_core.custom_referee.rules.defense_area_rule import DefenseAreaRule
+from utama_core.custom_referee.rules.goal_rule import GoalRule
+from utama_core.custom_referee.rules.keep_out_rule import KeepOutRule
+from utama_core.custom_referee.rules.out_of_bounds_rule import OutOfBoundsRule
+from utama_core.custom_referee.state_machine import GameStateMachine
+from utama_core.entities.data.referee import RefereeData
+from utama_core.entities.data.vector import Vector2D, Vector3D
+from utama_core.entities.game.ball import Ball
+from utama_core.entities.game.game_frame import GameFrame
+from utama_core.entities.game.robot import Robot
+from utama_core.entities.referee.referee_command import RefereeCommand
+from utama_core.entities.referee.stage import Stage
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+GEO = RefereeGeometry.from_field_dims(STANDARD_FIELD_DIMS)
+
+
+def _ball(x: float, y: float, z: float = 0.0) -> Ball:
+ return Ball(p=Vector3D(x, y, z), v=Vector3D(0, 0, 0), a=Vector3D(0, 0, 0))
+
+
+def _robot(robot_id: int, x: float, y: float, is_friendly: bool, has_ball: bool = False) -> Robot:
+ return Robot(
+ id=robot_id,
+ is_friendly=is_friendly,
+ has_ball=has_ball,
+ p=Vector2D(x, y),
+ v=Vector2D(0, 0),
+ a=Vector2D(0, 0),
+ orientation=0.0,
+ )
+
+
+def _frame(
+ ball: Ball,
+ friendly_robots: dict | None = None,
+ enemy_robots: dict | None = None,
+ my_team_is_yellow: bool = True,
+ my_team_is_right: bool = False,
+ ts: float = 10.0,
+) -> GameFrame:
+ return GameFrame(
+ ts=ts,
+ my_team_is_yellow=my_team_is_yellow,
+ my_team_is_right=my_team_is_right,
+ friendly_robots=friendly_robots or {},
+ enemy_robots=enemy_robots or {},
+ ball=ball,
+ referee=None,
+ )
+
+
+def _state_machine() -> GameStateMachine:
+ sm = GameStateMachine(
+ half_duration_seconds=300.0,
+ kickoff_team="yellow",
+ n_robots_yellow=3,
+ n_robots_blue=3,
+ )
+ sm.seed_clock(0.0)
+ sm.set_command(RefereeCommand.NORMAL_START, 0.0)
+ return sm
+
+
+# ---------------------------------------------------------------------------
+# GoalRule
+# ---------------------------------------------------------------------------
+
+
+class TestGoalRule:
+ def test_right_goal_blue_scores_when_yellow_is_right(self):
+ # Yellow defends right goal → ball in right goal → blue scored → yellow kicks off.
+ rule = GoalRule(cooldown_seconds=1.0)
+ frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True)
+ violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+ assert violation.rule_name == "goal"
+ assert violation.status_message == "Goal by Blue"
+ assert violation.next_command == RefereeCommand.PREPARE_KICKOFF_YELLOW
+
+ def test_left_goal_yellow_scores_when_yellow_is_right(self):
+ # Blue defends left goal → ball in left goal → yellow scored → blue kicks off.
+ rule = GoalRule(cooldown_seconds=1.0)
+ frame = _frame(ball=_ball(-5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True)
+ violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+ assert violation.status_message == "Goal by Yellow"
+ assert violation.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE
+
+ def test_right_goal_yellow_scores_when_yellow_is_left(self):
+ # Blue defends right goal → ball in right goal → yellow scored → blue kicks off.
+ rule = GoalRule(cooldown_seconds=1.0)
+ frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=False)
+ violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+ assert violation.status_message == "Goal by Yellow"
+ assert violation.next_command == RefereeCommand.PREPARE_KICKOFF_BLUE
+
+ def test_no_goal_wide_shot(self):
+ rule = GoalRule()
+ frame = _frame(ball=_ball(5.0, 1.0)) # y=1.0 > half_goal_width=0.5
+ assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
+
+ def test_goal_respects_cooldown(self):
+ rule = GoalRule(cooldown_seconds=2.0)
+ frame1 = _frame(ball=_ball(5.0, 0.0), my_team_is_right=True, ts=10.0)
+ v1 = rule.check(frame1, GEO, RefereeCommand.NORMAL_START)
+ assert v1 is not None
+
+ # Second detection within cooldown window — must be suppressed.
+ frame2 = _frame(ball=_ball(5.0, 0.0), my_team_is_right=True, ts=10.5)
+ v2 = rule.check(frame2, GEO, RefereeCommand.NORMAL_START)
+ assert v2 is None
+
+ # After cooldown expires — should fire again.
+ frame3 = _frame(ball=_ball(5.0, 0.0), my_team_is_right=True, ts=13.0)
+ v3 = rule.check(frame3, GEO, RefereeCommand.NORMAL_START)
+ assert v3 is not None
+
+ def test_no_detection_during_stop(self):
+ rule = GoalRule()
+ frame = _frame(ball=_ball(5.0, 0.0))
+ assert rule.check(frame, GEO, RefereeCommand.STOP) is None
+
+
+# ---------------------------------------------------------------------------
+# OutOfBoundsRule
+# ---------------------------------------------------------------------------
+
+
+class TestOutOfBoundsRule:
+ def test_ball_out_top(self):
+ rule = OutOfBoundsRule()
+ frame = _frame(ball=_ball(0.0, 3.5), my_team_is_yellow=True)
+ violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+ assert violation.rule_name == "out_of_bounds"
+
+ def test_ball_out_right_side(self):
+ rule = OutOfBoundsRule()
+ frame = _frame(ball=_ball(5.0, 1.0)) # wide — not in goal (y=1.0 > 0.5)
+ violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+
+ def test_ball_in_field_no_violation(self):
+ rule = OutOfBoundsRule()
+ frame = _frame(ball=_ball(0.0, 0.0))
+ assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
+
+ def test_ball_in_goal_no_out_of_bounds(self):
+ rule = OutOfBoundsRule()
+ frame = _frame(ball=_ball(5.0, 0.0)) # in right goal
+ # GoalRule handles this; OutOfBoundsRule must not also fire.
+ assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
+
+ def test_free_kick_assigned_to_non_touching_team_friendly_yellow(self):
+ """Friendly (yellow) last touched → enemy (blue) gets free kick."""
+ rule = OutOfBoundsRule()
+ friendly = {0: _robot(0, 4.4, 2.9, is_friendly=True, has_ball=True)}
+ frame_before = _frame(ball=_ball(4.4, 2.9), friendly_robots=friendly, my_team_is_yellow=True, ts=9.9)
+ rule.check(frame_before, GEO, RefereeCommand.NORMAL_START)
+
+ frame_out = _frame(ball=_ball(0.0, 3.5), my_team_is_yellow=True, ts=10.0)
+ violation = rule.check(frame_out, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+ assert violation.next_command == RefereeCommand.DIRECT_FREE_BLUE
+
+ def test_designated_position_is_infield(self):
+ rule = OutOfBoundsRule()
+ frame = _frame(ball=_ball(0.0, 3.5))
+ violation = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert violation is not None
+ px, py = violation.designated_position
+ assert abs(py) < GEO.half_width # placed infield
+
+
+# ---------------------------------------------------------------------------
+# DefenseAreaRule
+# ---------------------------------------------------------------------------
+
+
+class TestDefenseAreaRule:
+ def _frame_with_attacker_in_defense(self, my_team_is_right: bool = False) -> GameFrame:
+ # Enemy robot inside my (left) defense area.
+ enemy = {0: _robot(0, -4.3, 0.5, is_friendly=False)}
+ return _frame(
+ ball=_ball(0, 0),
+ enemy_robots=enemy,
+ my_team_is_right=my_team_is_right,
+ )
+
+ def test_fires_during_normal_start(self):
+ rule = DefenseAreaRule()
+ frame = self._frame_with_attacker_in_defense(my_team_is_right=False)
+ v = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert v is not None
+ assert v.rule_name == "defense_area"
+
+ def test_does_not_fire_during_stop(self):
+ rule = DefenseAreaRule()
+ frame = self._frame_with_attacker_in_defense()
+ assert rule.check(frame, GEO, RefereeCommand.STOP) is None
+
+ def test_does_not_fire_during_force_start_no_actually_fires(self):
+ rule = DefenseAreaRule()
+ frame = self._frame_with_attacker_in_defense(my_team_is_right=False)
+ v = rule.check(frame, GEO, RefereeCommand.FORCE_START)
+ assert v is not None
+
+ def test_too_many_defenders(self):
+ rule = DefenseAreaRule(max_defenders=1)
+ # Two friendly robots in own (left) defense area.
+ friendly = {
+ 0: _robot(0, -4.3, 0.0, is_friendly=True),
+ 1: _robot(1, -4.3, 0.5, is_friendly=True),
+ }
+ frame = _frame(ball=_ball(0, 0), friendly_robots=friendly, my_team_is_right=False, my_team_is_yellow=True)
+ v = rule.check(frame, GEO, RefereeCommand.NORMAL_START)
+ assert v is not None
+ assert v.next_command == RefereeCommand.DIRECT_FREE_BLUE
+
+
+# ---------------------------------------------------------------------------
+# KeepOutRule
+# ---------------------------------------------------------------------------
+
+
+class TestKeepOutRule:
+ def test_no_trigger_before_persistence_threshold(self):
+ rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=5)
+ friendly = {0: _robot(0, 0.2, 0.0, is_friendly=True)}
+ # Enemy is kicking → check friendly.
+ for _ in range(4):
+ frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
+ v = rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
+ assert v is None
+
+ def test_triggers_after_persistence_threshold(self):
+ rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=5)
+ friendly = {0: _robot(0, 0.2, 0.0, is_friendly=True)}
+ v = None
+ for _ in range(5):
+ frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
+ v = rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
+ assert v is not None
+ assert v.rule_name == "keep_out"
+
+ def test_resets_on_non_violation_frame(self):
+ rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=5)
+ friendly = {0: _robot(0, 0.2, 0.0, is_friendly=True)}
+ for _ in range(4):
+ frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
+ rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
+
+ # Robot moves away — count resets.
+ far_frame = _frame(
+ ball=_ball(0.0, 0.0),
+ friendly_robots={0: _robot(0, 2.0, 0.0, is_friendly=True)},
+ )
+ rule.check(far_frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
+
+ # Needs another full persistence run to trigger.
+ v = None
+ for _ in range(5):
+ frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
+ v = rule.check(frame, GEO, RefereeCommand.DIRECT_FREE_BLUE)
+ assert v is not None
+
+ def test_inactive_during_normal_start(self):
+ rule = KeepOutRule(radius_meters=0.5, violation_persistence_frames=1)
+ friendly = {0: _robot(0, 0.1, 0.0, is_friendly=True)}
+ frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=friendly)
+ assert rule.check(frame, GEO, RefereeCommand.NORMAL_START) is None
+
+
+# ---------------------------------------------------------------------------
+# GameStateMachine
+# ---------------------------------------------------------------------------
+
+
+class TestGameStateMachine:
+ def test_goal_increments_yellow_score(self):
+ from utama_core.custom_referee.rules.base_rule import RuleViolation
+
+ sm = _state_machine()
+ violation = RuleViolation(
+ rule_name="goal",
+ suggested_command=RefereeCommand.STOP,
+ next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
+ status_message="Goal by Yellow",
+ )
+ data = sm.step(current_time=10.0, violation=violation)
+ assert sm.yellow_team.score == 1
+ assert sm.blue_team.score == 0
+ assert data.referee_command == RefereeCommand.STOP
+ assert data.next_command == RefereeCommand.BALL_PLACEMENT_BLUE
+
+ def test_goal_increments_blue_score(self):
+ from utama_core.custom_referee.rules.base_rule import RuleViolation
+
+ sm = _state_machine()
+ violation = RuleViolation(
+ rule_name="goal",
+ suggested_command=RefereeCommand.STOP,
+ next_command=RefereeCommand.PREPARE_KICKOFF_YELLOW,
+ status_message="Goal by Blue",
+ )
+ sm.step(current_time=10.0, violation=violation)
+ assert sm.blue_team.score == 1
+ assert sm.yellow_team.score == 0
+
+ def test_transition_cooldown_suppresses_duplicate(self):
+ from utama_core.custom_referee.rules.base_rule import RuleViolation
+
+ sm = _state_machine()
+ violation = RuleViolation(
+ rule_name="goal",
+ suggested_command=RefereeCommand.STOP,
+ next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
+ status_message="Goal",
+ )
+ sm.step(current_time=10.0, violation=violation)
+ assert sm.yellow_team.score == 1
+
+ # Second goal within cooldown window — must be suppressed.
+ sm.step(current_time=10.1, violation=violation)
+ assert sm.yellow_team.score == 1 # still 1
+
+ def test_goal_sets_designated_position_to_centre(self):
+ from utama_core.custom_referee.rules.base_rule import RuleViolation
+
+ sm = _state_machine()
+ violation = RuleViolation(
+ rule_name="goal",
+ suggested_command=RefereeCommand.STOP,
+ next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
+ status_message="Goal by Yellow",
+ )
+ data = sm.step(current_time=10.0, violation=violation)
+ assert data.designated_position == (0.0, 0.0)
+
+ def test_goal_status_message_is_propagated_into_referee_data(self):
+ from utama_core.custom_referee.rules.base_rule import RuleViolation
+
+ sm = _state_machine()
+ violation = RuleViolation(
+ rule_name="goal",
+ suggested_command=RefereeCommand.STOP,
+ next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
+ status_message="Goal by Yellow",
+ )
+ data = sm.step(current_time=10.0, violation=violation)
+ assert data.status_message == "Goal by Yellow"
+
+ def test_manual_command_clears_status_message(self):
+ from utama_core.custom_referee.rules.base_rule import RuleViolation
+
+ sm = _state_machine()
+ violation = RuleViolation(
+ rule_name="goal",
+ suggested_command=RefereeCommand.STOP,
+ next_command=RefereeCommand.PREPARE_KICKOFF_BLUE,
+ status_message="Goal by Yellow",
+ )
+ sm.step(current_time=10.0, violation=violation)
+
+ sm.set_command(RefereeCommand.NORMAL_START, timestamp=11.0)
+ data = sm.step(current_time=11.0, violation=None)
+ assert data.status_message is None
+
+ def test_manual_set_command(self):
+ sm = _state_machine()
+ sm.set_command(RefereeCommand.NORMAL_START, timestamp=5.0)
+ assert sm.command == RefereeCommand.NORMAL_START
+
+ def test_manual_ball_placement_auto_advances_from_stop_and_then_to_normal_start(self):
+ sm = _state_machine()
+ sm.set_command(RefereeCommand.BALL_PLACEMENT_YELLOW, timestamp=1.0)
+ sm.ball_placement_target = (0.0, 0.0)
+
+ clear_frame = _frame(ball=_ball(2.0, 0.0), ts=10.0)
+ data = sm.step(current_time=10.0, violation=None, game_frame=clear_frame)
+ assert data.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW
+ assert data.next_command == RefereeCommand.NORMAL_START
+
+ placed_frame = _frame(ball=_ball(0.0, 0.0), ts=20.0)
+ sm.step(current_time=20.0, violation=None, game_frame=placed_frame)
+ data = sm.step(current_time=23.0, violation=None, game_frame=placed_frame)
+ assert data.referee_command == RefereeCommand.NORMAL_START
+ assert data.next_command is None
+
+ def test_manual_penalty_auto_advances_from_stop_and_then_to_normal_start(self):
+ sm = _state_machine()
+ sm.set_command(RefereeCommand.PREPARE_PENALTY_YELLOW, timestamp=1.0)
+
+ clear_frame = _frame(ball=_ball(0.0, 0.0), ts=10.0)
+ data = sm.step(current_time=10.0, violation=None, game_frame=clear_frame)
+ assert data.referee_command == RefereeCommand.PREPARE_PENALTY_YELLOW
+ assert data.next_command == RefereeCommand.NORMAL_START
+
+ ready_attackers = {0: _robot(0, 2.25, 0.0, is_friendly=True)}
+ ready_frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=ready_attackers, ts=14.0)
+ sm.step(current_time=14.0, violation=None, game_frame=ready_frame)
+ data = sm.step(current_time=17.0, violation=None, game_frame=ready_frame)
+ assert data.referee_command == RefereeCommand.NORMAL_START
+ assert data.next_command is None
+
+
+# ---------------------------------------------------------------------------
+# CustomReferee integration
+# ---------------------------------------------------------------------------
+
+
+class TestCustomReferee:
+ def test_returns_valid_referee_data(self):
+ referee = CustomReferee.from_profile_name("simulation")
+ frame = _frame(ball=_ball(0.0, 0.0))
+ data = referee.step(frame, current_time=10.0)
+ assert isinstance(data, RefereeData)
+ assert data.source_identifier == "custom_referee"
+
+ def test_simulation_goal_auto_advances_to_ball_placement_and_scores(self):
+ referee = CustomReferee.from_profile_name("simulation")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+
+ # Yellow is on the RIGHT — ball in right goal means yellow conceded, blue scored.
+ frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True, ts=10.0)
+ data = referee.step(frame, current_time=10.0)
+
assert data.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW
- assert data.next_command == RefereeCommand.NORMAL_START
-
- placed_frame = _frame(ball=_ball(0.0, 0.0), ts=20.0)
- sm.step(current_time=20.0, violation=None, game_frame=placed_frame)
- data = sm.step(current_time=23.0, violation=None, game_frame=placed_frame)
- assert data.referee_command == RefereeCommand.NORMAL_START
- assert data.next_command is None
-
- def test_manual_penalty_auto_advances_from_stop_and_then_to_normal_start(self):
- sm = _state_machine()
- sm.set_command(RefereeCommand.PREPARE_PENALTY_YELLOW, timestamp=1.0)
-
- clear_frame = _frame(ball=_ball(0.0, 0.0), ts=10.0)
- data = sm.step(current_time=10.0, violation=None, game_frame=clear_frame)
- assert data.referee_command == RefereeCommand.PREPARE_PENALTY_YELLOW
- assert data.next_command == RefereeCommand.NORMAL_START
-
- ready_attackers = {0: _robot(0, 2.25, 0.0, is_friendly=True)}
- ready_frame = _frame(ball=_ball(0.0, 0.0), friendly_robots=ready_attackers, ts=14.0)
- sm.step(current_time=14.0, violation=None, game_frame=ready_frame)
- data = sm.step(current_time=17.0, violation=None, game_frame=ready_frame)
- assert data.referee_command == RefereeCommand.NORMAL_START
- assert data.next_command is None
-
-
-# ---------------------------------------------------------------------------
-# CustomReferee integration
-# ---------------------------------------------------------------------------
-
-
-class TestCustomReferee:
- def test_returns_valid_referee_data(self):
- referee = CustomReferee.from_profile_name("simulation")
- frame = _frame(ball=_ball(0.0, 0.0))
- data = referee.step(frame, current_time=10.0)
- assert isinstance(data, RefereeData)
- assert data.source_identifier == "custom_referee"
-
- def test_simulation_goal_auto_advances_to_prepare_kickoff_and_scores(self):
- referee = CustomReferee.from_profile_name("simulation")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
-
- # Yellow is on the RIGHT — ball in right goal means yellow conceded, blue scored.
- frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True, ts=10.0)
- data = referee.step(frame, current_time=10.0)
-
- assert data.referee_command == RefereeCommand.PREPARE_KICKOFF_YELLOW
assert data.blue_team.score == 1
assert data.yellow_team.score == 0
- assert data.next_command == RefereeCommand.NORMAL_START
-
- def test_human_profile_no_oob(self):
- """Human profile disables out-of-bounds — ball outside must not trigger."""
- referee = CustomReferee.from_profile_name("human")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
- frame = _frame(ball=_ball(0.0, 4.0), ts=10.0) # ball outside field width
- data = referee.step(frame, current_time=10.0)
- assert data.referee_command == RefereeCommand.NORMAL_START
-
- def test_simulation_oob_exposes_status_message(self):
- referee = CustomReferee.from_profile_name("simulation")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
- frame = _frame(ball=_ball(0.0, 4.0), ts=10.0)
- data = referee.step(frame, current_time=10.0)
- assert data.status_message == "Ball out of bounds"
-
- def test_human_stays_in_stop_after_goal_until_operator_advances(self):
- """Human mode keeps the game in STOP after a goal for operator control."""
- referee = CustomReferee.from_profile_name("human")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
-
- # Score a goal (yellow on right, blue scores).
- goal_frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True, ts=10.0)
- data = referee.step(goal_frame, current_time=10.0)
- assert data.referee_command == RefereeCommand.STOP
+ assert data.next_command == RefereeCommand.PREPARE_KICKOFF_YELLOW
+
+ def test_human_profile_no_oob(self):
+ """Human profile disables out-of-bounds — ball outside must not trigger."""
+ referee = CustomReferee.from_profile_name("human")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+ frame = _frame(ball=_ball(0.0, 4.0), ts=10.0) # ball outside field width
+ data = referee.step(frame, current_time=10.0)
+ assert data.referee_command == RefereeCommand.NORMAL_START
+
+ def test_simulation_oob_exposes_status_message(self):
+ referee = CustomReferee.from_profile_name("simulation")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+ frame = _frame(ball=_ball(0.0, 4.0), ts=10.0)
+ data = referee.step(frame, current_time=10.0)
+ assert data.status_message == "Ball out of bounds"
+
+ def test_human_stays_in_stop_after_goal_until_operator_advances(self):
+ """Human mode keeps the game in STOP after a goal for operator control."""
+ referee = CustomReferee.from_profile_name("human")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+
+ # Score a goal (yellow on right, blue scores).
+ goal_frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True, ts=10.0)
+ data = referee.step(goal_frame, current_time=10.0)
+ assert data.referee_command == RefereeCommand.STOP
+ assert data.next_command == RefereeCommand.BALL_PLACEMENT_YELLOW
+
+ # Still in STOP later — operator must choose the next command.
+ still_stop = referee.step(_frame(ball=_ball(0.0, 0.0), ts=70.0), current_time=70.0)
+ assert still_stop.referee_command == RefereeCommand.STOP
+
+ def test_simulation_stays_in_ball_placement_after_goal_until_ball_is_placed(self):
+ """Simulation mode auto-advances into BALL_PLACEMENT and waits for the ball at centre."""
+ referee = CustomReferee.from_profile_name("simulation")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+
+ goal_frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True, ts=10.0)
+ referee.step(goal_frame, current_time=10.0)
+
+ data = referee.step(_frame(ball=_ball(1.0, 0.0), ts=70.0), current_time=70.0)
+ assert data.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW
assert data.next_command == RefereeCommand.PREPARE_KICKOFF_YELLOW
- # Still in STOP later — operator must choose the next command.
- still_stop = referee.step(_frame(ball=_ball(0.0, 0.0), ts=70.0), current_time=70.0)
- assert still_stop.referee_command == RefereeCommand.STOP
-
- def test_simulation_stays_in_prepare_kickoff_after_goal_without_ready_kicker(self):
- """Simulation mode auto-advances into PREPARE_KICKOFF and waits there until ready."""
- referee = CustomReferee.from_profile_name("simulation")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
-
- goal_frame = _frame(ball=_ball(5.0, 0.0), my_team_is_yellow=True, my_team_is_right=True, ts=10.0)
- referee.step(goal_frame, current_time=10.0)
-
- # With no kicker in the centre circle, the state remains in PREPARE_KICKOFF.
- data = referee.step(_frame(ball=_ball(0.0, 0.0), ts=70.0), current_time=70.0)
- assert data.referee_command == RefereeCommand.PREPARE_KICKOFF_YELLOW
-
- def test_simulation_oob_auto_advances_to_direct_free_and_then_normal_start(self):
- referee = CustomReferee.from_profile_name("simulation")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
-
- touch_frame = _frame(
- ball=_ball(4.4, 2.9),
- friendly_robots={0: _robot(0, 4.4, 2.9, is_friendly=True, has_ball=True)},
- my_team_is_yellow=True,
- ts=9.9,
- )
- referee.step(touch_frame, current_time=9.9)
+ def test_simulation_oob_auto_advances_to_ball_placement_then_direct_free(self):
+ referee = CustomReferee.from_profile_name("simulation")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+
+ touch_frame = _frame(
+ ball=_ball(4.4, 2.9),
+ friendly_robots={0: _robot(0, 4.4, 2.9, is_friendly=True, has_ball=True)},
+ my_team_is_yellow=True,
+ ts=9.9,
+ )
+ referee.step(touch_frame, current_time=9.9)
+
+ out_frame = _frame(ball=_ball(0.0, 3.5), my_team_is_yellow=True, ts=10.0)
+ data = referee.step(out_frame, current_time=10.0)
+ assert data.referee_command == RefereeCommand.BALL_PLACEMENT_BLUE
+ assert data.next_command == RefereeCommand.DIRECT_FREE_BLUE
- out_frame = _frame(ball=_ball(0.0, 3.5), my_team_is_yellow=True, ts=10.0)
- data = referee.step(out_frame, current_time=10.0)
+ ready_frame = _frame(
+ ball=_ball(0.0, 2.9),
+ friendly_robots={0: _robot(0, 1.0, 0.0, is_friendly=True)},
+ enemy_robots={0: _robot(0, 0.1, 0.0, is_friendly=False)},
+ my_team_is_yellow=True,
+ ts=20.0,
+ )
+ referee.step(ready_frame, current_time=20.0)
+ data = referee.step(ready_frame, current_time=23.0)
assert data.referee_command == RefereeCommand.DIRECT_FREE_BLUE
assert data.next_command == RefereeCommand.NORMAL_START
-
- ready_frame = _frame(
- ball=_ball(0.0, 0.0),
- friendly_robots={0: _robot(0, 1.0, 0.0, is_friendly=True)},
- enemy_robots={0: _robot(0, 0.1, 0.0, is_friendly=False)},
- my_team_is_yellow=True,
- ts=20.0,
- )
- referee.step(ready_frame, current_time=20.0)
- data = referee.step(ready_frame, current_time=23.0)
- assert data.referee_command == RefereeCommand.NORMAL_START
-
- def test_human_manual_direct_free_stays_in_stop_until_operator_advances(self):
- referee = CustomReferee.from_profile_name("human")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
- referee.set_command(RefereeCommand.DIRECT_FREE_BLUE, timestamp=1.0)
-
- data = referee.step(_frame(ball=_ball(0.0, 0.0), my_team_is_yellow=True, ts=10.0), current_time=10.0)
- assert data.referee_command == RefereeCommand.STOP
- assert data.next_command == RefereeCommand.DIRECT_FREE_BLUE
-
- still_stop = referee.step(_frame(ball=_ball(0.0, 0.0), ts=40.0), current_time=40.0)
- assert still_stop.referee_command == RefereeCommand.STOP
-
- def test_human_manual_penalty_stays_in_stop_until_operator_advances(self):
- referee = CustomReferee.from_profile_name("human")
- referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
- referee.set_command(RefereeCommand.PREPARE_PENALTY_YELLOW, timestamp=1.0)
-
- data = referee.step(_frame(ball=_ball(0.0, 0.0), ts=10.0), current_time=10.0)
- assert data.referee_command == RefereeCommand.STOP
- assert data.next_command == RefereeCommand.PREPARE_PENALTY_YELLOW
-
-
-# ---------------------------------------------------------------------------
-# Profile loader
-# ---------------------------------------------------------------------------
-
-
-class TestProfileLoader:
- def test_simulation_loads(self):
- profile = load_profile("simulation")
- assert profile.profile_name == "simulation"
- assert profile.rules.goal_detection.enabled is True
- assert profile.rules.keep_out.radius_meters == 0.5
-
- def test_human_loads(self):
- profile = load_profile("human")
- assert profile.profile_name == "human"
- assert profile.rules.out_of_bounds.enabled is False
- assert profile.game.force_start_after_goal is False
- assert profile.game.auto_advance.stop_to_next_command is False
- assert profile.game.auto_advance.prepare_penalty_to_normal is False
-
- def test_legacy_stop_to_prepare_kickoff_key_is_still_loaded(self, tmp_path):
- profile_path = tmp_path / "legacy_profile.yaml"
- profile_path.write_text(
- """
-profile_name: "legacy"
-geometry: {}
-rules: {}
-game:
- auto_advance:
- stop_to_prepare_kickoff: false
-""".strip()
- )
-
- profile = load_profile(str(profile_path))
- assert profile.game.auto_advance.stop_to_next_command is False
-
- def test_unknown_profile_raises(self):
- with pytest.raises(FileNotFoundError):
- load_profile("nonexistent_profile")
+
+ def test_human_manual_direct_free_stays_in_stop_until_operator_advances(self):
+ referee = CustomReferee.from_profile_name("human")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+ referee.set_command(RefereeCommand.DIRECT_FREE_BLUE, timestamp=1.0)
+
+ data = referee.step(_frame(ball=_ball(0.0, 0.0), my_team_is_yellow=True, ts=10.0), current_time=10.0)
+ assert data.referee_command == RefereeCommand.STOP
+ assert data.next_command == RefereeCommand.DIRECT_FREE_BLUE
+
+ still_stop = referee.step(_frame(ball=_ball(0.0, 0.0), ts=40.0), current_time=40.0)
+ assert still_stop.referee_command == RefereeCommand.STOP
+
+ def test_human_manual_penalty_stays_in_stop_until_operator_advances(self):
+ referee = CustomReferee.from_profile_name("human")
+ referee.set_command(RefereeCommand.NORMAL_START, timestamp=0.0)
+ referee.set_command(RefereeCommand.PREPARE_PENALTY_YELLOW, timestamp=1.0)
+
+ data = referee.step(_frame(ball=_ball(0.0, 0.0), ts=10.0), current_time=10.0)
+ assert data.referee_command == RefereeCommand.STOP
+ assert data.next_command == RefereeCommand.PREPARE_PENALTY_YELLOW
+
+
+# ---------------------------------------------------------------------------
+# Profile loader
+# ---------------------------------------------------------------------------
+
+
+class TestProfileLoader:
+ def test_simulation_loads(self):
+ profile = load_profile("simulation")
+ assert profile.profile_name == "simulation"
+ assert profile.rules.goal_detection.enabled is True
+ assert profile.rules.keep_out.radius_meters == 0.5
+
+ def test_human_loads(self):
+ profile = load_profile("human")
+ assert profile.profile_name == "human"
+ assert profile.rules.out_of_bounds.enabled is False
+ assert profile.game.force_start_after_goal is False
+ assert profile.game.auto_advance.stop_to_next_command is False
+ assert profile.game.auto_advance.prepare_penalty_to_normal is False
+
+ def test_legacy_stop_to_prepare_kickoff_key_is_still_loaded(self, tmp_path):
+ profile_path = tmp_path / "legacy_profile.yaml"
+ profile_path.write_text(
+ """
+profile_name: "legacy"
+geometry: {}
+rules: {}
+game:
+ auto_advance:
+ stop_to_prepare_kickoff: false
+""".strip()
+ )
+
+ profile = load_profile(str(profile_path))
+ assert profile.game.auto_advance.stop_to_next_command is False
+
+ def test_unknown_profile_raises(self):
+ with pytest.raises(FileNotFoundError):
+ load_profile("nonexistent_profile")
diff --git a/utama_core/tests/referee/test_referee_unit.py b/utama_core/tests/referee/test_referee_unit.py
index ce99eb69..dc07184c 100644
--- a/utama_core/tests/referee/test_referee_unit.py
+++ b/utama_core/tests/referee/test_referee_unit.py
@@ -450,16 +450,27 @@ def fake_move(game, motion_controller, robot_id, target_coords, target_oren, dri
assert cmd_map[1] is not None
def test_robot_with_ball_moves_to_designated_position(self, monkeypatch):
+ import math
+
from utama_core.strategy.referee import actions as referee_actions
- captured = []
+ move_captured = []
+ turn_captured = []
def fake_move(game, motion_controller, robot_id, target_coords, target_oren, dribbling=False):
- captured.append((robot_id, target_coords, dribbling))
+ move_captured.append((robot_id, target_coords, dribbling))
return ("move", robot_id)
+ def fake_turn_on_spot(game, motion_controller, robot_id, target_oren, dribbling=False):
+ turn_captured.append((robot_id, dribbling))
+ return ("turn", robot_id)
+
monkeypatch.setattr(referee_actions, "move", fake_move)
+ monkeypatch.setattr(referee_actions, "turn_on_spot", fake_turn_on_spot)
+ target = (1.5, -0.5)
+ # Orient the robot to face the target so face_error < threshold → move branch.
+ facing = math.atan2(target[1], target[0])
robots = {
0: Robot(
id=0,
@@ -468,13 +479,13 @@ def fake_move(game, motion_controller, robot_id, target_coords, target_oren, dri
p=Vector2D(0.0, 0.0),
v=Vector2D(0.0, 0.0),
a=Vector2D(0.0, 0.0),
- orientation=0.0,
+ orientation=facing,
)
}
referee = _make_referee_data(
command=RefereeCommand.BALL_PLACEMENT_YELLOW,
)
- referee.designated_position = (1.5, -0.5)
+ referee.designated_position = target
frame = GameFrame(
ts=0.0,
my_team_is_yellow=True,
@@ -501,9 +512,11 @@ def fake_move(game, motion_controller, robot_id, target_coords, target_oren, dri
status = node.update()
assert status == py_trees.common.Status.RUNNING
- assert captured[0][0] == 0
- assert captured[0][1] == Vector2D(1.5, -0.5)
- assert captured[0][2] is True
+ # Robot already faces target → should call move (not turn_on_spot).
+ assert len(move_captured) >= 1
+ assert move_captured[0][0] == 0
+ assert move_captured[0][1] == Vector2D(1.5, -0.5)
+ assert move_captured[0][2] is True
def test_non_placing_teammate_clears_from_ball(self, monkeypatch):
from utama_core.strategy.referee import actions as referee_actions
@@ -1253,8 +1266,12 @@ def fake_move(game, motion_controller, robot_id, target_coords, target_oren, dri
kicker_entries = [entry for entry in captured if entry[0] == 1]
assert len(kicker_entries) == 1
- assert kicker_entries[0][1].x == pytest.approx(1.0)
- assert kicker_entries[0][1].y == pytest.approx(0.0)
+ # Target is the approach point behind the ball, not the ball itself —
+ # verify it is closer to the ball than robot 1's starting position.
+ ball_pos = Vector2D(1.0, 0.0)
+ robot1_start = Vector2D(1.2, 0.0)
+ target = kicker_entries[0][1]
+ assert target.distance_to(ball_pos) < robot1_start.distance_to(ball_pos)
def test_kicker_moves_toward_ball(self, monkeypatch):
from utama_core.strategy.referee import actions as referee_actions
@@ -1294,8 +1311,12 @@ def fake_move(game, motion_controller, robot_id, target_coords, target_oren, dri
assert len(captured) == 1
assert captured[0][0] == 0
- assert captured[0][1].x == pytest.approx(2.0)
- assert captured[0][1].y == pytest.approx(1.0)
+ # Target is the approach point behind the ball, not the ball itself —
+ # verify it is closer to the ball than the robot's starting position.
+ ball_pos = Vector2D(2.0, 1.0)
+ robot_start = Vector2D(0.5, 0.0)
+ target = captured[0][1]
+ assert target.distance_to(ball_pos) < robot_start.distance_to(ball_pos)
def test_non_kicker_robots_get_stop_command(self, monkeypatch):
from utama_core.strategy.referee import actions as referee_actions
diff --git a/utama_core/tests/refiners/robot_info_test.py b/utama_core/tests/refiners/robot_info_test.py
index b9e2ef88..707e9931 100644
--- a/utama_core/tests/refiners/robot_info_test.py
+++ b/utama_core/tests/refiners/robot_info_test.py
@@ -155,3 +155,27 @@ def test_mixed_team_trusted_uses_ir_untrusted_uses_vision():
result = refiner.refine(frame, responses)
assert result.friendly_robots[0].has_ball is True # from IR
assert result.friendly_robots[1].has_ball is True # from vision (close enough)
+
+
+def test_untrusted_robot_missing_from_responses_uses_vision():
+ """Untrusted robot absent from responses this tick must not keep stale has_ball."""
+ refiner = RobotInfoRefiner(trusted_ir_robots=frozenset({0}))
+ robots = {
+ 0: _make_robot(0, 2.0, 0.0, has_ball=False), # trusted
+ 1: _make_robot(1, 0.05, 0.0, has_ball=True), # untrusted, stale True
+ }
+ frame = _make_frame(robots, ball_pos=(0.0, 0.0))
+ # Robot 1 did not respond this tick (non-blocking poll returned nothing for it).
+ responses = [RobotResponse(id=0, has_ball=False)]
+ result = refiner.refine(frame, responses)
+ # Robot 1 is close to ball → vision inference gives True (not stale, but correct)
+ assert result.friendly_robots[1].has_ball is True
+
+ # Now robot 1 is far from ball; stale value is True but inference should correct it.
+ robots_far = {
+ 0: _make_robot(0, 2.0, 0.0, has_ball=False),
+ 1: _make_robot(1, 1.0, 0.0, has_ball=True), # stale True, but now far
+ }
+ frame_far = _make_frame(robots_far, ball_pos=(0.0, 0.0))
+ result_far = refiner.refine(frame_far, [RobotResponse(id=0, has_ball=False)])
+ assert result_far.friendly_robots[1].has_ball is False # stale cleared by vision
diff --git a/utama_core/tests/skills/test_defend_parameter.py b/utama_core/tests/skills/test_defend_parameter.py
new file mode 100644
index 00000000..635f925b
--- /dev/null
+++ b/utama_core/tests/skills/test_defend_parameter.py
@@ -0,0 +1,464 @@
+from types import SimpleNamespace
+
+import numpy as np
+import pytest
+
+import utama_core.skills.src.defend_parameter as dp
+from utama_core.config.physical_constants import BALL_RADIUS, ROBOT_RADIUS
+from utama_core.entities.data.vector import Vector2D, Vector3D
+
+EDGE_OFFSET = BALL_RADIUS + ROBOT_RADIUS
+
+# ---------------------------------------------------------------------------
+# Standard-field stubs (9x6 field)
+# goal_x = +/-4.5, goal_half_width = 0.5
+# defense front at +/-3.5, defense_half_width = 1.0
+# defender_x (left) = -3.5 + ROBOT_RADIUS, defender_x (right) = 3.5 - ROBOT_RADIUS
+# keeper_x (left) = -4.5 + ROBOT_RADIUS, keeper_x (right) = 4.5 - ROBOT_RADIUS
+# post_limit = 0.5 - ROBOT_RADIUS
+# ---------------------------------------------------------------------------
+_STD_LEFT_GOAL_LINE = np.array([(-4.5, 0.5), (-4.5, -0.5)])
+_STD_RIGHT_GOAL_LINE = np.array([(4.5, 0.5), (4.5, -0.5)])
+_STD_LEFT_DEFENSE_AREA = np.array(
+ [
+ (-4.5, 1.0),
+ (-3.5, 1.0),
+ (-3.5, -1.0),
+ (-4.5, -1.0),
+ (-4.5, 1.0),
+ ]
+)
+_STD_RIGHT_DEFENSE_AREA = np.array(
+ [
+ (4.5, 1.0),
+ (3.5, 1.0),
+ (3.5, -1.0),
+ (4.5, -1.0),
+ (4.5, 1.0),
+ ]
+)
+
+# Defender stands one ROBOT_RADIUS in front of the defense area
+_LEFT_DEFENDER_X = -3.5 + ROBOT_RADIUS
+_RIGHT_DEFENDER_X = 3.5 - ROBOT_RADIUS
+
+# Keeper-line references
+_LEFT_KEEPER_X = -4.5 + ROBOT_RADIUS
+_RIGHT_KEEPER_X = 4.5 - ROBOT_RADIUS
+_STD_POST_LIMIT = 0.5 - ROBOT_RADIUS
+
+
+def _std_field(my_team_is_right: bool):
+ if my_team_is_right:
+ return SimpleNamespace(
+ my_goal_line=_STD_RIGHT_GOAL_LINE,
+ my_defense_area=_STD_RIGHT_DEFENSE_AREA,
+ half_goal_width=0.5,
+ )
+ return SimpleNamespace(
+ my_goal_line=_STD_LEFT_GOAL_LINE,
+ my_defense_area=_STD_LEFT_DEFENSE_AREA,
+ half_goal_width=0.5,
+ )
+
+
+def _custom_field(goal_x, goal_half_width, defense_front_x, defense_half_width):
+ """Create a field stub with arbitrary goal line and defense area."""
+ return SimpleNamespace(
+ my_goal_line=np.array(
+ [
+ (goal_x, goal_half_width),
+ (goal_x, -goal_half_width),
+ ]
+ ),
+ my_defense_area=np.array(
+ [
+ (goal_x, defense_half_width),
+ (defense_front_x, defense_half_width),
+ (defense_front_x, -defense_half_width),
+ (goal_x, -defense_half_width),
+ (goal_x, defense_half_width),
+ ]
+ ),
+ half_goal_width=goal_half_width,
+ )
+
+
+def _make_game(team_is_right, field, friendly_robots, ball_pos):
+ return SimpleNamespace(
+ my_team_is_right=team_is_right,
+ field=field,
+ friendly_robots=friendly_robots,
+ ball=SimpleNamespace(
+ p=Vector3D(ball_pos[0], ball_pos[1], 0.0),
+ v=Vector3D(0.0, 0.0, 0.0),
+ ),
+ )
+
+
+def _capture_go_to_point(monkeypatch):
+ captured = {}
+
+ def fake(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ captured["dribbling"] = dribbling
+ captured["robot_id"] = robot_id
+ return "sentinel-command"
+
+ monkeypatch.setattr(dp, "go_to_point", fake)
+ return captured
+
+
+# ---------------------------------------------------------------------------
+# Core geometry: defender on ball-to-post line using keeper-x reference
+# ---------------------------------------------------------------------------
+
+
+def test_defender_on_ball_to_post_line_left_team(monkeypatch):
+ """Left team, robot 1 covers top post. Ball at origin."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ # goal_point = (keeper_x, post_limit + EDGE_OFFSET)
+ # t = (defender_x - 0) / (keeper_x - 0)
+ t = _LEFT_DEFENDER_X / _LEFT_KEEPER_X
+ expected_y = t * (_STD_POST_LIMIT + EDGE_OFFSET)
+ assert captured["target"].x == pytest.approx(_LEFT_DEFENDER_X)
+ assert captured["target"].y == pytest.approx(expected_y)
+ assert captured["dribbling"] is True
+
+
+def test_defender_on_ball_to_post_line_right_team(monkeypatch):
+ """Right team, robot 1 covers top post. Ball at origin."""
+ game = _make_game(
+ team_is_right=True,
+ field=_std_field(True),
+ friendly_robots={1: SimpleNamespace(p=Vector2D(3.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ # goal_point = (keeper_x, post_limit + EDGE_OFFSET)
+ t = _RIGHT_DEFENDER_X / _RIGHT_KEEPER_X
+ expected_y = t * (_STD_POST_LIMIT + EDGE_OFFSET)
+ assert captured["target"].x == pytest.approx(_RIGHT_DEFENDER_X)
+ assert captured["target"].y == pytest.approx(expected_y)
+
+
+def test_defender_robot2_covers_bottom_post(monkeypatch):
+ """Robot 2 defaults to the bottom post (-post_limit)."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={2: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=2)
+
+ t = _LEFT_DEFENDER_X / _LEFT_KEEPER_X
+ expected_y = t * (-_STD_POST_LIMIT - EDGE_OFFSET)
+ assert captured["target"].x == pytest.approx(_LEFT_DEFENDER_X)
+ assert captured["target"].y == pytest.approx(expected_y)
+
+
+# ---------------------------------------------------------------------------
+# goal_frame_y=0.0 targets centre of goal (explicit override still works)
+# ---------------------------------------------------------------------------
+
+
+def test_goal_frame_y_zero_targets_centre(monkeypatch):
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 0.5),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1, goal_frame_y=0.0)
+
+ # goal_point = (keeper_x, EDGE_OFFSET)
+ t = _LEFT_DEFENDER_X / _LEFT_KEEPER_X
+ expected_y = 0.5 + t * (EDGE_OFFSET - 0.5)
+ assert captured["target"].x == pytest.approx(_LEFT_DEFENDER_X)
+ assert captured["target"].y == pytest.approx(expected_y)
+ assert captured["dribbling"] is True
+
+
+# ---------------------------------------------------------------------------
+# Multi-robot: both defenders dynamically track (no static positions)
+# ---------------------------------------------------------------------------
+
+
+def test_three_robots_both_defenders_dynamic(monkeypatch):
+ """With 3+ robots, both defenders should track the ball-to-post line."""
+ field = _std_field(False)
+ robots = {
+ 0: SimpleNamespace(p=Vector2D(-4.5, 0.0)),
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.3)),
+ 2: SimpleNamespace(p=Vector2D(-3.0, -0.3)),
+ }
+ captured_calls = []
+
+ def fake(game, motion_controller, robot_id, target, dribbling=False):
+ captured_calls.append({"robot_id": robot_id, "target": target})
+ return "sentinel-command"
+
+ monkeypatch.setattr(dp, "go_to_point", fake)
+
+ game1 = _make_game(False, field, robots, (0.0, 0.0))
+ dp.defend_parameter(game1, motion_controller=object(), robot_id=1)
+
+ game2 = _make_game(False, field, robots, (0.0, 0.0))
+ dp.defend_parameter(game2, motion_controller=object(), robot_id=2)
+
+ assert len(captured_calls) == 2
+ # Robot 1 covers top post -> positive y
+ assert captured_calls[0]["target"].y > 0
+ # Robot 2 covers bottom post -> negative y
+ assert captured_calls[1]["target"].y < 0
+ # Both just in front of defense area
+ assert captured_calls[0]["target"].x == pytest.approx(_LEFT_DEFENDER_X)
+ assert captured_calls[1]["target"].x == pytest.approx(_LEFT_DEFENDER_X)
+
+
+# ---------------------------------------------------------------------------
+# Y clamped to defense_half_width
+# ---------------------------------------------------------------------------
+
+
+def test_target_y_clamped_to_defense_half_width(monkeypatch):
+ """When ball-to-post line exits defense area vertically, y is clamped."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 3.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ assert captured["target"].y == pytest.approx(1.0)
+ assert captured["target"].x == pytest.approx(_LEFT_DEFENDER_X)
+
+
+# ---------------------------------------------------------------------------
+# Custom geometry
+# ---------------------------------------------------------------------------
+
+
+def test_custom_geometry_uses_field_values(monkeypatch):
+ """Defender positioning adapts to non-standard field dimensions."""
+ field = _custom_field(-6.0, 0.8, -4.5, 1.5)
+ keeper_x = -6.0 + ROBOT_RADIUS
+ post_limit = 0.8 - ROBOT_RADIUS
+ defender_x = -4.5 + ROBOT_RADIUS # left team
+ game = _make_game(
+ team_is_right=False,
+ field=field,
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-4.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ # goal_point = (keeper_x, post_limit + EDGE_OFFSET)
+ t = defender_x / keeper_x
+ expected_y = t * (post_limit + EDGE_OFFSET)
+ assert captured["target"].x == pytest.approx(defender_x)
+ assert captured["target"].y == pytest.approx(expected_y)
+
+
+def test_custom_geometry_default_goal_frame_y(monkeypatch):
+ """Default goal_frame_y uses post_limit from field, not hardcoded 0.5."""
+ field = _custom_field(-4.5, 0.8, -3.5, 1.0)
+ post_limit = 0.8 - ROBOT_RADIUS
+ game = _make_game(
+ team_is_right=False,
+ field=field,
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ # goal_frame_y = post_limit (not 0.5 or 0.8)
+ # goal_point = (keeper_x, post_limit + EDGE_OFFSET)
+ keeper_x = -4.5 + ROBOT_RADIUS
+ t = _LEFT_DEFENDER_X / keeper_x
+ expected_y = t * (post_limit + EDGE_OFFSET)
+ assert captured["target"].y == pytest.approx(expected_y)
+
+
+def test_custom_geometry_y_clamped_to_custom_defense_half_width(monkeypatch):
+ """Y clamping uses custom defense_half_width, not hardcoded 1.0."""
+ field = _custom_field(-4.5, 0.5, -3.5, 0.3)
+ game = _make_game(
+ team_is_right=False,
+ field=field,
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ # Unclamped y > 0.3 -> clamped
+ assert captured["target"].y == pytest.approx(0.3)
+
+
+# ---------------------------------------------------------------------------
+# Radius-aware target geometry
+# ---------------------------------------------------------------------------
+
+
+def test_shadow_target_uses_keeper_x_not_goal_x(monkeypatch):
+ """The shadow target x-coordinate is keeper_x, not the raw goal line."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={1: SimpleNamespace(p=Vector2D(-3.0, 0.0))},
+ ball_pos=(0.0, 0.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1, goal_frame_y=0.3)
+
+ # If using keeper_x (-4.41) vs goal_x (-4.5), the t parameter differs.
+ # Verify by checking y matches the keeper_x-based formula:
+ t = _LEFT_DEFENDER_X / _LEFT_KEEPER_X
+ expected_y = t * (0.3 + EDGE_OFFSET)
+ assert captured["target"].y == pytest.approx(expected_y)
+
+
+# ---------------------------------------------------------------------------
+# Single-defender dynamic side choice (2-robot case)
+# ---------------------------------------------------------------------------
+
+
+def test_single_defender_picks_side_for_off_centre_ball(monkeypatch):
+ """When ball is clearly off-centre, the defender should pick the correct side."""
+ # Ball far to the positive-y side: defender should cover +post_limit
+ # so that the keeper doesn't have to travel far to cover the near post
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.41, 0.0)), # goalkeeper
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.0)), # defender
+ },
+ ball_pos=(0.0, 2.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+ monkeypatch.setattr(dp, "predict_ball_pos_at_x", lambda game, x: None)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ # Defender should be shifted toward the ball side (positive y)
+ assert captured["target"].y > 0
+
+
+def test_single_defender_picks_opposite_side_for_negative_ball(monkeypatch):
+ """Mirror: ball far negative-y, defender should cover -post_limit."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.41, 0.0)),
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.0)),
+ },
+ ball_pos=(0.0, -2.0),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+ monkeypatch.setattr(dp, "predict_ball_pos_at_x", lambda game, x: None)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+
+ assert captured["target"].y < 0
+
+
+def test_single_defender_centred_ball_uses_keeper_ref_tiebreak(monkeypatch):
+ """With a centred ball, side choice considers keeper reference position."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.41, 0.2)), # keeper slightly positive
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.0)),
+ },
+ ball_pos=(0.0, 0.0),
+ )
+ captured_a = _capture_go_to_point(monkeypatch)
+ monkeypatch.setattr(dp, "predict_ball_pos_at_x", lambda game, x: None)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+ y_a = captured_a["target"].y
+
+ # Now mirror: keeper slightly negative
+ game2 = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.41, -0.2)),
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.0)),
+ },
+ ball_pos=(0.0, 0.0),
+ )
+ captured_b = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game2, motion_controller=object(), robot_id=1)
+ y_b = captured_b["target"].y
+
+ # The two should mirror: keeper-positive -> defender-negative and vice versa
+ assert y_a * y_b < 0 # opposite signs
+
+
+def test_single_defender_uses_predicted_intercept_for_keeper_ref(monkeypatch):
+ """When a ball prediction is available, it is used as the keeper reference."""
+ game = _make_game(
+ team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.41, 0.0)), # keeper at centre
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.0)),
+ },
+ ball_pos=(0.0, 0.0),
+ )
+ # Predict ball heading to positive y
+ monkeypatch.setattr(
+ dp,
+ "predict_ball_pos_at_x",
+ lambda game, x: Vector2D(x, 0.3),
+ )
+ captured = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+ y_with_positive_prediction = captured["target"].y
+
+ # Predict ball heading to negative y
+ monkeypatch.setattr(
+ dp,
+ "predict_ball_pos_at_x",
+ lambda game, x: Vector2D(x, -0.3),
+ )
+ captured2 = _capture_go_to_point(monkeypatch)
+
+ dp.defend_parameter(game, motion_controller=object(), robot_id=1)
+ y_with_negative_prediction = captured2["target"].y
+
+ # Different predictions should produce different (mirrored) side choices
+ assert y_with_positive_prediction * y_with_negative_prediction < 0
diff --git a/utama_core/tests/skills/test_defense_utils.py b/utama_core/tests/skills/test_defense_utils.py
new file mode 100644
index 00000000..77d207bc
--- /dev/null
+++ b/utama_core/tests/skills/test_defense_utils.py
@@ -0,0 +1,174 @@
+from types import SimpleNamespace
+
+import numpy as np
+import pytest
+
+from utama_core.skills.src.utils.defense_utils import (
+ DefenseGeometry,
+ _defense_geometry,
+ calculate_defense_area,
+ clamp_to_goal_width,
+)
+
+
+# ---------------------------------------------------------------------------
+# Field stubs
+# ---------------------------------------------------------------------------
+def _make_field(goal_x, goal_half_width, defense_front_x, defense_half_width, my_team_is_right):
+ return SimpleNamespace(
+ my_goal_line=np.array(
+ [
+ (goal_x, goal_half_width),
+ (goal_x, -goal_half_width),
+ ]
+ ),
+ my_defense_area=np.array(
+ [
+ (goal_x, defense_half_width),
+ (defense_front_x, defense_half_width),
+ (defense_front_x, -defense_half_width),
+ (goal_x, -defense_half_width),
+ (goal_x, defense_half_width),
+ ]
+ ),
+ )
+
+
+def _make_game(my_team_is_right, field):
+ return SimpleNamespace(my_team_is_right=my_team_is_right, field=field)
+
+
+# ---------------------------------------------------------------------------
+# _defense_geometry – standard fields
+# ---------------------------------------------------------------------------
+
+
+def test_defense_geometry_left_goal():
+ field = _make_field(-4.5, 0.5, -3.5, 1.0, False)
+ game = _make_game(False, field)
+ geo = _defense_geometry(game)
+
+ assert geo.goal_x == pytest.approx(-4.5)
+ assert geo.goal_half_width == pytest.approx(0.5)
+ assert geo.defense_front_x == pytest.approx(-3.5)
+ assert geo.defense_depth == pytest.approx(1.0)
+ assert geo.defense_half_width == pytest.approx(1.0)
+
+
+def test_defense_geometry_right_goal():
+ field = _make_field(4.5, 0.5, 3.5, 1.0, True)
+ game = _make_game(True, field)
+ geo = _defense_geometry(game)
+
+ assert geo.goal_x == pytest.approx(4.5)
+ assert geo.goal_half_width == pytest.approx(0.5)
+ assert geo.defense_front_x == pytest.approx(3.5)
+ assert geo.defense_depth == pytest.approx(1.0)
+ assert geo.defense_half_width == pytest.approx(1.0)
+
+
+def test_defense_geometry_custom():
+ field = _make_field(-6.0, 0.8, -4.5, 1.5, False)
+ game = _make_game(False, field)
+ geo = _defense_geometry(game)
+
+ assert geo.goal_x == pytest.approx(-6.0)
+ assert geo.goal_half_width == pytest.approx(0.8)
+ assert geo.defense_front_x == pytest.approx(-4.5)
+ assert geo.defense_depth == pytest.approx(1.5)
+ assert geo.defense_half_width == pytest.approx(1.5)
+
+
+# ---------------------------------------------------------------------------
+# clamp_to_goal_width
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ ("y", "half_width", "expected"),
+ [
+ (0.3, 0.5, 0.3),
+ (0.7, 0.5, 0.5),
+ (-0.9, 0.5, -0.5),
+ (0.3, 0.8, 0.3),
+ (0.9, 0.8, 0.8),
+ (-1.0, 0.8, -0.8),
+ ],
+)
+def test_clamp_to_goal_width(y, half_width, expected):
+ assert clamp_to_goal_width(y, half_width) == pytest.approx(expected)
+
+
+# ---------------------------------------------------------------------------
+# calculate_defense_area – standard field
+# ---------------------------------------------------------------------------
+
+
+def test_calculate_defense_area_standard_field_left_frontmost():
+ """At t=pi (front of curve), the x should be goal_x - (defense_depth + 0.1)."""
+ field = _make_field(-4.5, 0.5, -3.5, 1.0, False)
+ game = _make_game(False, field)
+ pos = calculate_defense_area(game, np.pi)
+
+ # Front extent = 1.0 + 0.1 = 1.1, r=2.1
+ # At t=pi: cos=-1, sin=0 → x_raw = 1.1 * ((1-2.1)*1*(-1) + 2.1*(-1)) = 1.1 * (1.1 - 2.1) = 1.1 * (-1) = -1.1
+ # Left goal: goal_x - x_raw = -4.5 - (-1.1) = -3.4
+ assert pos.x == pytest.approx(-3.4)
+ assert pos.y == pytest.approx(0.0)
+
+
+def test_calculate_defense_area_standard_field_right_frontmost():
+ """At t=pi (front of curve), for right goal."""
+ field = _make_field(4.5, 0.5, 3.5, 1.0, True)
+ game = _make_game(True, field)
+ pos = calculate_defense_area(game, np.pi)
+
+ # Right goal: goal_x + x_raw = 4.5 + (-1.1) = 3.4
+ assert pos.x == pytest.approx(3.4)
+ assert pos.y == pytest.approx(0.0)
+
+
+def test_calculate_defense_area_standard_field_top_bottom():
+ """At t=pi/2 and t=3pi/2, the y extents should be ±(defense_half_width + 0.1)."""
+ field = _make_field(-4.5, 0.5, -3.5, 1.0, False)
+ game = _make_game(False, field)
+
+ top = calculate_defense_area(game, np.pi / 2)
+ bottom = calculate_defense_area(game, 3 * np.pi / 2)
+
+ # At t=pi/2: cos=0, sin=1 → y_raw = 1.1 * ((1-2.1)*1*1 + 2.1*1) = 1.1 * 1 = 1.1
+ assert top.y == pytest.approx(1.1)
+ assert bottom.y == pytest.approx(-1.1)
+ # x at top/bottom should be at goal line
+ assert top.x == pytest.approx(-4.5)
+ assert bottom.x == pytest.approx(-4.5)
+
+
+# ---------------------------------------------------------------------------
+# calculate_defense_area – custom geometry
+# ---------------------------------------------------------------------------
+
+
+def test_calculate_defense_area_custom_geometry_front_extent():
+ """Custom defense_depth=2.0 should produce front_extent=2.1."""
+ field = _make_field(-6.0, 0.8, -4.0, 1.5, False)
+ game = _make_game(False, field)
+ pos = calculate_defense_area(game, np.pi)
+
+ # defense_depth = 2.0, front_extent = 2.1
+ # x_raw at t=pi: 2.1 * (-1) = -2.1
+ # left goal: -6.0 - (-2.1) = -3.9
+ assert pos.x == pytest.approx(-3.9)
+ assert pos.y == pytest.approx(0.0)
+
+
+def test_calculate_defense_area_custom_geometry_vertical_extent():
+ """Custom defense_half_width=1.5 should produce vertical_extent=1.6."""
+ field = _make_field(-6.0, 0.8, -4.0, 1.5, False)
+ game = _make_game(False, field)
+
+ top = calculate_defense_area(game, np.pi / 2)
+
+ # vertical_extent = 1.5 + 0.1 = 1.6
+ assert top.y == pytest.approx(1.6)
+ assert top.x == pytest.approx(-6.0)
diff --git a/utama_core/tests/skills/test_goalkeep.py b/utama_core/tests/skills/test_goalkeep.py
new file mode 100644
index 00000000..37ef3f38
--- /dev/null
+++ b/utama_core/tests/skills/test_goalkeep.py
@@ -0,0 +1,430 @@
+from types import SimpleNamespace
+
+import numpy as np
+import pytest
+
+import utama_core.skills.src.goalkeep as gk
+from utama_core.config.physical_constants import BALL_RADIUS, ROBOT_RADIUS
+from utama_core.entities.data.vector import Vector2D, Vector3D
+from utama_core.skills.src.utils.defense_utils import (
+ intersection_with_x_line,
+ single_defender_stop_y,
+)
+
+EDGE_OFFSET = BALL_RADIUS + ROBOT_RADIUS
+
+# ---------------------------------------------------------------------------
+# Standard-field stub (9x6 field: goal_x = +/-4.5, goal_half_width = 0.5)
+# ---------------------------------------------------------------------------
+_STD_LEFT_GOAL_LINE = np.array([(-4.5, 0.5), (-4.5, -0.5)])
+_STD_RIGHT_GOAL_LINE = np.array([(4.5, 0.5), (4.5, -0.5)])
+
+_LEFT_KEEPER_X = -4.5 + ROBOT_RADIUS
+_RIGHT_KEEPER_X = 4.5 - ROBOT_RADIUS
+_STD_POST_LIMIT = 0.5 - ROBOT_RADIUS
+
+
+def _std_field(my_team_is_right: bool):
+ goal_line = _STD_RIGHT_GOAL_LINE if my_team_is_right else _STD_LEFT_GOAL_LINE
+ return SimpleNamespace(my_goal_line=goal_line, half_goal_width=0.5)
+
+
+# ---------------------------------------------------------------------------
+# Shared helper unit tests (intersection_with_x_line)
+# ---------------------------------------------------------------------------
+
+
+def test_intersection_with_x_line_basic():
+ intersection = intersection_with_x_line(
+ (-1.0, 0.0),
+ (-3.0, 0.1),
+ -4.5,
+ 0.5,
+ )
+
+ assert intersection == pytest.approx((-4.5, 0.175))
+
+
+@pytest.mark.parametrize(
+ ("a", "b", "target_x", "expected"),
+ [
+ ((-3.0, 0.8), (-3.0, 0.9), -4.5, (-4.5, 0.5)),
+ ((-1.0, 0.2), (1.0, -0.4), -4.5, (-4.5, 0.2)),
+ ],
+)
+def test_intersection_with_x_line_handles_vertical_and_moving_away_cases(a, b, target_x, expected):
+ intersection = intersection_with_x_line(a, b, target_x, 0.5)
+
+ assert intersection == pytest.approx(expected)
+
+
+# ---------------------------------------------------------------------------
+# Shared helper unit tests (single_defender_stop_y)
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ ("ball_pos", "defender_pos", "keeper_x", "post_limit", "expected_sign"),
+ [
+ # Defender shifted positive-y relative to ball → keeper covers negative gap
+ (Vector2D(-1.0, -0.3), Vector2D(-3.0, 0.2), -4.5, 0.5, -1),
+ # Mirror: defender shifted negative-y → keeper covers positive gap
+ (Vector2D(-1.0, 0.3), Vector2D(-3.0, -0.2), -4.5, 0.5, 1),
+ ],
+)
+def test_single_defender_stop_y_targets_open_half(ball_pos, defender_pos, keeper_x, post_limit, expected_sign):
+ stop_y = single_defender_stop_y(ball_pos, defender_pos, keeper_x, post_limit, EDGE_OFFSET)
+
+ # The keeper should be on the opposite side of the defender's offset
+ assert stop_y * expected_sign > 0
+
+
+def test_single_defender_stop_y_with_custom_post_limit():
+ """Wider post_limit changes the midpoint used for shadow-based stop_y."""
+ ball_pos = Vector2D(-1.0, -0.3)
+ defender_pos = Vector2D(-3.0, 0.2)
+ keeper_x = -4.5
+ narrow = single_defender_stop_y(ball_pos, defender_pos, keeper_x, 0.5, EDGE_OFFSET)
+ wide = single_defender_stop_y(ball_pos, defender_pos, keeper_x, 1.0, EDGE_OFFSET)
+ # Wider post_limit means larger uncovered region → keeper shifts further
+ assert abs(wide) > abs(narrow)
+
+
+# ---------------------------------------------------------------------------
+# Goalkeep integration tests
+# ---------------------------------------------------------------------------
+
+
+def test_goalkeep_fallback_uses_side_aware_shadow_target(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.2)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, -0.3, 0.0),
+ v=Vector3D(1.0, 0.0, 0.0),
+ ),
+ )
+
+ captured = {}
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["robot_id"] = robot_id
+ captured["target"] = target
+ captured["dribbling"] = dribbling
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ result = gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert result == "sentinel-command"
+ assert captured["robot_id"] == 0
+ assert captured["dribbling"] is True
+ assert captured["target"].x == pytest.approx(_LEFT_KEEPER_X)
+ # Keeper should be in the negative-y gap (defender is at y=0.2, ball at y=-0.3)
+ assert captured["target"].y < 0
+
+
+def test_goalkeep_uses_predicted_intercept_inside_goal(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.2)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, 0.0, 0.0),
+ v=Vector3D(-1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: Vector2D(x, 0.2))
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["robot_id"] = robot_id
+ captured["target"] = target
+ captured["dribbling"] = dribbling
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ result = gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert result == "sentinel-command"
+ assert captured["robot_id"] == 0
+ assert captured["dribbling"] is True
+ assert captured["target"] == Vector2D(_LEFT_KEEPER_X, 0.2)
+
+
+def test_goalkeep_single_keeper_no_prediction_tracks_ball_y(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, 0.2, 0.0),
+ v=Vector3D(1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ result = gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert result == "sentinel-command"
+ assert captured["target"] == Vector2D(_LEFT_KEEPER_X, 0.2)
+
+
+def test_goalkeep_single_keeper_no_prediction_clamps_to_upper_post(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_custom_field(-1.5, 0.4),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-1.35, 0.0)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-0.679, 0.623, 0.0),
+ v=Vector3D(0.078, -2.35, 0.0),
+ ),
+ )
+ captured = {}
+ keeper_x = -1.5 + ROBOT_RADIUS
+ post_limit = 0.4 - ROBOT_RADIUS
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert captured["target"].x == pytest.approx(keeper_x)
+ assert captured["target"].y == pytest.approx(post_limit)
+
+
+def test_goalkeep_single_keeper_no_prediction_clamps_to_lower_post(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_custom_field(-1.5, 0.4),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-1.35, 0.0)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-0.626, -0.495, 0.0),
+ v=Vector3D(0.002, 0.001, 0.0),
+ ),
+ )
+ captured = {}
+ keeper_x = -1.5 + ROBOT_RADIUS
+ post_limit = 0.4 - ROBOT_RADIUS
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert captured["target"].x == pytest.approx(keeper_x)
+ assert captured["target"].y == pytest.approx(-post_limit)
+
+
+def test_goalkeep_three_robots_uses_midpoint_of_two_shadow_edges(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ 1: SimpleNamespace(p=Vector2D(-3.0, 0.0)),
+ 2: SimpleNamespace(p=Vector2D(-3.0, 0.4)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, -0.2, 0.0),
+ v=Vector3D(1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ result = gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert result == "sentinel-command"
+ assert captured["target"].x == pytest.approx(_LEFT_KEEPER_X)
+ # Compute expected: intersection of two shadow edges at keeper_x with post_limit clamp
+ _, yy1 = intersection_with_x_line(
+ (-1.0, -0.2),
+ (-3.0, 0.0 + EDGE_OFFSET),
+ _LEFT_KEEPER_X,
+ _STD_POST_LIMIT,
+ )
+ _, yy2 = intersection_with_x_line(
+ (-1.0, -0.2),
+ (-3.0, 0.4 - EDGE_OFFSET),
+ _LEFT_KEEPER_X,
+ _STD_POST_LIMIT,
+ )
+ assert captured["target"].y == pytest.approx((yy1 + yy2) / 2)
+
+
+def test_goalkeep_missing_expected_defender_id_falls_back_to_centre(monkeypatch):
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_std_field(False),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ 2: SimpleNamespace(p=Vector2D(-3.0, 0.2)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, 0.0, 0.0),
+ v=Vector3D(1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ result = gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert result == "sentinel-command"
+ assert captured["target"] == Vector2D(_LEFT_KEEPER_X, 0.0)
+
+
+# ---------------------------------------------------------------------------
+# Non-standard geometry tests
+# ---------------------------------------------------------------------------
+
+
+def _custom_field(goal_x: float, goal_half_width: float):
+ """Create a field stub with arbitrary goal line."""
+ return SimpleNamespace(
+ my_goal_line=np.array([(goal_x, goal_half_width), (goal_x, -goal_half_width)]),
+ half_goal_width=goal_half_width,
+ )
+
+
+def test_goalkeep_custom_goal_line_changes_intercept_x(monkeypatch):
+ """With a wider goal at x=-6.0, the keeper should target keeper_x = -6.0 + ROBOT_RADIUS."""
+ keeper_x = -6.0 + ROBOT_RADIUS
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_custom_field(-6.0, 0.8),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-5.5, 0.0)),
+ 1: SimpleNamespace(p=Vector2D(-4.0, 0.2)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, -0.3, 0.0),
+ v=Vector3D(1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: None)
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ assert captured["target"].x == pytest.approx(keeper_x)
+
+
+def test_goalkeep_custom_goal_width_changes_clamp_range(monkeypatch):
+ """With goal_half_width=0.8, a prediction at y=0.7 should be kept (not clamped)."""
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_custom_field(-4.5, 0.8),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, 0.0, 0.0),
+ v=Vector3D(-1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+ keeper_x = -4.5 + ROBOT_RADIUS
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: Vector2D(x, 0.7))
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ # 0.7 < 0.8 half-width, so prediction is used directly
+ assert captured["target"] == Vector2D(keeper_x, 0.7)
+
+
+def test_goalkeep_wide_shot_clamps_to_post_limit(monkeypatch):
+ """With goal_half_width=0.3, a prediction at y=0.4 is wide -> clamp to post_limit."""
+ game = SimpleNamespace(
+ my_team_is_right=False,
+ field=_custom_field(-4.5, 0.3),
+ friendly_robots={
+ 0: SimpleNamespace(p=Vector2D(-4.2, 0.0)),
+ },
+ ball=SimpleNamespace(
+ p=Vector3D(-1.0, 0.0, 0.0),
+ v=Vector3D(-1.0, 0.0, 0.0),
+ ),
+ )
+ captured = {}
+ keeper_x = -4.5 + ROBOT_RADIUS
+ post_limit = 0.3 - ROBOT_RADIUS
+
+ monkeypatch.setattr(gk, "predict_ball_pos_at_x", lambda game, x: Vector2D(x, 0.4))
+
+ def fake_go_to_point(game, motion_controller, robot_id, target, dribbling=False):
+ captured["target"] = target
+ return "sentinel-command"
+
+ monkeypatch.setattr(gk, "go_to_point", fake_go_to_point)
+
+ gk.goalkeep(game, motion_controller=object(), robot_id=0)
+
+ # abs(0.4) > 0.3 half-width -> clamp to post_limit
+ assert captured["target"].x == pytest.approx(keeper_x)
+ assert captured["target"].y == pytest.approx(post_limit)
diff --git a/utama_core/tests/strategy_runner/test_ball_placement_rsim.py b/utama_core/tests/strategy_runner/test_ball_placement_rsim.py
new file mode 100644
index 00000000..7274ea3a
--- /dev/null
+++ b/utama_core/tests/strategy_runner/test_ball_placement_rsim.py
@@ -0,0 +1,451 @@
+"""Integration tests for the ball placement feature.
+
+These tests verify that ``BallPlacementStrategy`` (and the underlying
+``BallPlacementStep`` behaviour) satisfies the core requirements of automatic
+ball placement:
+
+ 1. **Approach** — after BALL_PLACEMENT_YELLOW is issued, the placer robot
+ genuinely closes distance to the ball (robot starts far away so the test
+ cannot pass before the strategy acts).
+ 2. **Carry** — once the robot captures the ball (``robot.has_ball`` or
+ proximity), it moves toward ``designated_position`` with the dribbler on.
+ 3. **Clearance** — non-placer robots stay outside ``BALL_KEEP_OUT_DISTANCE``
+ throughout the placement phase. The placer ID is fixed to what was set in
+ reset_field so the check is independent of the implementation's own
+ selection logic.
+ 4. **Placer selection** — when robot 1 is closer to the ball than robot 0,
+ robot 1 is selected as the placer and robot 0 stays away.
+
+Setup
+-----
+- Exhibition Road field (``GREAT_EXHIBITION_FIELD_DIMS``, 4 m × 3 m).
+- 2v2 — two yellow robots (our team) controlled by ``BallPlacementStrategy``;
+ no enemy robots (they would interfere with deterministic positioning).
+- CustomReferee with the "simulation" profile so auto-advance fires and the
+ full BALL_PLACEMENT → DIRECT_FREE → NORMAL_START cycle can complete.
+
+Running
+-------
+ pixi run pytest utama_core/tests/strategy_runner/test_ball_placement_rsim.py -v
+
+Known gap
+---------
+The carry/dribbler phase transition (approach → has_ball → carry) is not tested
+end-to-end because the PID motion controller decelerates before physically
+touching the dribbler geometry, so ``robot.infrared`` (and therefore
+``robot.has_ball``) may not fire reliably. This requires motion controller
+tuning (slower final approach, behind-ball offset) before it can be tested.
+Test 2 currently covers carry progress as a proxy but does not assert dribbler
+state directly.
+
+All four tests are independent: they each start a fresh rsim episode, teleport
+robots and ball to known positions, issue BALL_PLACEMENT_YELLOW directly, and
+then observe the strategy's response over time.
+"""
+
+from __future__ import annotations
+
+import math
+from typing import Optional
+
+import py_trees
+
+from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
+from utama_core.config.referee_constants import BALL_KEEP_OUT_DISTANCE
+from utama_core.custom_referee import CustomReferee
+from utama_core.entities.game import Game
+from utama_core.entities.game.field import FieldBounds
+from utama_core.entities.referee.referee_command import RefereeCommand
+from utama_core.run.strategy_runner import StrategyRunner
+from utama_core.strategy.examples.ball_placement_strategy import BallPlacementStrategy
+from utama_core.team_controller.src.controllers import AbstractSimController
+from utama_core.tests.common.abstract_test_manager import (
+ AbstractTestManager,
+ TestingStatus,
+)
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+# Designated placement target used across all tests (inside exhibition field)
+_TARGET = (0.8, 0.5)
+
+# Tolerance within which a robot is considered to have "approached" the ball
+_APPROACH_TOLERANCE = 0.5 # metres
+
+# Tolerance within which the placer is considered to be "heading to target"
+_TARGET_TOLERANCE = 0.6 # metres
+
+# Clearance margin: robots must be at least this far from the ball
+_CLEAR_MARGIN = 0.05 # metres — allowed slack inside keep-out boundary
+
+
+# ---------------------------------------------------------------------------
+# Shared runner factory
+# ---------------------------------------------------------------------------
+
+
+def _make_runner(referee: CustomReferee) -> StrategyRunner:
+ """Build a 2v2 StrategyRunner on the Exhibition Road field."""
+ return StrategyRunner(
+ strategy=BallPlacementStrategy(),
+ my_team_is_yellow=True,
+ my_team_is_right=True,
+ mode="rsim",
+ exp_friendly=2,
+ exp_enemy=0,
+ exp_ball=True,
+ full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
+ referee=referee,
+ )
+
+
+# ---------------------------------------------------------------------------
+# Test 1: placer robot drives toward the ball
+# ---------------------------------------------------------------------------
+
+
+class _ApproachBallManager(AbstractTestManager):
+ """Verify that after BALL_PLACEMENT_YELLOW the placer genuinely closes on the ball.
+
+ Robot 0 starts 1.5 m from the ball — well outside _APPROACH_TOLERANCE — so
+ the test cannot pass before the strategy acts. We record the initial distance
+ and require it to decrease by at least _APPROACH_PROGRESS metres, proving the
+ robot is actively moving toward the ball rather than already being close.
+ """
+
+ n_episodes = 1
+ # Robot 0 starts this far from the ball; must be > _APPROACH_TOLERANCE so the
+ # test cannot pass before the strategy has done anything.
+ _INITIAL_DIST = 1.5 # metres
+ # How much closer robot 0 must get before the test passes.
+ _APPROACH_PROGRESS = 0.8 # metres
+
+ def __init__(self, referee: CustomReferee) -> None:
+ super().__init__()
+ self._referee = referee
+ self.command_seen: bool = False
+ self.robot_approached_ball: bool = False
+ self._initial_dist: Optional[float] = None
+
+ def reset_field(self, sim_controller: AbstractSimController, game: Game) -> None:
+ # Ball at centre-left; robot 0 starts 1.5 m away along x so it must
+ # travel a meaningful distance before the test can pass.
+ sim_controller.teleport_ball(-0.5, 0.0)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 0, 1.0, 0.0) # 1.5 m from ball
+ sim_controller.teleport_robot(game.my_team_is_yellow, 1, 0.8, -0.8) # kept away
+
+ self._referee.set_command(RefereeCommand.BALL_PLACEMENT_YELLOW, game.ts)
+ self._referee._state.ball_placement_target = _TARGET
+
+ def eval_status(self, game: Game) -> TestingStatus:
+ ref = game.referee
+ if ref is None:
+ return TestingStatus.IN_PROGRESS
+
+ if ref.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW:
+ self.command_seen = True
+
+ if not self.command_seen:
+ return TestingStatus.IN_PROGRESS
+
+ ball = game.ball
+ robot0 = game.friendly_robots.get(0)
+ if ball is None or robot0 is None:
+ return TestingStatus.IN_PROGRESS
+
+ dist = math.hypot(robot0.p.x - ball.p.x, robot0.p.y - ball.p.y)
+
+ if self._initial_dist is None:
+ self._initial_dist = dist
+ return TestingStatus.IN_PROGRESS
+
+ if self._initial_dist - dist >= self._APPROACH_PROGRESS:
+ self.robot_approached_ball = True
+ return TestingStatus.SUCCESS
+
+ return TestingStatus.IN_PROGRESS
+
+
+def test_placer_approaches_ball_after_command(headless: bool) -> None:
+ """After BALL_PLACEMENT_YELLOW, the placer robot closes at least 0.8 m toward the ball."""
+ referee = CustomReferee.from_profile_name("simulation")
+ runner = _make_runner(referee)
+ tm = _ApproachBallManager(referee)
+
+ passed = runner.run_test(tm, episode_timeout=20.0, rsim_headless=headless)
+
+ assert tm.command_seen, "BALL_PLACEMENT_YELLOW was never seen in game.referee"
+ assert tm.robot_approached_ball, (
+ f"Robot 0 did not close {_ApproachBallManager._APPROACH_PROGRESS} m toward the ball "
+ f"(started {_ApproachBallManager._INITIAL_DIST} m away)"
+ )
+ assert passed
+
+
+# ---------------------------------------------------------------------------
+# Test 2: placer moves toward designated_position (carry phase)
+# ---------------------------------------------------------------------------
+
+
+class _CarryToTargetManager(AbstractTestManager):
+ """Verify that the placer moves toward the designated position.
+
+ Robot 0 starts far from the ball, which sits between the robot and the
+ target. Because rsim's infrared sensor does not reliably fire, we test
+ the approach phase: the robot drives toward the ball (and therefore toward
+ the target), and must close at least _PROGRESS_THRESHOLD metres on the
+ target before the episode times out.
+ """
+
+ n_episodes = 1
+
+ # How much closer the placer must get to the target over the observation window
+ _PROGRESS_THRESHOLD = 0.2 # metres
+
+ def __init__(self, referee: CustomReferee) -> None:
+ super().__init__()
+ self._referee = referee
+ self.command_seen: bool = False
+ self.placer_made_progress: bool = False
+ self._initial_dist_to_target: Optional[float] = None
+
+ def reset_field(self, sim_controller: AbstractSimController, game: Game) -> None:
+ # Ball between robot 0 and the target so approach motion reduces
+ # distance to target, making progress measurable without has_ball.
+ sim_controller.teleport_ball(-0.6, 0.0)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 0, -1.3, 0.0)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 1, 0.6, -0.6)
+
+ self._referee.set_command(RefereeCommand.BALL_PLACEMENT_YELLOW, game.ts)
+ self._referee._state.ball_placement_target = _TARGET
+
+ def eval_status(self, game: Game) -> TestingStatus:
+ ref = game.referee
+ if ref is None:
+ return TestingStatus.IN_PROGRESS
+
+ if ref.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW:
+ self.command_seen = True
+
+ if not self.command_seen:
+ return TestingStatus.IN_PROGRESS
+
+ robot0 = game.friendly_robots.get(0)
+ if robot0 is None:
+ return TestingStatus.IN_PROGRESS
+
+ tx, ty = _TARGET
+ dist = math.hypot(robot0.p.x - tx, robot0.p.y - ty)
+
+ if self._initial_dist_to_target is None:
+ self._initial_dist_to_target = dist
+ return TestingStatus.IN_PROGRESS
+
+ improvement = self._initial_dist_to_target - dist
+ if improvement >= self._PROGRESS_THRESHOLD:
+ self.placer_made_progress = True
+ return TestingStatus.SUCCESS
+
+ return TestingStatus.IN_PROGRESS
+
+
+def test_placer_moves_toward_designated_position(headless: bool) -> None:
+ """After capturing the ball, the placer robot moves toward the designated position."""
+ referee = CustomReferee.from_profile_name("simulation")
+ runner = _make_runner(referee)
+ tm = _CarryToTargetManager(referee)
+
+ passed = runner.run_test(tm, episode_timeout=25.0, rsim_headless=headless)
+
+ assert tm.command_seen, "BALL_PLACEMENT_YELLOW was never seen in game.referee"
+ assert tm.placer_made_progress, "Placer robot did not make sufficient progress toward the designated position"
+ assert passed
+
+
+# ---------------------------------------------------------------------------
+# Test 3: non-placer robot stays outside keep-out distance
+# ---------------------------------------------------------------------------
+
+
+class _ClearanceManager(AbstractTestManager):
+ """Verify that non-placer robots respect the ball keep-out distance.
+
+ Robot 0 is placed closest to the ball and is therefore the expected placer.
+ Robot 1 starts inside the keep-out radius. The placer ID is fixed to 0
+ based on what reset_field sets up — it is NOT re-derived from the game state
+ using the same logic as the implementation, which would make the check
+ tautological. Robot 1 must clear and hold outside the keep-out zone.
+ """
+
+ n_episodes = 1
+ _PLACER_ID = 0 # fixed: robot 0 is closest to the ball in reset_field
+ _FRAMES_REQUIRED = 60
+
+ def __init__(self, referee: CustomReferee) -> None:
+ super().__init__()
+ self._referee = referee
+ self.command_seen: bool = False
+ self.clearance_achieved: bool = False
+ self._clear_frame_count: int = 0
+
+ def reset_field(self, sim_controller: AbstractSimController, game: Game) -> None:
+ # Ball at centre so there is plenty of room in all directions for robot 1
+ # to reach 0.8 m clearance. Robot 0 is 0.2 m from the ball → it is the
+ # placer. Robot 1 starts 0.3 m from the ball (inside keep-out) → must clear.
+ sim_controller.teleport_ball(0.0, 0.0)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 0, 0.2, 0.0)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 1, 0.0, 0.3) # within keep-out
+
+ self._referee.set_command(RefereeCommand.BALL_PLACEMENT_YELLOW, game.ts)
+ self._referee._state.ball_placement_target = _TARGET
+
+ def eval_status(self, game: Game) -> TestingStatus:
+ ref = game.referee
+ if ref is None:
+ return TestingStatus.IN_PROGRESS
+
+ if ref.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW:
+ self.command_seen = True
+
+ if not self.command_seen:
+ return TestingStatus.IN_PROGRESS
+
+ ball = game.ball
+ if ball is None:
+ return TestingStatus.IN_PROGRESS
+
+ threshold = BALL_KEEP_OUT_DISTANCE - _CLEAR_MARGIN
+
+ non_placers_clear = all(
+ math.hypot(robot.p.x - ball.p.x, robot.p.y - ball.p.y) >= threshold
+ for rid, robot in game.friendly_robots.items()
+ if rid != self._PLACER_ID
+ )
+
+ if non_placers_clear:
+ self._clear_frame_count += 1
+ else:
+ self._clear_frame_count = 0
+
+ if self._clear_frame_count >= self._FRAMES_REQUIRED:
+ self.clearance_achieved = True
+ return TestingStatus.SUCCESS
+
+ return TestingStatus.IN_PROGRESS
+
+
+def test_non_placer_clears_ball_keep_out_zone(headless: bool) -> None:
+ """Non-placer robot (id=1) clears and holds outside the ball keep-out radius."""
+ referee = CustomReferee.from_profile_name("simulation")
+ runner = _make_runner(referee)
+ tm = _ClearanceManager(referee)
+
+ passed = runner.run_test(tm, episode_timeout=20.0, rsim_headless=headless)
+
+ assert tm.command_seen, "BALL_PLACEMENT_YELLOW was never seen in game.referee"
+ assert tm.clearance_achieved, (
+ f"Robot 1 did not sustain clearance outside "
+ f"BALL_KEEP_OUT_DISTANCE ({BALL_KEEP_OUT_DISTANCE} m) "
+ f"for {_ClearanceManager._FRAMES_REQUIRED} consecutive frames"
+ )
+ assert passed
+
+
+# ---------------------------------------------------------------------------
+# Test 4: placer selection — robot 1 closer to ball is chosen as placer
+# ---------------------------------------------------------------------------
+
+
+class _PlacerSelectionManager(AbstractTestManager):
+ """Verify that the robot closest to the ball is selected as the placer.
+
+ Robot 1 starts closer to the ball than robot 0. The correct behaviour is:
+ - Robot 1 (the placer) closes distance to the ball.
+ - Robot 0 (the non-placer) does NOT drive toward the ball — it should either
+ hold position or move away to clear the keep-out zone.
+
+ This test exercises the min(distance_to_ball) selection logic with robot 0
+ as the non-placer, which the original tests never covered.
+ """
+
+ n_episodes = 1
+ _PLACER_ID = 1 # robot 1 is closer to the ball in reset_field
+ _NON_PLACER_ID = 0
+ _PLACER_PROGRESS = 0.2 # metres robot 1 must close toward the ball
+ _NON_PLACER_GRACE = 0.3 # seconds before we start checking robot 0
+
+ def __init__(self, referee: CustomReferee) -> None:
+ super().__init__()
+ self._referee = referee
+ self.command_seen: bool = False
+ self.placer_approached: bool = False
+ self.non_placer_stayed_away: bool = False
+ self._placer_initial_dist: Optional[float] = None
+ self._non_placer_initial_dist: Optional[float] = None
+ self._command_ts: Optional[float] = None
+
+ def reset_field(self, sim_controller: AbstractSimController, game: Game) -> None:
+ # Ball at centre; robot 1 is 0.3 m from ball, robot 0 is 1.2 m away.
+ sim_controller.teleport_ball(0.0, 0.0)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 0, -1.2, 0.0) # far — non-placer
+ sim_controller.teleport_robot(game.my_team_is_yellow, 1, 0.3, 0.0) # close — placer
+
+ self._referee.set_command(RefereeCommand.BALL_PLACEMENT_YELLOW, game.ts)
+ self._referee._state.ball_placement_target = _TARGET
+
+ def eval_status(self, game: Game) -> TestingStatus:
+ ref = game.referee
+ if ref is None:
+ return TestingStatus.IN_PROGRESS
+
+ if ref.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW:
+ if not self.command_seen:
+ self.command_seen = True
+ self._command_ts = game.ts
+
+ if not self.command_seen:
+ return TestingStatus.IN_PROGRESS
+
+ ball = game.ball
+ robot1 = game.friendly_robots.get(self._PLACER_ID)
+ robot0 = game.friendly_robots.get(self._NON_PLACER_ID)
+ if ball is None or robot1 is None or robot0 is None:
+ return TestingStatus.IN_PROGRESS
+
+ dist1 = math.hypot(robot1.p.x - ball.p.x, robot1.p.y - ball.p.y)
+ dist0 = math.hypot(robot0.p.x - ball.p.x, robot0.p.y - ball.p.y)
+
+ if self._placer_initial_dist is None:
+ self._placer_initial_dist = dist1
+ self._non_placer_initial_dist = dist0
+
+ # Placer (robot 1) must close on the ball.
+ if self._placer_initial_dist - dist1 >= self._PLACER_PROGRESS:
+ self.placer_approached = True
+
+ # Non-placer (robot 0) must not move closer to the ball than it started.
+ # Allow a grace period for the command to propagate, then check.
+ grace_elapsed = (game.ts - self._command_ts) >= self._NON_PLACER_GRACE
+ if grace_elapsed and dist0 >= self._non_placer_initial_dist - 0.1:
+ self.non_placer_stayed_away = True
+
+ if self.placer_approached and self.non_placer_stayed_away:
+ return TestingStatus.SUCCESS
+
+ return TestingStatus.IN_PROGRESS
+
+
+def test_closer_robot_selected_as_placer(headless: bool) -> None:
+ """Robot 1 (closer to ball) is selected as placer; robot 0 does not drive toward ball."""
+ referee = CustomReferee.from_profile_name("simulation")
+ runner = _make_runner(referee)
+ tm = _PlacerSelectionManager(referee)
+
+ passed = runner.run_test(tm, episode_timeout=20.0, rsim_headless=headless)
+
+ assert tm.command_seen, "BALL_PLACEMENT_YELLOW was never seen in game.referee"
+ assert tm.placer_approached, "Robot 1 (closer to ball) did not drive toward the ball — wrong placer selected"
+ assert tm.non_placer_stayed_away, "Robot 0 (non-placer) moved toward the ball — it should clear away"
+ assert passed
diff --git a/utama_core/tests/strategy_runner/test_error_handling.py b/utama_core/tests/strategy_runner/test_error_handling.py
index 1e5e9329..09b1b1ad 100644
--- a/utama_core/tests/strategy_runner/test_error_handling.py
+++ b/utama_core/tests/strategy_runner/test_error_handling.py
@@ -1,5 +1,7 @@
"""Tests for error handling and safety mechanisms in strategies (REAL mode only)."""
+import signal
+import threading
from unittest.mock import MagicMock, patch
import pytest
@@ -48,6 +50,17 @@ def mock_runner():
yield runner
+def assert_zero_velocity_commands(commands_dict, expected_robot_ids):
+ assert set(commands_dict.keys()) == expected_robot_ids
+ for cmd in commands_dict.values():
+ assert cmd.local_forward_vel == 0
+ assert cmd.local_left_vel == 0
+ assert cmd.angular_vel == 0
+ assert not cmd.kick
+ assert not cmd.chip
+ assert not cmd.dribble
+
+
class TestStopRobotsOnClose:
"""Tests for _stop_robots behavior when close() is called."""
@@ -101,14 +114,7 @@ def test_stop_commands_have_zero_velocity(self, mock_runner):
controller = mock_runner.my.strategy.robot_controller
commands_dict = controller.add_robot_commands.call_args[0][0]
-
- for robot_id, cmd in commands_dict.items():
- assert cmd.local_forward_vel == 0
- assert cmd.local_left_vel == 0
- assert cmd.angular_vel == 0
- assert not cmd.kick
- assert not cmd.chip
- assert not cmd.dribble
+ assert_zero_velocity_commands(commands_dict, {0, 1, 2})
def test_stop_not_called_in_rsim_mode(self, mock_runner):
mock_runner.mode = Mode.RSIM
@@ -123,6 +129,28 @@ def test_stop_not_called_in_rsim_mode(self, mock_runner):
class TestStopRobotsOnError:
"""Tests for stop behavior when errors occur during execution."""
+ def test_first_sigint_sets_stop_event_stops_fps_live_and_prints_message(self, mock_runner, capsys):
+ mock_runner._stop_event = threading.Event()
+ fps_live = MagicMock()
+ mock_runner._fps_live = fps_live
+
+ mock_runner._handle_sigint(signal.SIGINT, None)
+
+ assert mock_runner._stop_event.is_set()
+ fps_live.stop.assert_called_once()
+ assert mock_runner._fps_live is None
+ captured = capsys.readouterr()
+ assert "Stopping gracefully" in captured.out
+ assert "Press Ctrl+C again to force quit" in captured.out
+
+ def test_second_sigint_raises_keyboard_interrupt(self, mock_runner):
+ mock_runner._stop_event = threading.Event()
+
+ mock_runner._handle_sigint(signal.SIGINT, None)
+
+ with pytest.raises(KeyboardInterrupt):
+ mock_runner._handle_sigint(signal.SIGINT, None)
+
def test_stop_on_runtime_exception(self, mock_runner):
call_count = {"value": 0}
@@ -172,6 +200,23 @@ def signaled_run_step():
controller = mock_runner.my.strategy.robot_controller
assert controller.add_robot_commands.call_count >= 1
+ def test_run_sends_stop_robot_commands_on_first_sigint_shutdown(self, mock_runner):
+ mock_runner._stop_event = threading.Event()
+
+ def signaled_run_step():
+ mock_runner._handle_sigint(signal.SIGINT, None)
+
+ mock_runner._run_step = signaled_run_step
+
+ mock_runner.run()
+
+ controller = mock_runner.my.strategy.robot_controller
+ assert controller.add_robot_commands.call_count == 20
+ assert controller.send_robot_commands.call_count == 20
+
+ commands_dict = controller.add_robot_commands.call_args[0][0]
+ assert_zero_velocity_commands(commands_dict, {0, 1})
+
if __name__ == "__main__":
pytest.main([__file__, "-v"])
diff --git a/utama_core/tests/strategy_runner/test_referee_rsim.py b/utama_core/tests/strategy_runner/test_referee_rsim.py
index 67f3877e..1d4ae3b2 100644
--- a/utama_core/tests/strategy_runner/test_referee_rsim.py
+++ b/utama_core/tests/strategy_runner/test_referee_rsim.py
@@ -37,13 +37,19 @@
import py_trees
+from utama_core.config.field_params import STANDARD_FIELD_DIMS
from utama_core.config.referee_constants import BALL_KEEP_OUT_DISTANCE
from utama_core.custom_referee import CustomReferee
+from utama_core.custom_referee.geometry import RefereeGeometry
+from utama_core.custom_referee.rules.out_of_bounds_rule import OutOfBoundsRule
from utama_core.entities.game import Game
from utama_core.entities.game.field import FieldBounds
from utama_core.entities.referee.referee_command import RefereeCommand
from utama_core.run.strategy_runner import StrategyRunner
+from utama_core.skills.src.go_to_ball import go_to_ball
+from utama_core.strategy.common.abstract_behaviour import AbstractBehaviour
from utama_core.strategy.common.abstract_strategy import AbstractStrategy
+from utama_core.strategy.examples.utils import SetBlackboardVariable
from utama_core.team_controller.src.controllers import AbstractSimController
from utama_core.tests.common.abstract_test_manager import (
AbstractTestManager,
@@ -86,6 +92,69 @@ def get_min_bounding_req(self) -> Optional[FieldBounds]:
return None
+class _StateHasBall(AbstractBehaviour):
+ def __init__(self, robot_id_key: str, name: str = "StateHasBall"):
+ super().__init__(name=name)
+ self.robot_id_key = robot_id_key
+
+ def setup_(self):
+ self.blackboard.register_key(key=self.robot_id_key, access=py_trees.common.Access.READ)
+
+ def update(self) -> py_trees.common.Status:
+ robot_id = self.blackboard.get(self.robot_id_key)
+ robot = self.blackboard.game.friendly_robots[robot_id]
+ return py_trees.common.Status.SUCCESS if robot.has_ball else py_trees.common.Status.FAILURE
+
+
+class _GoToBallUntilStatePossession(AbstractBehaviour):
+ def __init__(self, robot_id_key: str, name: str = "GoToBallUntilStatePossession"):
+ super().__init__(name=name)
+ self.robot_id_key = robot_id_key
+
+ def setup_(self):
+ self.blackboard.register_key(key=self.robot_id_key, access=py_trees.common.Access.READ)
+
+ def update(self) -> py_trees.common.Status:
+ robot_id = self.blackboard.get(self.robot_id_key)
+ self.blackboard.cmd_map[robot_id] = go_to_ball(self.blackboard.game, self.blackboard.motion_controller, robot_id)
+ return py_trees.common.Status.RUNNING
+
+
+class _StateGoToBallStrategy(AbstractStrategy):
+ exp_ball: bool = True
+
+ def __init__(self, robot_id: int):
+ self.robot_id = robot_id
+ self.robot_id_key = "robot_id"
+ super().__init__()
+
+ def create_behaviour_tree(self) -> py_trees.behaviour.Behaviour:
+ root = py_trees.composites.Sequence(name="StateGoToBallRoot", memory=True)
+ go_to_ball_until_possession = py_trees.composites.Selector(name="StateGoToBall", memory=False)
+ go_to_ball_until_possession.add_children(
+ [
+ _StateHasBall(robot_id_key=self.robot_id_key),
+ _GoToBallUntilStatePossession(robot_id_key=self.robot_id_key),
+ ]
+ )
+ root.add_children(
+ [
+ SetBlackboardVariable(name="SetRobotID", variable_name=self.robot_id_key, value=self.robot_id),
+ go_to_ball_until_possession,
+ ]
+ )
+ return root
+
+ def assert_exp_robots(self, n_runtime_friendly: int, n_runtime_enemy: int) -> bool:
+ return n_runtime_friendly == 1
+
+ def assert_exp_goals(self, includes_my_goal_line: bool, includes_opp_goal_line: bool) -> bool:
+ return True
+
+ def get_min_bounding_req(self) -> Optional[FieldBounds]:
+ return None
+
+
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
@@ -177,15 +246,10 @@ def test_ball_placement_robot_approaches_designated_position(headless):
class _DirectFreeOursManager(AbstractTestManager):
- """Ball exits the side boundary with no friendly robot nearby → DIRECT_FREE_YELLOW (ours).
-
- With no robot close enough to the ball to register a friendly last-touch,
- OutOfBoundsRule defaults to DIRECT_FREE_YELLOW. We verify the kicker
- drives toward the (now out-of-bounds) ball.
- """
+ """DIRECT_FREE_YELLOW is injected directly; kicker must drive toward the ball."""
n_episodes = 1
- APPROACH_TOLERANCE = 0.6 # slightly wider: ball may be just outside boundary
+ APPROACH_TOLERANCE = 0.6
def __init__(self, referee: CustomReferee):
super().__init__()
@@ -194,13 +258,12 @@ def __init__(self, referee: CustomReferee):
self.robot_near_ball: bool = False
def reset_field(self, sim_controller: AbstractSimController, game: Game):
- # Keep all robots well away from the ball so last-touch is unknown → DIRECT_FREE_YELLOW
sim_controller.teleport_robot(game.my_team_is_yellow, 0, -1.5, 0.0)
sim_controller.teleport_robot(game.my_team_is_yellow, 1, -2.0, 0.5)
sim_controller.teleport_robot(game.my_team_is_yellow, 2, -2.0, -0.5)
- # Ball heading out the top sideline
- sim_controller.teleport_ball(0.0, 2.5, vx=0.0, vy=2.5)
- self._referee.set_command(RefereeCommand.FORCE_START, game.ts)
+ sim_controller.teleport_ball(0.0, 0.0)
+ # Inject directly — bypass OOB detection which now routes through ball placement first.
+ self._referee.force_command(RefereeCommand.DIRECT_FREE_YELLOW, game.ts)
def eval_status(self, game: Game) -> TestingStatus:
ref = game.referee
@@ -245,34 +308,26 @@ def test_direct_free_kick_ours_robot_drives_to_ball(headless):
class _DirectFreeTheirsManager(AbstractTestManager):
- """A robot starts inside the keep-out radius; DIRECT_FREE_BLUE (theirs) is issued.
-
- We place robot 0 right next to the ball before it exits so last-touch registers
- as friendly → OutOfBoundsRule issues DIRECT_FREE_BLUE (opponent's free kick).
- The test verifies that all robots end up outside the keep-out radius.
- """
+ """DIRECT_FREE_BLUE is injected directly; all robots must clear the keep-out radius."""
n_episodes = 1
- # All robots must clear beyond this radius from the ball position.
- CLEAR_TOLERANCE = 0.1 # allowed margin inside keep-out (robots should be well clear)
+ CLEAR_TOLERANCE = 0.1
def __init__(self, referee: CustomReferee):
super().__init__()
self._referee = referee
self.direct_free_seen: bool = False
self.robots_cleared: bool = False
- # We record the ball position when DIRECT_FREE_BLUE fires to check clearing.
self._ball_pos_at_call: Optional[tuple[float, float]] = None
def reset_field(self, sim_controller: AbstractSimController, game: Game):
- # Robot 0 is placed right next to the ball — it will register as last-toucher.
- sim_controller.teleport_robot(game.my_team_is_yellow, 0, 0.0, 2.4)
- # Robots 1 and 2 also start near the ball path — both inside keep-out.
- sim_controller.teleport_robot(game.my_team_is_yellow, 1, 0.1, 2.3)
- sim_controller.teleport_robot(game.my_team_is_yellow, 2, -0.1, 2.3)
- # Ball heading out the top sideline; robot 0 is close enough for last-touch
- sim_controller.teleport_ball(0.0, 2.5, vx=0.0, vy=2.5)
- self._referee.set_command(RefereeCommand.FORCE_START, game.ts)
+ # All robots start inside keep-out radius around the ball.
+ sim_controller.teleport_robot(game.my_team_is_yellow, 0, 0.0, 0.3)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 1, 0.1, -0.3)
+ sim_controller.teleport_robot(game.my_team_is_yellow, 2, -0.1, 0.3)
+ sim_controller.teleport_ball(0.0, 0.0)
+ # Inject directly — bypass OOB detection which now routes through ball placement first.
+ self._referee.force_command(RefereeCommand.DIRECT_FREE_BLUE, game.ts)
def eval_status(self, game: Game) -> TestingStatus:
ref = game.referee
@@ -387,6 +442,69 @@ def test_prepare_kickoff_robots_form_on_own_half_outside_circle(headless):
assert passed, "Robots did not sustain a legal kickoff formation for the required number of frames"
+# ---------------------------------------------------------------------------
+# Scenario 4: out-of-bounds restart spot can be played
+# ---------------------------------------------------------------------------
+
+
+class _RestartSpotCaptureManager(AbstractTestManager):
+ """Places the ball at the out-of-bounds restart spot and verifies capture.
+
+ This guards against restart positions that are technically infield but too
+ close to the wall for the robot/dribbler geometry to acquire the ball.
+ """
+
+ n_episodes = 1
+
+ def __init__(self, restart_spot: tuple[float, float]):
+ super().__init__()
+ self.restart_spot = restart_spot
+ self.robot_has_ball = False
+ self.min_distance_to_ball = math.inf
+
+ def reset_field(self, sim_controller: AbstractSimController, game: Game):
+ target_x, target_y = self.restart_spot
+ sim_controller.teleport_robot(game.my_team_is_yellow, 0, target_x, target_y - 0.9, math.pi / 2)
+ sim_controller.teleport_ball(target_x, target_y)
+
+ def eval_status(self, game: Game) -> TestingStatus:
+ robot = game.friendly_robots[0]
+ ball = game.ball
+ self.min_distance_to_ball = min(
+ self.min_distance_to_ball,
+ math.hypot(robot.p.x - ball.p.x, robot.p.y - ball.p.y),
+ )
+ if robot.has_ball:
+ self.robot_has_ball = True
+ return TestingStatus.SUCCESS
+ return TestingStatus.IN_PROGRESS
+
+
+def test_out_of_bounds_restart_spot_is_capturable_by_go_to_ball(headless):
+ geometry = RefereeGeometry.from_field_dims(STANDARD_FIELD_DIMS)
+ restart_spot = OutOfBoundsRule._nearest_infield_point(0.0, geometry.half_width + 0.5, geometry)
+
+ runner = StrategyRunner(
+ strategy=_StateGoToBallStrategy(robot_id=0),
+ my_team_is_yellow=True,
+ my_team_is_right=False,
+ mode="rsim",
+ exp_friendly=1,
+ exp_enemy=0,
+ exp_ball=True,
+ referee=None,
+ )
+ tm = _RestartSpotCaptureManager(restart_spot)
+
+ passed = runner.run_test(tm, episode_timeout=5.0, rsim_headless=headless)
+
+ assert tm.robot_has_ball, (
+ "Robot could not acquire the out-of-bounds restart ball at "
+ f"{restart_spot}; closest approach was {tm.min_distance_to_ball:.3f} m"
+ )
+ assert passed
+
+
# ---------------------------------------------------------------------------
# Future work: full out-of-bounds sequence integration test
#
diff --git a/utama_core/tests/strategy_runner/test_runner_misconfig.py b/utama_core/tests/strategy_runner/test_runner_misconfig.py
index 9aa805b4..a963838f 100644
--- a/utama_core/tests/strategy_runner/test_runner_misconfig.py
+++ b/utama_core/tests/strategy_runner/test_runner_misconfig.py
@@ -389,8 +389,8 @@ def test_validate_vision_to_cmd_mapping_incorrect_length_enemy():
def test_validate_vision_to_cmd_mapping_correct_count_non_contiguous_ids_passes_init():
# Non-contiguous vision IDs (e.g. real field robots numbered 5,6,7) with the right
- # count must PASS at init time — coverage against observed IDs is validated later
- # in _validate_mapping_covers_game_frame() after _load_game().
+ # count must PASS at init time — the allowlist derived from the mapping ensures only
+ # those IDs are accepted from vision at runtime.
runner = SimpleNamespace(
mode=Mode.REAL,
my_team_is_yellow=True,
@@ -482,6 +482,24 @@ def test_trusted_ir_robots_blue_team_is_my_team():
assert runner.my.robot_info_refiner._trusted_ir_robots == frozenset({2})
+def test_blue_allowlist_not_set_in_single_team_real_mode():
+ # In single-team real mode, blue robots are tracked as enemies and must not be filtered.
+ # _allowed_blue must be None even when blue_vision_to_cmd_mapping resolves to {}.
+ runner = SimpleNamespace(
+ mode=Mode.REAL,
+ my_team_is_yellow=True,
+ opp=None,
+ exp_friendly=1,
+ exp_enemy=0,
+ )
+ # Validate mapping returns {} for blue (no opp, mapping=None)
+ blue_mapping = StrategyRunner._validate_vision_to_cmd_mapping(runner, None, is_yellow=False)
+ assert blue_mapping == {}
+ # Simulate the allowlist derivation: empty mapping with no opp → None, not frozenset()
+ _allowed_blue = (frozenset(blue_mapping) or None) if runner.opp is not None else None
+ assert _allowed_blue is None
+
+
def test_trusted_ir_robots_both_teams_pvp():
"""Both colour params route correctly in PVP mode."""
runner = StrategyRunner(
diff --git a/utama_core/tests/strategy_runner/test_scenario_hooks.py b/utama_core/tests/strategy_runner/test_scenario_hooks.py
new file mode 100644
index 00000000..867e15d0
--- /dev/null
+++ b/utama_core/tests/strategy_runner/test_scenario_hooks.py
@@ -0,0 +1,53 @@
+from unittest.mock import Mock
+
+from utama_core.custom_referee import CustomReferee
+from utama_core.entities.referee.referee_command import RefereeCommand
+from utama_core.rsoccer_simulator.src.ssl import ssl_gym_base
+from utama_core.run.strategy_runner import StrategyRunner
+
+
+def test_strategy_runner_step_once_delegates_to_single_step_loop():
+ runner = StrategyRunner.__new__(StrategyRunner)
+ runner._run_step = Mock()
+
+ runner.step_once()
+
+ runner._run_step.assert_called_once_with()
+
+
+def test_custom_referee_set_command_accepts_scripted_metadata():
+ referee = CustomReferee.from_profile_name("simulation")
+
+ referee.set_command(
+ RefereeCommand.BALL_PLACEMENT_YELLOW,
+ timestamp=3.0,
+ designated_position=(1.0, -0.5),
+ next_command=RefereeCommand.NORMAL_START,
+ status_message="scripted placement",
+ )
+ data = referee.step(game_frame=None, current_time=3.0)
+
+ assert data.referee_command == RefereeCommand.BALL_PLACEMENT_YELLOW
+ assert data.designated_position == (1.0, -0.5)
+ assert data.next_command == RefereeCommand.NORMAL_START
+ assert data.status_message == "scripted placement"
+
+
+def test_ssl_base_env_reset_seeds_random_generators(monkeypatch):
+ random_seed = Mock()
+ numpy_seed = Mock()
+ monkeypatch.setattr(ssl_gym_base.random, "seed", random_seed)
+ monkeypatch.setattr(ssl_gym_base.np.random, "seed", numpy_seed)
+
+ env = ssl_gym_base.SSLBaseEnv.__new__(ssl_gym_base.SSLBaseEnv)
+ env.render_mode = None
+ env.rsim = Mock()
+ env.rsim.get_frame.return_value = object()
+ env._get_initial_positions_frame = Mock(return_value=object())
+ env._frame_to_observations = Mock(return_value=("obs",))
+
+ result = ssl_gym_base.SSLBaseEnv.reset(env, seed=42)
+
+ random_seed.assert_called_once_with(42)
+ numpy_seed.assert_called_once_with(42)
+ assert result == (("obs",), {})