Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0ee5652
the dwa is cooked
Jawad12256 Nov 19, 2025
bc68db2
Merge branch 'main' into dwa-new
wowthecoder Nov 19, 2025
0f4327b
Merge branch 'main' into dwa-new
wowthecoder Nov 24, 2025
ad2bd95
test: write simple straight line test
wowthecoder Nov 25, 2025
855fb39
test: uncomment straight line test with obstacles
wowthecoder Nov 25, 2025
7be09fa
test: moving robots obstacle course, also added separate motion contr…
wowthecoder Nov 25, 2025
c2a697a
test: implement unit test of 12 robots dashing straight at each other
wowthecoder Nov 26, 2025
c75f02a
test: implement test with 6 robots moving randomly in a half court
wowthecoder Nov 26, 2025
37eeb8e
revert dwa config and planner
wowthecoder Nov 26, 2025
d0b3a6d
rename test file
wowthecoder Nov 26, 2025
7a47ec2
Merge branch 'main' into motion-planning-tests
wowthecoder Nov 26, 2025
cc57336
changed to from 6 to 2 robots so inevitable collisions are reduced
valentinbruehl Nov 30, 2025
dba9797
Random movement test terminates after 3 targets reached
valentinbruehl Dec 2, 2025
cb20b79
Update utama_core/tests/motion_planning/random_movement_test.py
energy-in-joles Dec 23, 2025
894d470
Update utama_core/tests/motion_planning/multiple_robots_test.py
energy-in-joles Dec 23, 2025
6efe37c
Update utama_core/tests/motion_planning/multiple_robots_test.py
energy-in-joles Dec 23, 2025
c386f6c
Update utama_core/tests/motion_planning/strategies/multi_robot_naviga…
energy-in-joles Dec 23, 2025
cce9e3e
Update utama_core/tests/motion_planning/strategies/random_movement_st…
energy-in-joles Dec 23, 2025
f6cee66
Update utama_core/tests/motion_planning/multiple_robots_test.py
energy-in-joles Dec 23, 2025
7745d3c
copilot changes
energy-in-joles Dec 23, 2025
fc8e8bd
removed unused imports
energy-in-joles Dec 24, 2025
29405aa
replace raw values
energy-in-joles Dec 24, 2025
56cd758
remove unused variables
energy-in-joles Dec 24, 2025
97f83d2
Update utama_core/tests/motion_planning/strategies/simple_navigation_…
energy-in-joles Dec 24, 2025
f8d5364
remove unused imports
energy-in-joles Dec 24, 2025
52cbeeb
Merge branch 'motion-planning-tests' of https://github.com/First-Orde…
energy-in-joles Dec 24, 2025
a2645bb
some minor changes
energy-in-joles Dec 24, 2025
eefa9cb
Merge branch 'main' into motion-planning-tests
wowthecoder Jan 9, 2026
4e09fb3
refactor: remove print in when robot reach random target
wowthecoder Jan 9, 2026
08a2446
feat: extract n_robots parameter and restore it to 6
wowthecoder Jan 9, 2026
98506ef
refactor: added typing for dict variable
wowthecoder Jan 9, 2026
e71c569
test: exclude motion planning tests for now until the algos work
wowthecoder Jan 9, 2026
f117112
doc: changed test function name and doc string to better describe the…
wowthecoder Jan 12, 2026
c89fab4
doc: add description for n_robots variable
wowthecoder Jan 12, 2026
f0b0326
test: removed custom control scheme in tests so it uses the default s…
wowthecoder Jan 12, 2026
5cc6a85
test: combine f string in assertion
wowthecoder Jan 12, 2026
1601e2f
test: remove unused variable in random movement strategy
wowthecoder Jan 12, 2026
49323c5
test: add type annotation to test manager variable in random movement…
wowthecoder Jan 12, 2026
3350293
Merge branch 'main' into motion-planning-tests
wowthecoder Jan 12, 2026
7b46133
Update utama_core/tests/motion_planning/strategies/random_movement_st…
wowthecoder Jan 12, 2026
b7527d5
test: use snake case for variable name
wowthecoder Jan 12, 2026
4acd52a
test: change variables to snake case
wowthecoder Jan 12, 2026
955a724
doc: update random movement test description to say correct number of…
wowthecoder Jan 12, 2026
e65cb89
test: resolve copilot PR comments
wowthecoder Jan 12, 2026
5269993
final cleanup
energy-in-joles Jan 13, 2026
46e1c31
more minor stuff
energy-in-joles Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When doing a code review, keep your suggestions focused on real issues in readability, maintainability, performance, security, and adherence to best practices. Avoid suggesting trivial minor issues like unused imports, American vs British spelling or minor formatting tweaks unless they significantly impact the quality of the code.
16 changes: 16 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def pytest_addoption(parser):
default=False,
help="Don't display any graphics (runs faster)",
)
parser.addoption(
"--no-headless",
action="store_false",
dest="headless",
help="Run with graphics",
)


