Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
e74145f
Forward correct image topics
ayin21 Jun 19, 2026
5c21291
Make robots black
ayin21 Jun 19, 2026
6cd3000
Add example script for formation calc
ayin21 Jun 19, 2026
1ced7f6
basic creation of positioning_capsule
cleWu03 Jun 21, 2026
61bf6e7
connect positioning capsule with behaviour (example for defender)
cleWu03 Jun 21, 2026
4fa2aed
fix some type errors and run format (now works in simulation for a si…
cleWu03 Jun 22, 2026
eca2b97
test integration of different roles in simulation (only normal behavi…
cleWu03 Jun 22, 2026
775da12
add x any pos slider to rqt data sim
cleWu03 Jun 22, 2026
842adfd
Use events executor for rviz markers
Flova Jun 29, 2026
43c6e31
Isolate placement test script
Flova Jun 29, 2026
49ed7aa
Delete redundant file
Flova Jun 29, 2026
6d616d6
Add freekick mode
Flova Jun 29, 2026
251a18d
Apply PR feedback
Flova Jun 29, 2026
9002998
Add typing
Flova Jun 29, 2026
49d519a
Apply format
Flova Jun 29, 2026
eaaf36d
improved dsd based on comments in PR (not testet for building)
cleWu03 Jun 30, 2026
bfbf3c7
integrate positioning capsule changes in main dsd
cleWu03 Jun 30, 2026
3fc4e2c
rqt add ball pose publish
cleWu03 Jun 30, 2026
28dbf2e
add @ in dsd go tos
cleWu03 Jun 30, 2026
ae03c94
Merge branch 'main' into feature/better_assignment to add double touch
cleWu03 Jun 30, 2026
d058eeb
fix positioning ready
cleWu03 Jun 30, 2026
b884405
fix hysteresis call
cleWu03 Jun 30, 2026
656f27f
fix closest to ball decision
cleWu03 Jun 30, 2026
e2c3c4a
no double toch integration with Strategy passiv
cleWu03 Jul 1, 2026
ab88f08
change freekick param name and add function to set it
cleWu03 Jul 1, 2026
2ef42fe
fixes sim errors
cleWu03 Jul 1, 2026
b9bfea2
this as well
cleWu03 Jul 1, 2026
104cea8
remove wrong log
cleWu03 Jul 1, 2026
b6b222d
add own action to get passive
cleWu03 Jul 1, 2026
ee12f90
some more dsd fixes
cleWu03 Jul 2, 2026
c09ac88
Merge main to get the newest simulator stuff for testing
cleWu03 Jul 2, 2026
1b03218
check for alingnement with goalPose to stop moving on place
cleWu03 Jul 2, 2026
5190a13
fix for communicated actions
cleWu03 Jul 2, 2026
09a1641
quick save continue in formation position
cleWu03 Jul 3, 2026
d86cc96
add reached goal logic to go to formation
cleWu03 Jul 3, 2026
e10883c
Fix small regressions
Jul 3, 2026
fa5e0ce
rewrite action
ayin21 Jul 3, 2026
9ddf72d
Fix various issues with GoToFormationPosition
Jul 3, 2026
bedf8e9
Merge branch 'main' into feature/better_assignment
Jul 3, 2026
5551f44
Remove Init Animation
Jul 3, 2026
94408f2
update passiv action for multiple players (Needs to be tested!)
cleWu03 Jul 3, 2026
85c0aaa
Remove old goto targets
Flova Jul 3, 2026
93cd9c4
Remove hysteresis and add set play formation
Flova Jul 3, 2026
7c664c7
Adapt and fix the behavior
Flova Jul 3, 2026
5cf3bab
Fix set play in gui
Flova Jul 3, 2026
eeed157
Fix seperation during complex / crowded set play situations
Flova Jul 3, 2026
a3f9344
Merge branch 'feature/better_assignment' of github.com:bit-bots/bitbo…
Flova Jul 3, 2026
8b63646
Apply format
Flova Jul 3, 2026
9a3099b
Fix passive robots
Flova Jul 3, 2026
bcfdbb2
Adapt gui to model passive robots during assignment
Flova Jul 3, 2026
426c857
Fix typing for positoning capsule
Jul 3, 2026
a093312
pixi format
Jul 3, 2026
33a0ff2
Fix formatting
Flova Jul 4, 2026
0e885e3
Remove unreachable code
Flova Jul 4, 2026
5904a09
Rework set play situation
Flova Jul 4, 2026
1b38e33
Add method to retrieve robot's own role and clean up role assignment …
Flova Jul 4, 2026
a99dc2c
Merge branch 'main' into feature/better_assignment
Flova Jul 4, 2026
6d3d637
Merge branch 'main' into feature/better_assignment
Jul 4, 2026
6acf08a
Merge remote-tracking branch 'origin/main' into feature/better_assign…
Jul 4, 2026
d35ad73
Correct unit of parameter
Jul 4, 2026
888ea17
remove latch
cleWu03 Jul 4, 2026
1ece6ee
dont reevaluate Forget Ball
cleWu03 Jul 4, 2026
3e11e17
Add deadband filter for with player number prio for striker assignment
Flova Jul 4, 2026
ad9f591
Merge branch 'feature/better_assignment' of github.com:bit-bots/bitbo…
Flova Jul 4, 2026
6e49a6c
Make supporter optional
Flova Jul 4, 2026
6818fa5
Add variable defender gap
Flova Jul 4, 2026
aaed253
Merge branch 'main' of github.com:bit-bots/bitbots_main into feature/…
Flova Jul 4, 2026
463144b
Add better defensive positions
Flova Jul 4, 2026
53874db
Merge origin/main into feature/better_assignment
jaagut Jul 18, 2026
bd8cb9c
publish absolut_ball from slider values to team_data
cleWu03 Jul 30, 2026
4867dce
small fixes from copilot, use rl_kick specific go_to_ball action, co…
cleWu03 Jul 31, 2026
bdae2ba
code style fix
cleWu03 Jul 31, 2026
7d52bbe
use float explicitly for ci
cleWu03 Jul 31, 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
476 changes: 476 additions & 0 deletions formation_prototype.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from bitbots_blackboard.capsules.kick_capsule import KickCapsule
from bitbots_blackboard.capsules.misc_capsule import MiscCapsule
from bitbots_blackboard.capsules.pathfinding_capsule import PathfindingCapsule
from bitbots_blackboard.capsules.positioning_capsule import PositioningCapsule
from bitbots_blackboard.capsules.team_data_capsule import TeamDataCapsule
from bitbots_blackboard.capsules.world_model_capsule import WorldModelCapsule

Expand All @@ -34,6 +35,7 @@ def __init__(self, node: Node, tf_buffer: tf2.BufferInterface):
self.costmap = CostmapCapsule(self.node, self)
self.pathfinding = PathfindingCapsule(self.node, self)
self.team_data = TeamDataCapsule(self.node, self)
self.positioning = PositioningCapsule(self.node, self)

self.capsules = [
self.misc,
Expand All @@ -44,6 +46,7 @@ def __init__(self, node: Node, tf_buffer: tf2.BufferInterface):
self.costmap,
self.pathfinding,
self.team_data,
self.positioning,
]

def clear_cache(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ def received_gamestate(self) -> bool:
def get_team_id(self) -> int:
return self.team_id

def get_own_id(self) -> int:
return self.own_id

def gamestate_callback(self, gamestate_msg: GameState) -> None:
if self.gamestate.penalized and not gamestate_msg.penalized:
self.unpenalized_time = self._node.get_clock().now().nanoseconds / 1e9
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
from typing import Literal, Optional

import numpy as np
Expand Down Expand Up @@ -183,6 +184,26 @@ def get_active_teammate_poses(self, count_goalies: bool = False) -> list[Pose]:
poses.append(data.robot_position.pose)
return poses

def quaternion_to_yaw(self, q) -> float:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Specify the quaternion notation xyzw vs wxyz and maybe move/check but it's utils for these functions.

"""Extract yaw (theta) from a quaternion."""
return math.atan2(2.0 * (q.w * q.z + q.x * q.y), 1.0 - 2.0 * (q.y * q.y + q.z * q.z))

def get_robot_poses(self) -> dict[int, list[float]]:
"""Returns a mapping of jersey_number -> [x, y, theta] for all active robots."""
robot_poses = {}
data: TeamData
for data in self.team_data.values():
if self.is_valid(data):
pose = data.robot_position.pose
robot_poses[data.robot_id] = [
pose.position.x,
pose.position.y,
self.quaternion_to_yaw(pose.orientation),
]
# include own data
robot_poses[self._blackboard.gamestate.get_own_id()] = list(self._blackboard.world_model.get_current_position())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add a flag for that

return robot_poses

def get_number_of_active_field_players(self, count_goalie: bool = False) -> int:
def is_not_goalie(team_data: TeamData) -> bool:
return team_data.strategy.role != Strategy.ROLE_GOALIE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
from dynamic_stack_decider.abstract_action_element import AbstractActionElement
from tf2_geometry_msgs import PoseStamped

class GoToGoaliePosition(AbstractActionElement):
blackboard: BodyBlackboard

def __init__(self, blackboard, dsd, parameters):
super().__init__(blackboard, dsd, parameters)

def perform(self, reevaluate=False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now it treads go to goalie, defense, etc all the same. That might be confusing, because somebody calls go to defense position to go explicitly there and he ends up at the goalie position.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer an explicit go to placement position. We can keep the old supporter position for the forced two robot ball touch, but that works outside of this placement. The PR is still pending, but it essentially removes the robot from the assignment after the ball was touched and goes to an explicit position to wait. While the others play with essentially one robot less. This can be done in the new concept, but we need the explicit support position for this. Other than that all other go to defense, block etc can be removed. Go to role needs to stay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We also need to think about what to do if we don't see a ball. The position assignment should still work based on the lastest ball. I think the striker should perform our normal ball discovery behavior (turn around, go to opponent goal, go to middle line on the side). The rest of the robots should go to their role positions to not interfere with the striker during this procedure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the whole part with coding the positioning goals in the old GoTo decisions was only for testing :) I use GoToFormationPosition now in the most cases, we still need to discuss how to handle the supporter during no double touch, I would prefer a direct integration in the formation capsule


pose_msg = PoseStamped()
pose_msg.header.stamp = self.blackboard.node.get_clock().now().to_msg()
pose_msg.header.frame_id = self.blackboard.map_frame

optimal_positioning = self.blackboard.positioning.get_formation_assignment()
own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()]
pose = own_position["goal_pose"]
pose_msg.pose.position.x = pose[0]
pose_msg.pose.position.y = pose[1]
pose_msg.pose.orientation.w = pose[2]

self.blackboard.pathfinding.publish(pose_msg)

class GoToBlockPosition(AbstractActionElement):
blackboard: BodyBlackboard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,11 @@ def perform(self, reevaluate=False):
pose_msg.pose.orientation = Quaternion(x=x, y=y, z=z, w=w)
else:
# center point between ball and own goal
pose_msg.pose.position.x = (goal_position[0] + ball_position[0]) / 2
pose_msg.pose.position.y = ball_position[1] / 2 + self.y_offset
pose_msg.pose.orientation.w = 1.0
optimal_positioning = self.blackboard.positioning.get_formation_assignment()
own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()]
pose = own_position["goal_pose"]
pose_msg.pose.position.x = pose[0]
pose_msg.pose.position.y = pose[1]
pose_msg.pose.orientation.w = pose[2]

self.blackboard.pathfinding.publish(pose_msg)
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@ def perform(self, reevaluate=False):
pose_msg = PoseStamped()
pose_msg.header.stamp = self.blackboard.node.get_clock().now().to_msg()
pose_msg.header.frame_id = self.blackboard.map_frame
pose_msg.pose.position.x = float(goal_x)
pose_msg.pose.position.y = float(goal_y)
pose_msg.pose.orientation = quat_from_yaw(goal_yaw)

optimal_positioning = self.blackboard.positioning.get_formation_assignment()
own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()]
pose = own_position["goal_pose"]
pose_msg.pose.position.x = pose[0]
pose_msg.pose.position.y = pose[1]
pose_msg.pose.orientation.w = pose[2]

self.blackboard.pathfinding.publish(pose_msg)

if not self.blocking:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,42 @@ def perform(self, reevaluate=False):
self.publish_debug_data("time to ball", my_time_to_ball)
self.publish_debug_data("Rank to ball", rank)
if rank == 1:
return "YES"
return "NO" # only for testing remeber to REMOVE!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remove :P

return "NO"

def get_reevaluate(self):
return True


class RankToBallNoGoalie(AbstractDecisionElement):
class RankToBallWithGoalie(AbstractDecisionElement):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't call it rank to ball. Maybe something like assigned role.

blackboard: BodyBlackboard

def __init__(self, blackboard, dsd, parameters):
super().__init__(blackboard, dsd, parameters)

def perform(self, reevaluate=False):
my_time_to_ball = self.blackboard.team_data.get_own_time_to_ball()
rank = self.blackboard.team_data.team_rank_to_ball(my_time_to_ball, count_goalies=False, use_time_to_ball=True)
self.publish_debug_data("time to ball", my_time_to_ball)
self.publish_debug_data("Rank to ball", rank)
if rank == 1:
return "FIRST"
elif rank == 2:
return "SECOND"
elif rank == 3:
return "THIRD"
optimal_positioning = self.blackboard.positioning.get_formation_assignment()
own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()]
role = own_position["role"]
self.publish_debug_data("Role from positioning", role)
if role == "striker":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again we might want to use an enum here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would use 1 = striker, 2 = goalie, 3, 5...x = defender 4 = supporter wich is the logical order for adding the roles depending on the number of robots. must be synchronised with capsule functions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can use a string enum.

