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: +
+ +
@@ -741,6 +783,12 @@ def _build_static_config(profile: "RefereeProfile") -> dict:
+ +
+ + +
+
Profile — loading…
@@ -1004,6 +1052,80 @@ def _build_static_config(profile: "RefereeProfile") -> dict: return `
${title}
${rows}
`; } +// --- 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",), {})