# These parameter names match up with the parameter names for
Expand Down Expand Up @@ -46,6 +52,16 @@ def pytest_generate_tests(metafunc):
metafunc.parametrize(param, cases[metafunc.config.getoption("level")])


# temporarily excludes the motion planning tests until the motion planning algorithms are working
def pytest_collection_modifyitems(config, items):
"""Exclude tests from motion_planning folder."""
remaining = []
for item in items:
if "motion_planning" not in str(item.fspath):
remaining.append(item)
items[:] = remaining


@pytest.fixture
def headless(pytestconfig):
return pytestconfig.getoption("--headless")
4 changes: 2 additions & 2 deletions utama_core/entities/game/ball.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def is_ball_in_goal(self, right_goal: bool) -> bool:
ball_pos = self.p
return (
ball_pos.x < -self.field.half_length
and (ball_pos.y < self.field.half_goal_width and ball_pos.y > -self.field.half_goal_width)
and (ball_pos.y < self.field.HALF_GOAL_WIDTH and ball_pos.y > -self.field.HALF_GOAL_WIDTH)
and not right_goal
or ball_pos.x > self.field.half_length
and (ball_pos.y < self.field.half_goal_width and ball_pos.y > -self.field.half_goal_width)
and (ball_pos.y < self.field.HALF_GOAL_WIDTH and ball_pos.y > -self.field.HALF_GOAL_WIDTH)
and right_goal
)
18 changes: 9 additions & 9 deletions utama_core/entities/game/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,39 +166,39 @@ def field_bounds(self) -> FieldBounds:
### Class Properties for standard field dimensions ###

@ClassProperty
def half_goal_width(cls) -> float:
def HALF_GOAL_WIDTH(cls) -> float:
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 'cls', as their first parameter.

Copilot uses AI. Check for mistakes.
return cls._HALF_GOAL_WIDTH

@ClassProperty
def left_goal_line(cls) -> np.ndarray:
def LEFT_GOAL_LINE(cls) -> np.ndarray:
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 'cls', as their first parameter.

Copilot uses AI. Check for mistakes.
return cls._LEFT_GOAL_LINE

@ClassProperty
def right_goal_line(cls) -> np.ndarray:
def RIGHT_GOAL_LINE(cls) -> np.ndarray:
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 'cls', as their first parameter.

Copilot uses AI. Check for mistakes.
return cls._RIGHT_GOAL_LINE

@ClassProperty
def left_defense_area(cls) -> np.ndarray:
def LEFT_DEFENSE_AREA(cls) -> np.ndarray:
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 'cls', as their first parameter.

Copilot uses AI. Check for mistakes.
return cls._LEFT_DEFENSE_AREA

@ClassProperty
def right_defense_area(cls) -> np.ndarray:
def RIGHT_DEFENSE_AREA(cls) -> np.ndarray:
return cls._RIGHT_DEFENSE_AREA

@ClassProperty
def full_field_half_length(cls) -> float:
def FULL_FIELD_HALF_LENGTH(cls) -> float:
return cls._FULL_FIELD_HALF_LENGTH

@ClassProperty
def full_field_half_width(cls) -> float:
def FULL_FIELD_HALF_WIDTH(cls) -> float:
return cls._FULL_FIELD_HALF_WIDTH

@ClassProperty
def full_field(cls) -> np.ndarray:
def FULL_FIELD(cls) -> np.ndarray:
return cls._FULL_FIELD