return "STRIKER"
elif role == "goalie":
return "GOALIE"
elif role == "defender_0":
return "DEFENDER_ZERO"
elif role == "defender_1":
return "DEFENDER1"
elif role == "defender_2":
return "DEFENDER2"
elif role == "defender_3":
return "DEFENDER3"
elif role == "supporter":
return "SUPPORTER"
else:
# emergency fall back if something goes wrong
self.blackboard.node.get_logger().warning("Rank to ball had some issues")
return "FIRST"
self.blackboard.node.get_logger().warning("Rank to ball had some issues. Role" + role)
return "STRIKER"

def get_reevaluate(self):
return True
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,6 @@ $GoalieHandlingBall
YES --> @ChangeAction + action:positioning, @AvoidBallActive, @LookAtFieldFeatures, @GoToDefensePosition //quick fix for playing with two robots during GO
NO --> #KickWithAvoidance

#DefensePositioning
$GoalieActive
YES --> @LookAtFieldFeatures, @ChangeAction + action:positioning, @GoToDefensePosition
NO --> @LookAtFieldFeatures, @ChangeAction + action:positioning, @GoToBlockPosition

#SupporterRole
$PassStarted
YES --> $BallSeen
Expand Down Expand Up @@ -94,10 +89,12 @@ $DoOnce
NO --> $SecondaryStateTeamDecider
OUR --> #SearchBall
ELSE --> @AvoidBallActive, @LookAtFieldFeatures, @WalkInPlace + duration:2, @GoToRelativePosition + x:1 + y:0 + t:0, @Stand

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can do the search ball behavior in both cases here as our robots are way faster now and we had the issue in the past that we didn't leave the keep out area in time

