Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
ec7010a
Added NeverExcessivelyDribbles functionality into OffensePlayTest
adrianchan787 Nov 1, 2025
b690a45
intermediate commit
adrianchan787 Nov 30, 2025
c339388
intermediate change
adrianchan787 Nov 30, 2025
a4f6bc0
Switched to world proto subscription measurement
adrianchan787 Nov 30, 2025
544bc55
Added excessive dribbling functionality and testing
adrianchan787 Nov 30, 2025
d928b5d
Reverted verbosity change which was for print testing previously
adrianchan787 Nov 30, 2025
a3929cf
Merge branch 'master' into adrian/offense_play_test_dribble
adrianchan787 Nov 30, 2025
32cc42d
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 30, 2025
5b0b98c
Added error margin of 0.05 to max dribbling distance
adrianchan787 Nov 30, 2025
2fb03f8
Merge branch 'adrian/offense_play_test_dribble' of https://github.com…
adrianchan787 Nov 30, 2025
dff8853
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 30, 2025
8336df9
Made testing file one function
adrianchan787 Jan 9, 2026
8d1354f
Increased max dribble margin to 0.06 to reduce flakiness
adrianchan787 Jan 9, 2026
75141c3
Fixed import statements for test file
adrianchan787 Jan 9, 2026
2d235e3
Changed get validation to reflect correct start point
adrianchan787 Jan 9, 2026
ed1102e
Merge conflict fix
adrianchan787 Jan 9, 2026
e3afcc5
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 9, 2026
7edc073
Added constants to the excessive dribbling
adrianchan787 Jan 10, 2026
87eb7b9
Removed allow excessive dribbling for the tests
adrianchan787 Jan 10, 2026
20f5cfd
Merged master
adrianchan787 Jan 10, 2026
e9d2ff6
Fixed merge conflict
adrianchan787 Jan 10, 2026
0e8e809
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 10, 2026
fd5ceec
Merge branch 'master' into adrian/offense_play_test_dribble
adrianchan787 Jan 15, 2026
da98427
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 15, 2026
0916bcb
Added back import
adrianchan787 Jan 31, 2026
82532bd
Added back import
adrianchan787 Jan 31, 2026
7e46bdf
Merge conflicts fixed
adrianchan787 Jan 31, 2026
6d9a363
Added overrides
adrianchan787 Jan 31, 2026
9812bc1
pulled from master
adrianchan787 Feb 7, 2026
871d210
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Feb 7, 2026
9022e22
test
adrianchan787 Feb 7, 2026
8a2bf29
fixed syntax
adrianchan787 Feb 7, 2026
22ec418
Merge conflicts fixed
adrianchan787 Feb 7, 2026
57e14e9
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Feb 7, 2026
1d98837
i think it's finally good now?
adrianchan787 Feb 7, 2026
14ef1b0
test push to see if it fixes the 59 changed files...
adrianchan787 Feb 7, 2026
5e15f1a
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Feb 7, 2026
c0632cd
Revert "Added overrides"
adrianchan787 Feb 7, 2026
d36bd78
revert
adrianchan787 Feb 7, 2026
3edb21c
revert
adrianchan787 Feb 7, 2026
6cef848
revert merge
adrianchan787 Feb 7, 2026
41344a3
revert
adrianchan787 Feb 7, 2026
417a094
merge
adrianchan787 Feb 7, 2026
6e3cb23
Revert
adrianchan787 Feb 7, 2026
212b4db
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Feb 7, 2026
7a55bf4
fixed some of the review stuff
adrianchan787 Feb 14, 2026
e77077e
Revert precommit bot
adrianchan787 Feb 14, 2026
53ed48b
Fixed test formatting
adrianchan787 Feb 14, 2026
8b1620a
Reverted excessive dribbling implementation
adrianchan787 Feb 20, 2026
d802f94
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Feb 20, 2026
25abf85
Moved constants to within file
adrianchan787 Feb 28, 2026
6016eb2
Fixed merge
adrianchan787 Feb 28, 2026
5f6b5f9
Fixed get validation status and get validation geometry, still needs …
adrianchan787 Mar 1, 2026
183708e
Ran formatting script
adrianchan787 Mar 1, 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
5 changes: 4 additions & 1 deletion src/software/ai/hl/stp/play/offense/offense_play_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import software.python_bindings as tbots_cpp
from proto.play_pb2 import Play, PlayName