@ClassProperty
def full_field_bounds(cls) -> FieldBounds:
def FULL_FIELD_BOUNDS(cls) -> FieldBounds:
return FieldBounds(
top_left=(-cls._FULL_FIELD_HALF_LENGTH, cls._FULL_FIELD_HALF_WIDTH),
bottom_right=(cls._FULL_FIELD_HALF_LENGTH, -cls._FULL_FIELD_HALF_WIDTH),
Expand Down
4 changes: 2 additions & 2 deletions utama_core/entities/game/game_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ def is_ball_in_goal(self, right_goal: bool) -> bool:
ball_pos = self.ball.p
return (
ball_pos.x < -self.field.half_length
and (ball_pos.y < self.field.half_goal_width and ball_pos.y > -self.field.half_goal_width)
and (ball_pos.y < self.field.HALF_GOAL_WIDTH and ball_pos.y > -self.field.HALF_GOAL_WIDTH)
and not right_goal
or ball_pos.x > self.field.half_length
and (ball_pos.y < self.field.half_goal_width and ball_pos.y > -self.field.half_goal_width)
and (ball_pos.y < self.field.HALF_GOAL_WIDTH and ball_pos.y > -self.field.HALF_GOAL_WIDTH)
and right_goal
)
6 changes: 3 additions & 3 deletions utama_core/motion_planning/src/planning/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

class TempObstacleType(Enum):
NONE = []
FIELD = [Field.full_field]
DEFENCE_ZONES = [Field.left_defense_area, Field.right_defense_area]
ALL = [Field.left_defense_area, Field.right_defense_area, Field.full_field]
FIELD = [Field.FULL_FIELD]
DEFENCE_ZONES = [Field.LEFT_DEFENSE_AREA, Field.RIGHT_DEFENSE_AREA]
ALL = [Field.LEFT_DEFENSE_AREA, Field.RIGHT_DEFENSE_AREA, Field.FULL_FIELD]


class TimedSwitchController:
Expand Down
33 changes: 19 additions & 14 deletions utama_core/run/strategy_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ class StrategyRunner:
exp_enemy (int): Expected number of enemy robots.
field_bounds (FieldBounds): Configuration of the field. Defaults to standard field.
opp_strategy (AbstractStrategy, optional): Opponent strategy for pvp. Defaults to None for single player.
replay_writer_config (ReplayWriterConfig, optional): Configuration for the replay writer. If unset, replay is disabled.
control_scheme (str, optional): Name of the motion control scheme to use.
opp_control_scheme (str, optional): Name of the opponent motion control scheme to use. If not set, uses same as friendly.
replay_writer_config (ReplayWriterConfig, optional): Configuration for the replay writer. If unset, replay is disabled.
print_real_fps (bool, optional): Whether to print real FPS. Defaults to False.
profiler_name (Optional[str], optional): Enables and sets profiler name. Defaults to None which disables profiler.
"""
Expand All @@ -85,9 +86,10 @@ def __init__(
mode: str,
exp_friendly: int,
exp_enemy: int,
field_bounds: FieldBounds = Field.full_field_bounds,
field_bounds: FieldBounds = Field.FULL_FIELD_BOUNDS,
opp_strategy: Optional[AbstractStrategy] = None,
control_scheme: str = "pid",
control_scheme: str = "pid", # This is also the default control scheme used in the motion planning tests
opp_control_scheme: Optional[str] = None,
replay_writer_config: Optional[ReplayWriterConfig] = None,
print_real_fps: bool = False, # Turn this on for RSim
profiler_name: Optional[str] = None,
Expand All @@ -103,7 +105,11 @@ def __init__(
self.field_bounds = field_bounds
self.opp_strategy = opp_strategy

self.motion_controller = get_control_scheme(control_scheme)
self.my_motion_controller = get_control_scheme(control_scheme)
if opp_control_scheme is not None:
self.opp_motion_controller = get_control_scheme(opp_control_scheme)
else:
self.opp_motion_controller = self.my_motion_controller

self.my_strategy.setup_behaviour_tree(is_opp_strat=False)
if self.opp_strategy:
Expand Down Expand Up @@ -358,10 +364,10 @@ def _load_robot_controllers(self):
raise ValueError("mode is invalid. Must be 'rsim', 'grsim' or 'real'")

self.my_strategy.load_robot_controller(my_robot_controller)
self.my_strategy.load_motion_controller(self.motion_controller(self.mode, self.rsim_env))
self.my_strategy.load_motion_controller(self.my_motion_controller(self.mode, self.rsim_env))
if self.opp_strategy:
self.opp_strategy.load_robot_controller(opp_robot_controller)
self.opp_strategy.load_motion_controller(self.motion_controller(self.mode, self.rsim_env))
self.opp_strategy.load_motion_controller(self.opp_motion_controller(self.mode, self.rsim_env))

def _load_game(self):
"""
Expand Down Expand Up @@ -480,36 +486,35 @@ def close(self, stop_command_mult: int = 20):

def run_test(
self,
testManager: AbstractTestManager,
test_manager: AbstractTestManager,
episode_timeout: float = 10.0,
rsim_headless: bool = False,
) -> bool:
"""Run a test with the given test manager and episode timeout.

Args:
testManager (AbstractTestManager): The test manager to run the test.
test_manager (AbstractTestManager): The test manager to run the test.
episode_timeout (float): The timeout for each episode in seconds.
rsim_headless (bool): Whether to run RSim in headless mode. Defaults to False.
"""
signal.signal(signal.SIGINT, self._handle_sigint)

passed = True
n_episodes = testManager.get_n_episodes()
n_episodes = test_manager.get_n_episodes()
if not rsim_headless and self.rsim_env:
self.rsim_env.render_mode = "human"
if self.sim_controller is None:
warnings.warn("Running test in real, defaulting to 1 episode.")
n_episodes = 1

testManager.load_strategies(self.my_strategy, self.opp_strategy)
test_manager.load_strategies(self.my_strategy, self.opp_strategy)

for i in range(n_episodes):
testManager.update_episode_n(i)
test_manager.update_episode_n(i)

if self.sim_controller:
testManager.reset_field(self.sim_controller, self.my_game)
test_manager.reset_field(self.sim_controller, self.my_game)
time.sleep(0.1) # wait for the field to reset
# wait for the field to reset
self._reset_game()
episode_start_time = time.time()
# for simplicity, we assume rsim is running in real time. May need to change this
Expand Down Expand Up @@ -537,7 +542,7 @@ def run_test(
else:
raise e

status = testManager.eval_status(self.my_game)
status = test_manager.eval_status(self.my_game)

if status == TestingStatus.FAILURE:
passed = False
Expand Down
Loading
Loading