Skip to content

Commit 5e0160f

Browse files
authored
Automate handling referee game events (#3643)
* Implement auto approving goals * Re-write code to be a bit cleaner * Re-initialize world state after ball placement failure * Make sure teams get reset with proper amount of robots * Set only ball to ball placement point when failed * Remove unused import * Separate automate_referee method into helpers * Add comments * Implement auto start second half * Add call to automate_stage_change * Fix copying enum * Refactor method a bit * Implement starting new game * Uncomment call to automate_referee, move after updating latest world * Refactor a bit * Extract helper function for handling stage change * Add doc comment * Let both teams do ball placement before manual placement * Get rid of magic number
1 parent 54ce1be commit 5e0160f

1 file changed

Lines changed: 113 additions & 1 deletion

File tree

src/software/thunderscope/binary_context_managers/game_controller.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
import itertools
23

34
import queue
45
import random
@@ -9,6 +10,7 @@
910
from typing import Any
1011

1112
from proto.import_all_protos import *
13+
from proto.message_translation.tbots_protobuf import create_default_world_state
1214
from proto.ssl_gc_common_pb2 import Team as SslTeam
1315
from software.networking.ssl_proto_communication import *
1416
import software.python_bindings as tbots_cpp
@@ -21,9 +23,9 @@
2123
ThreadSafeCircularBuffer,
2224
)
2325
from software.thunderscope.util import is_current_platform_macos
26+
from software.py_constants import DIV_B_NUM_ROBOTS
2427

2528
logger = logging.getLogger(__name__)
26-
import itertools
2729

2830

2931
class Gamecontroller:
@@ -68,6 +70,7 @@ def __init__(
6870
self.latest_world = None
6971
self.blue_removed_robot_ids = queue.Queue()
7072
self.yellow_removed_robot_ids = queue.Queue()
73+
self.processed_event_ids = set()
7174

7275
def get_referee_port(self) -> int:
7376
"""Sometimes, the port that we are using changes depending on context.
@@ -178,6 +181,31 @@ def __update_robot_count(
178181
except queue.Empty:
179182
return
180183

184+
def __automate_referee(self, referee: Referee) -> None:
185+
"""Automate referee events by handling possible goals, ball placement failures,
186+
game stage changes, and starting new game.
187+
188+
:param referee: the referee protobuf message
189+
"""
190+
if referee.stage_time_left < 0:
191+
self.__handle_game_stage_change(referee.stage)
192+
193+
possible_goal_events = self.__find_unprocessed_events(
194+
referee, GameEvent.Type.POSSIBLE_GOAL
195+
)
196+
placement_failed_events = self.__find_unprocessed_events(
197+
referee, GameEvent.Type.PLACEMENT_FAILED
198+
)
199+
200+
if len(possible_goal_events) >= 1:
201+
self.__handle_possible_goal(possible_goal_events[0].possible_goal.by_team)
202+
self.processed_event_ids.add(possible_goal_events[0].id)
203+
204+
if len(placement_failed_events) >= 2:
205+
self.__handle_placement_failed()
206+
self.processed_event_ids.add(placement_failed_events[0].id)
207+
self.processed_event_ids.add(placement_failed_events[1].id)
208+
181209
def handle_referee(self, referee: Referee) -> None:
182210
"""Updates the world state based on the referee message
183211
:param referee: the referee protobuf message
@@ -192,6 +220,9 @@ def handle_referee(self, referee: Referee) -> None:
192220
block=False, return_cached=True
193221
)
194222

223+
# TODO (#3633): only automate referee events in record_stats mode
224+
self.__automate_referee(referee)
225+
195226
max_allowed_bots_yellow: int = referee.yellow.max_allowed_bots
196227
max_allowed_bots_blue: int = referee.blue.max_allowed_bots
197228
# Ignore if nothing needs to be updated
@@ -235,6 +266,87 @@ def handle_referee(self, referee: Referee) -> None:
235266
# Send out updated world state
236267
self.simulator_proto_unix_io.send_proto(WorldState, world_state)
237268

269+
def __handle_game_stage_change(self, game_stage: Referee.Stage) -> None:
270+
"""Handle game stage change by advancing to the next half once first half is over
271+
and starting a new game once the second half is over.
272+
273+
:param game_stage: The current game stage from the referee message
274+
"""
275+
if game_stage == Referee.Stage.NORMAL_FIRST_HALF:
276+
# skip to pre second half
277+
new_stage = Referee.Stage.NORMAL_SECOND_HALF_PRE
278+
else:
279+
# reset game
280+
new_stage = Referee.Stage.NORMAL_FIRST_HALF_PRE
281+
282+
# reset game state
283+
self.simulator_proto_unix_io.send_proto(
284+
WorldState, create_default_world_state(num_robots=DIV_B_NUM_ROBOTS)
285+
)
286+
287+
ci_input = CiInput(timestamp=int(time.time_ns()))
288+
api_input = Input()
289+
change = Change()
290+
change.change_stage_change.new_stage = new_stage
291+
api_input.change.CopyFrom(change)
292+
ci_input.api_inputs.append(api_input)
293+
294+
self.send_ci_input(ci_input)
295+
296+
self.send_gc_command(gc_command=Command.Type.STOP, team=SslTeam.UNKNOWN)
297+
298+
def __find_unprocessed_events(
299+
self, referee: Referee, event_type: GameEvent.Type
300+
) -> list[GameEvent]:
301+
"""Find unprocessed game events of the specified type.
302+
303+
:param referee: the referee protobuf message
304+
:param event_type: the type of game event to search for
305+
:return: list of unprocessed events of the given type
306+
"""
307+
return [
308+
event
309+
for event in referee.game_events
310+
if event.type == event_type and event.id not in self.processed_event_ids
311+
]
312+
313+
def __handle_possible_goal(self, scoring_team: SslTeam) -> None:
314+
"""Handle a possible goal event by approving the goal and starting ball placement.
315+
316+
:param scoring_team: the team that scored the goal
317+
"""
318+
ci_input = CiInput(timestamp=int(time.time_ns()))
319+
api_input = Input()
320+
change = Change()
321+
322+
# Send goal game event to resolve the possible goal
323+
game_event = GameEvent(
324+
type=GameEvent.Type.GOAL,
325+
origin=[
326+
"Majority"
327+
], # Required or else ssl-gamecontroller will convert game event to proposal
328+
goal=GameEvent.Goal(by_team=scoring_team),
329+
)
330+
331+
change.add_game_event_change.game_event.CopyFrom(game_event)
332+
api_input.change.CopyFrom(change)
333+
ci_input.api_inputs.append(api_input)
334+
self.send_ci_input(ci_input)
335+
336+
# reset the robot and ball positions
337+
self.simulator_proto_unix_io.send_proto(
338+
WorldState, create_default_world_state(num_robots=DIV_B_NUM_ROBOTS)
339+
)
340+
self.send_gc_command(Command.Type.STOP, SslTeam.UNKNOWN)
341+
342+
def __handle_placement_failed(self) -> None:
343+
"""Handle a placement failed event by moving the ball to the placement point."""
344+
world_state = WorldState()
345+
world_state.ball_state.global_position.CopyFrom(
346+
self.latest_world.game_state.ball_placement_point
347+
)
348+
self.simulator_proto_unix_io.send_proto(WorldState, world_state)
349+
238350
def is_valid_port(self, port):
239351
"""Determine whether or not a given port is valid
240352

0 commit comments

Comments
 (0)