from software.simulated_tests.excessive_dribbling import NeverExcessivelyDribbles
from software.simulated_tests.friendly_team_scored import *
from software.simulated_tests.ball_enters_region import *
from software.simulated_tests.friendly_has_ball_possession import *
Expand Down Expand Up @@ -76,7 +78,8 @@ def setup(start_point):

# Always Validation
inv_always_validation_sequence_set = [
[BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])]
[BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])],
[NeverExcessivelyDribbles()],
]

ag_always_validation_sequence_set = [[FriendlyAlwaysHasBallPossession()]]
Expand Down
19 changes: 19 additions & 0 deletions src/software/ai/hl/stp/tactic/dribble/BUILD
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
load("@simulated_tests_deps//:requirements.bzl", "requirement")

package(default_visibility = ["//visibility:public"])

cc_library(
Expand Down Expand Up @@ -60,3 +62,20 @@ cc_test(
"//software/world",
],
)

py_test(
name = "excessive_dribble_test",
srcs = [
"excessive_dribble_test.py",
],
# TODO (#2619) Remove tag to run in parallel
tags = [
"exclusive",
],
deps = [
"//software:conftest",
"//software/simulated_tests:speed_threshold_helpers",
"//software/simulated_tests:validation",
requirement("pytest"),
],
)
184 changes: 184 additions & 0 deletions src/software/ai/hl/stp/tactic/dribble/excessive_dribble_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import pytest

import software.python_bindings as tbots_cpp
from software.simulated_tests.excessive_dribbling import (
NeverExcessivelyDribbles,
EventuallyStartsExcessivelyDribbling,
)
from proto.message_translation.tbots_protobuf import (
WorldState,
AssignedTacticPlayControlParams,
DribbleTactic,
)
from software.simulated_tests.simulated_test_fixture import (
pytest_main,
)
from proto.message_translation.tbots_protobuf import create_world_state


