Skip to content

Commit 92bfe05

Browse files
add FieldBounds overlay and rendered rsim field based on FieldDimensions
1 parent 5719111 commit 92bfe05

File tree

7 files changed

+146
-8
lines changed

7 files changed

+146
-8
lines changed

main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
12
from utama_core.entities.game.field import FieldBounds
23
from utama_core.replay import ReplayWriterConfig
34
from utama_core.rsoccer_simulator.src.Utils.gaussian_noise import RsimGaussianNoise
@@ -15,17 +16,18 @@
1516
def main():
1617
# Setup for real testing
1718
# Custom field size based setup in real
18-
custom_bounds = FieldBounds(top_left=(-4.5, 1.5), bottom_right=(-2.25, -1.5))
19+
custom_bounds = FieldBounds(top_left=(-2, 1.5), bottom_right=(1, -1.5))
1920

2021
runner = StrategyRunner(
2122
strategy=RandomMovementStrategy(n_robots=2, field_bounds=custom_bounds, endpoint_tolerance=0.1, seed=42),
2223
my_team_is_yellow=True,
2324
my_team_is_right=True,
24-
mode="grsim",
25+
mode="rsim",
2526
exp_friendly=2,
2627
exp_enemy=0,
2728
replay_writer_config=ReplayWriterConfig(replay_name="test_replay", overwrite_existing=True),
2829
field_bounds=custom_bounds,
30+
full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
2931
print_real_fps=True,
3032
profiler_name=None,
3133
)

utama_core/rsoccer_simulator/src/Render/field.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,38 @@ class SSLRenderField(VSSRenderField):
263263
corner_arc_r = 0.01
264264
_scale = 100
265265

266+
def __init__(
267+
self,
268+
length: float | None = None,
269+
width: float | None = None,
270+
penalty_length: float | None = None,
271+
penalty_width: float | None = None,
272+
goal_width: float | None = None,
273+
goal_depth: float | None = None,
274+
margin: float | None = None,
275+
center_circle_r: float | None = None,
276+
scale: float | None = None,
277+
):
278+
if length is not None:
279+
self.length = length
280+
if width is not None:
281+
self.width = width
282+
if penalty_length is not None:
283+
self.penalty_length = penalty_length
284+
if penalty_width is not None:
285+
self.penalty_width = penalty_width
286+
if goal_width is not None:
287+
self.goal_width = goal_width
288+
if goal_depth is not None:
289+
self.goal_depth = goal_depth
290+
if margin is not None:
291+
self.margin = margin
292+
if center_circle_r is not None:
293+
self.center_circle_r = center_circle_r
294+
if scale is not None:
295+
self._scale = scale
296+
super().__init__()
297+
266298

267299
if __name__ == "__main__":
268300
field = Sim2DRenderField()

utama_core/rsoccer_simulator/src/ssl/envs/standard_ssl.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from numpy.random import normal
66