YES --> $RankToBallNoGoalie
FIRST --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition + mode:freekick_first
SECOND --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition + mode:freekick_second
THIRD --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition
YES --> $RankToBallWithGoalie

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this selection is necessary anymore. Everyone should just go to their placement position and stand there if it was reached.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i will take a closer look there once we have discussed how to handle set play situations with the new positioning capsule, got a view ideas there

STRIKER --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition + mode:freekick_first

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't go to defense position. Go to placement position or whatever we want to call it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also we have an issue here. We need an explicit placement for situations where the other team has e.g. a free kick. In this situation the striker role should be deactivated, the striker should be reassigned to a defensive role and we should enforce a keep out area around the ball. This should be pretty easily implemtable in the placement function if we add a flag for this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would use the positioning capsule with specialised formation functions for each set play situation, we should also integrate the "no double touch allowed" situation directly to the formation logic

DEFENDER0 --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition + mode:freekick_second
SUPPORTER --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition
GOALIE --> @GoToGoaliePosition

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this different?

ELSE --> @ChangeAction + action:positioning, @LookAtFieldFeatures, @AvoidBallActive, @GoToDefensePosition + mode:freekick_second

#Init
@Stand + duration:0.1 + r:false, @ChangeAction + action:waiting, @LookForward, @Stand
Expand All @@ -110,16 +107,12 @@ $BallSeen
YES --> $KickOffTimeUp
NO_NORMAL --> #StandAndLook
NO_FREEKICK --> #Placing
YES --> $ConfigRole
GOALIE --> #GoalieBehavior
ELSE --> $CountActiveRobotsWithoutGoalie
ONE --> $RankToBallNoGoalie
FIRST --> #StrikerRole
SECOND --> #DefensePositioning
ELSE --> $RankToBallNoGoalie
FIRST --> #StrikerRole
SECOND --> #SupporterRole
THIRD --> #DefensePositioning
YES --> $RankToBallWithGoalie

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the only really useful destinction here is striker, supporter, defender, goalie (maybe). We can probably do the same placement strategy for the goalie and and defenders and we are not limited in the number of defenders that way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The striker needs special handling because it wants to kick if it reached its goal position. The supporter wants to track the ball if the striker is kicking and needs special handling for that and the others just go to their positions to stand in the way.

STRIKER --> #StrikerRole

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The striker role needs to be reworked to position itself using the new striker placement.

GOALIE --> @GoToGoaliePosition
DEFENDER_ZERO --> @LookAtFieldFeatures, @ChangeAction + action:positioning, @GoToDefensePosition
SUPPORTER --> #SupporterRole
ELSE --> @LookAtFieldFeatures, @ChangeAction + action:positioning, @GoToDefensePosition //defender 1 bis 4 integrieren?

#PlayingBehavior
$SecondaryStateDecider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def generate_domain_bridge_config(robot_domain: int, output_dir: Path) -> Path:
sensor_topics = [
("joint_states", "sensor_msgs/msg/JointState"),
("imu/data", "sensor_msgs/msg/Imu"),
("camera/image_proc", "sensor_msgs/msg/Image"),
("camera/camera_info", "sensor_msgs/msg/CameraInfo"),
("zed/zed_node/rgb/image_rect_color", "sensor_msgs/msg/Image"),
("zed/zed_node/rgb/camera_info", "sensor_msgs/msg/CameraInfo"),
]

for topic_suffix, msg_type in sensor_topics:
Expand Down
Loading
Loading