@pytest.mark.parametrize(
"initial_location,dribble_destination,final_dribble_orientation, should_excessively_dribble, blue_robot_location",
[
# The following tests check that DribbleTactic does not false trigger for distances within regulations
# Dribble Destination for the ball < 1.0 from its starting position
(
tbots_cpp.Point(0.5, 0),
tbots_cpp.Point(1.02, 0),
tbots_cpp.Angle(),
False,
tbots_cpp.Point(0, 1),
),
# Dribble Testing diagonally
(
tbots_cpp.Point(0.25, 0.25),
tbots_cpp.Point(0.80, 0.50),
tbots_cpp.Angle.fromRadians(50),
False,
tbots_cpp.Point(0, 1),
),
# Boundary Testing, because of the autoref implementation (initial of position Bot to final of Ball),
# a conservative max dribble distance (0.95 m) is used
# Test vertical dribbling
(
tbots_cpp.Point(0.01, 0),
tbots_cpp.Point(0.96, 0),
tbots_cpp.Angle(),
False,
tbots_cpp.Point(0, 1),
),
# Test horizontal dribbling
(
tbots_cpp.Point(1, 1.5),
tbots_cpp.Point(1.95, 1.5),
tbots_cpp.Angle(),
False,
tbots_cpp.Point(0, 1),
),
# Test bot and ball in same position
(
tbots_cpp.Point(0, 1),
tbots_cpp.Point(0.95, 1),
tbots_cpp.Angle(),
False,
tbots_cpp.Point(0, 1),
),
# The following tests check that DribbleTactic correctly triggers for distances outside regulation
# Dribble Destination for the ball > 1.0 from its starting position
(
tbots_cpp.Point(0, 2),
tbots_cpp.Point(0, 0.5),
tbots_cpp.Angle(),
True,
tbots_cpp.Point(0, 0),
),
# Dribble Testing diagonally
(
tbots_cpp.Point(0.1, 1.1),
tbots_cpp.Point(1.1, 0.1),
tbots_cpp.Angle.fromRadians(50),
True,
tbots_cpp.Point(0, 0),
),
# Boundary Testing, due to the conservative implementation a dribble distance of 1 m should fail
# Test Vertical Dribbling
(
tbots_cpp.Point(0, 1),
tbots_cpp.Point(0, 2),
tbots_cpp.Angle(),
True,
tbots_cpp.Point(0, 0),
),
# Test Horizontal Dribbling
(
tbots_cpp.Point(1, 2),
tbots_cpp.Point(0, 2),
tbots_cpp.Angle(),
True,
tbots_cpp.Point(0, 0),
),
# Test Diagonal Dribbling
(
tbots_cpp.Point(0, 1),
tbots_cpp.Point(0.6, 1.8),
tbots_cpp.Angle(),
True,
tbots_cpp.Point(0, 0),
),
# Test robot and ball at same position (affects dribbling orientation and therefore perceived dribble distance)
(
tbots_cpp.Point(0, 0),
tbots_cpp.Point(0, 1),
tbots_cpp.Angle(),
True,
tbots_cpp.Point(0, 0),
),
(
tbots_cpp.Point(0.0, 0.01),
tbots_cpp.Point(0.81, 0.61),
tbots_cpp.Angle(),
True,
tbots_cpp.Point(0, 0),
),
],
)
def test_excessive_dribbling(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not quite sure what the reasoning was for this test, since the issue this PR is marked for is regarding offense play. Reading through the work here, it appears to be more akin to a DribbleTactic test. Excessive dribbling is a characteristic of Gameplay more than a particular state, so I think it is a bit odd to be testing it explicitly since it is really just testing the implementation of DribbleTactic itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm ig that makes sense. I was thinking (please correct me if I'm wrong or really misguided) but the main dribbling tactic used in offense play is DribbleTactic, and if that's the case if it works in DribbleTactic then it should work everywhere else, including in offense play. As it is easier to test (especially boundary test) DribbleTactic directly rather than test offense play, I was thinking it might be the better option.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if I fully understand Andrew's point, but imo the purpose of this test is clear and makes sense

initial_location,
dribble_destination,
final_dribble_orientation,
should_excessively_dribble,
simulated_test_runner,
blue_robot_location,
):
if should_excessively_dribble:
# Always and Eventually validation sets for excessive dribbling
always_validation_sequence_set = [[]]
eventually_validation_sequence_set = [[EventuallyStartsExcessivelyDribbling()]]
else:
# Always and Eventually validation sets for not excessive dribbling
always_validation_sequence_set = [[NeverExcessivelyDribbles()]]
eventually_validation_sequence_set = [[]]

blue_robot_locations = [blue_robot_location]

simulated_test_runner.simulator_proto_unix_io.send_proto(
WorldState,
create_world_state(
[],
blue_robot_locations=blue_robot_locations,
ball_location=initial_location,
ball_velocity=tbots_cpp.Vector(0, 0),
),
)

# Setup Tactic
params = AssignedTacticPlayControlParams()
params.assigned_tactics[0].dribble.CopyFrom(
DribbleTactic(
dribble_destination=tbots_cpp.createPointProto(dribble_destination),
final_dribble_orientation=tbots_cpp.createAngleProto(
final_dribble_orientation
),
allow_excessive_dribbling=True,
)
)

simulated_test_runner.blue_full_system_proto_unix_io.send_proto(
AssignedTacticPlayControlParams, params
)

# Setup no tactics on the enemy side
params = AssignedTacticPlayControlParams()
simulated_test_runner.yellow_full_system_proto_unix_io.send_proto(
AssignedTacticPlayControlParams, params
)

simulated_test_runner.run_test(
inv_eventually_validation_sequence_set=eventually_validation_sequence_set,
inv_always_validation_sequence_set=always_validation_sequence_set,
ag_eventually_validation_sequence_set=eventually_validation_sequence_set,
ag_always_validation_sequence_set=always_validation_sequence_set,
)


if __name__ == "__main__":
pytest_main(__file__)
51 changes: 35 additions & 16 deletions src/software/simulated_tests/excessive_dribbling.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import software.python_bindings as tbots_cpp
from proto.import_all_protos import *
from proto.import_all_protos import ValidationStatus, ValidationGeometry

from software.simulated_tests.validation import (
Validation,
Expand All @@ -13,42 +13,61 @@ class ExcessivelyDribbling(Validation):
"""Checks if any friendly robot is excessively dribbling the ball, i.e. for over 1m."""

def __init__(self):
self.continous_dribbling_start_point = None
self.continuous_dribbling_start_point = None
self.dribbler_tolerance = 0.05
self.max_dribbling_displacement = 1.00
self.dribbling_error_margin = 0.05
Comment on lines +17 to +19
Copy link
Member

Choose a reason for hiding this comment

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

I would normally make these class constants rather than instance attributes accessed via self, e.g. ExcessivelyDribbling.MAX_DRIBBLING_DISPLACEMENT. Another option (if you want to allow greater flexibility) would be to keep the instance attributes and set them based on constructor params with default arguments. You don't have to change anything for this PR, but just something to keep in mind for the future.


@override
def get_validation_status(self, world) -> ValidationStatus:
"""Checks if any friendly robot is excessively dribbling the ball, i.e. for over 1m.
"""Checks if any friendly robot is excessively dribbling the ball past the max dribble displacement
minus the dribbling error margin

:param world: The world msg to validate
:return: FAILING when the robot is excessively dribbling
PASSING when the robot is not excessively dribbling
"""
ball_position = tbots_cpp.createPoint(world.ball.current_state.global_position)
for robot in world.friendly_team.team_robots:
if not tbots_cpp.Robot(robot).isNearDribbler(ball_position, 0.01):
# if ball is not near dribbler then de-activate this validation
self.continous_dribbling_start_point = None
elif (
ball_position - (self.continous_dribbling_start_point or ball_position)
).length() > 1.0:
return ValidationStatus.FAILING
elif self.continous_dribbling_start_point is None:
# ball is in dribbler, but previously wasn't in dribbler, so set continuous dribbling start point
self.continous_dribbling_start_point = ball_position
if tbots_cpp.Robot(robot).isNearDribbler(
ball_position, self.dribbler_tolerance
):
if self.continuous_dribbling_start_point is None:
# Set the dribbling validation start point to the current ball position
self.continuous_dribbling_start_point = ball_position
elif (
ball_position - self.continuous_dribbling_start_point
).length() > (
self.max_dribbling_displacement - self.dribbling_error_margin
):
return ValidationStatus.FAILING
return ValidationStatus.PASSING

# Reset the dribbling validation start point if no robots are near the ball
self.continuous_dribbling_start_point = None
return ValidationStatus.PASSING

@override
def get_validation_geometry(self, world) -> ValidationGeometry:
"""(override) Shows the max allowed dribbling circle"""
return create_validation_geometry(
[tbots_cpp.Circle(self.continous_dribbling_start_point, 1.0)]
if self.continous_dribbling_start_point is not None
[
tbots_cpp.Circle(
self.continuous_dribbling_start_point,
self.max_dribbling_displacement,
),
tbots_cpp.Circle(
self.continuous_dribbling_start_point,
self.max_dribbling_displacement - self.dribbling_error_margin,
),
]
if self.continuous_dribbling_start_point is not None
else []
)

@override
def __repr__(self):
return "Check that the dribbling robot has not dribbled for more than 1m"
return f"Check that the dribbling robot has not dribbled for more than {self.max_dribbling_displacement} m minus error margin ({self.dribbling_error_margin} m)"


(
Expand Down