7-
from utama_core.config.field_params import STANDARD_FIELD_DIMS
7+
from utama_core.config.field_params import STANDARD_FIELD_DIMS, FieldDimensions
88
from utama_core.config.formations import FormationEntry, FormationType, get_formations
99
from utama_core.config.robot_params import RSIM_PARAMS
1010
from utama_core.config.settings import (
@@ -83,16 +83,28 @@ def __init__(
8383
time_step: float = TIMESTEP,
8484
blue_starting_formation: Optional[list[FormationEntry]] = None,
8585
yellow_starting_formation: Optional[list[FormationEntry]] = None,
86+
full_field_dims: Optional[FieldDimensions] = None,
8687
ball_starting_position: Optional[Tuple[float, float]] = None,
8788
gaussian_noise: RsimGaussianNoise = RsimGaussianNoise(),
8889
vanishing: float = 0,
8990
):
91+
render_field_overrides = None
92+
if full_field_dims is not None:
93+
render_field_overrides = {
94+
"length": 2 * full_field_dims.full_field_half_length,
95+
"width": 2 * full_field_dims.full_field_half_width,
96+
"penalty_length": 2 * full_field_dims.half_defense_area_depth,
97+
"penalty_width": 2 * full_field_dims.half_defense_area_width,
98+
"goal_width": 2 * full_field_dims.half_goal_width,
99+
}
100+
90101
super().__init__(
91102
field_type=field_type,
92103
n_robots_blue=n_robots_blue,
93104
n_robots_yellow=n_robots_yellow,
94105
time_step=time_step,
95106
render_mode=render_mode,
107+
render_field_overrides=render_field_overrides,
96108
)
97109

98110
# NOTE: observation_space and action_space removed - not needed for non-RL use

utama_core/rsoccer_simulator/src/ssl/ssl_gym_base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# - To create your wrapper from env to communcation, use inherit from this class!
55
"""
66

7-
from typing import List
7+
from typing import List, Optional
88

99
import numpy as np
1010
import pygame
@@ -42,6 +42,7 @@ def __init__(
4242
n_robots_yellow: int,
4343
time_step: float,
4444
render_mode=None,
45+
render_field_overrides: Optional[dict[str, float]] = None,
4546
):
4647
# Initialize Simulator
4748
self.render_mode = render_mode
@@ -73,7 +74,7 @@ def __init__(
7374
self.overlay: list[OverlayObject] = []
7475

7576
# Render
76-
self.field_renderer = SSLRenderField()
77+
self.field_renderer = SSLRenderField(**render_field_overrides) if render_field_overrides else SSLRenderField()
7778
self.window_surface = None
7879
self.window_size = self.field_renderer.window_size
7980
self.clock = None

utama_core/run/strategy_runner.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ def _load_sim(
376376
render_mode=None,
377377
blue_starting_formation=blue_start,
378378
yellow_starting_formation=yellow_start,
379+
full_field_dims=self.full_field_dims,
379380
ball_starting_position=self.field_bounds.center,
380381
gaussian_noise=rsim_noise,
381382
vanishing=rsim_vanishing,
@@ -645,12 +646,13 @@ def _stop_robots(self, repeat: int = 1):
645646
def build_commands(team: SideRuntime) -> dict[int, RobotCommand]:
646647
return {robot_id: RobotCommand(0, 0, 0, 0, 0, 0) for robot_id in team.game.friendly_robots.keys()}
647648

648-
my_cmds = build_commands(self.my)
649+
my_cmds = build_commands(self.my) if getattr(self.my, "_game", None) is not None else None
649650
opp_cmds = build_commands(self.opp) if self.opp and getattr(self.opp, "_game", None) is not None else None
650651

651652
for _ in range(repeat):
652-
self.my.strategy.robot_controller.add_robot_commands(my_cmds)
653-
self.my.strategy.robot_controller.send_robot_commands()
653+
if my_cmds:
654+
self.my.strategy.robot_controller.add_robot_commands(my_cmds)
655+
self.my.strategy.robot_controller.send_robot_commands()
654656

655657
if opp_cmds:
656658
self.opp.strategy.robot_controller.add_robot_commands(opp_cmds)
@@ -794,6 +796,8 @@ def _run_step(self):
794796
No return value; updates internal game state and controllers.
795797
"""
796798
frame_start = time.perf_counter()
799+
self._draw_rsim_field_bounds_overlay()
800+
797801
if self.mode == Mode.RSIM:
798802
vision_frames = [self.rsim_env._frame_to_observations()[0]]
799803
else:
@@ -835,6 +839,26 @@ def _run_step(self):
835839
self.elapsed_time = 0.0
836840
self.num_frames_elapsed = 0
837841

842+
def _draw_rsim_field_bounds_overlay(self) -> None:
843+
"""Draw active field bounds overlay in RSIM human render mode."""
844+
if self.mode != Mode.RSIM or not self.rsim_env:
845+
return
846+
847+
# Overlays are cleared during render; only enqueue when the frame will be rendered.
848+
if self.rsim_env.render_mode != "human":
849+
return
850+
851+
top_left = self.field_bounds.top_left
852+
bottom_right = self.field_bounds.bottom_right
853+
854+
bounds_polygon = [
855+
(top_left[0], top_left[1]),
856+
(bottom_right[0], top_left[1]),
857+
(bottom_right[0], bottom_right[1]),
858+
(top_left[0], bottom_right[1]),
859+
]
860+
self.rsim_env.draw_polygon(bounds_polygon, color="PINK", width=2)
861+
838862
def _step_game(
839863
self,
840864
vision_frames: List[RawVisionData],

utama_core/tests/strategy_runner/test_rsim_formations.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import py_trees
44
import pytest
55

6+
from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
67
from utama_core.config.formations import FormationType, get_formations
78
from utama_core.entities.game.field import FieldBounds
89
from utama_core.global_utils.mapping_utils import map_left_right_to_colors
@@ -139,3 +140,34 @@ def test_rsim_spawn_respects_shifted_bounds_and_ball_center():
139140
assert manager.ball_position is not None
140141
assert abs(manager.ball_position[0] - expected_x) <= POSITION_TOLERANCE
141142
assert abs(manager.ball_position[1] - expected_y) <= POSITION_TOLERANCE
143+
144+
145+
def test_rsim_renderer_resizes_with_non_standard_field_dimensions():
146+
dims = GREAT_EXHIBITION_FIELD_DIMS
147+
runner = StrategyRunner(
148+
strategy=_IdleStrategy(),
149+
my_team_is_yellow=True,
150+
my_team_is_right=False,
151+
mode="rsim",
152+
exp_friendly=1,
153+
exp_enemy=0,
154+
full_field_dims=dims,
155+
)
156+
157+
renderer = runner.rsim_env.field_renderer
158+
scale = renderer.scale
159+
160+
assert renderer.length == pytest.approx(2 * dims.full_field_half_length * scale)
161+
assert renderer.width == pytest.approx(2 * dims.full_field_half_width * scale)
162+
assert renderer.penalty_length == pytest.approx(2 * dims.half_defense_area_depth * scale)
163+
assert renderer.penalty_width == pytest.approx(2 * dims.half_defense_area_width * scale)
164+
assert renderer.goal_width == pytest.approx(2 * dims.half_goal_width * scale)
165+
166+
# Goal line and boundary should be centered and align with resized geometry.
167+
goal_top = (renderer.screen_height - renderer.goal_width) / 2
168+
goal_bottom = goal_top + renderer.goal_width
169+
170+
assert renderer.margin == pytest.approx(renderer.center_x - renderer.length / 2)
171+
assert renderer.margin == pytest.approx(renderer.center_y - renderer.width / 2)
172+
assert goal_top == pytest.approx(renderer.center_y - dims.half_goal_width * scale)
173+
assert goal_bottom == pytest.approx(renderer.center_y + dims.half_goal_width * scale)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
from utama_core.config.enums import Mode
4+
from utama_core.entities.game.field import FieldBounds
5+
from utama_core.run.strategy_runner import StrategyRunner
6+
7+
8+
def _make_runner_for_overlay_tests(render_mode: str | None):
9+
with patch.object(StrategyRunner, "__init__", lambda self: None):
10+
runner = StrategyRunner()
11+
runner.mode = Mode.RSIM
12+
runner.field_bounds = FieldBounds(top_left=(-1.0, 2.0), bottom_right=(3.0, -4.0))
13+
runner.rsim_env = MagicMock()
14+
runner.rsim_env.render_mode = render_mode
15+
return runner
16+
17+
18+
def test_draw_rsim_field_bounds_overlay_draws_expected_polygon():
19+
runner = _make_runner_for_overlay_tests(render_mode="human")
20+
21+
runner._draw_rsim_field_bounds_overlay()
22+
23+
runner.rsim_env.draw_polygon.assert_called_once_with(
24+
[(-1.0, 2.0), (3.0, 2.0), (3.0, -4.0), (-1.0, -4.0)],
25+
color="PINK",
26+
width=2,
27+
)
28+
29+
30+
def test_draw_rsim_field_bounds_overlay_not_drawn_when_not_human():
31+
runner = _make_runner_for_overlay_tests(render_mode=None)
32+
33+
runner._draw_rsim_field_bounds_overlay()
34+
35+
runner.rsim_env.draw_polygon.assert_not_called()

0 commit comments

Comments
 (0)