diff --git a/RLBotPack/RLDojo/Dojo/.python-version b/RLBotPack/RLDojo/Dojo/.python-version new file mode 100644 index 00000000..8d7f852b --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/.python-version @@ -0,0 +1 @@ +3.10.4 diff --git a/RLBotPack/RLDojo/Dojo/constants.py b/RLBotPack/RLDojo/Dojo/constants.py new file mode 100644 index 00000000..fbe58b96 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/constants.py @@ -0,0 +1,40 @@ +# Game field constants +SIDE_WALL = 4096 +BACK_WALL = -5120 # From perspective of default scenario - blue team defending +GOAL_WIDTH = 893 +GOAL_DEPTH = 880 +CORNER_OFFSET = 1152 + +# Game timing constants +DEFAULT_TIMEOUT = 10.0 +FREE_GOAL_TIMEOUT = 7.0 +DEFAULT_PAUSE_TIME = 1.0 + +# UI constants +MENU_START_X = 20 +MENU_START_Y = 400 +MENU_WIDTH = 500 +MENU_HEIGHT = 500 + +SCORE_BOX_START_X = 50 +SCORE_BOX_START_Y = 100 +SCORE_BOX_WIDTH = 240 +SCORE_BOX_HEIGHT = 110 + +CUSTOM_MODE_MENU_START_X = 20 +CUSTOM_MODE_MENU_START_Y = 600 +CUSTOM_MODE_MENU_WIDTH = 400 +CUSTOM_MODE_MENU_HEIGHT = 200 + +CONTROLS_MENU_WIDTH = 350 +CONTROLS_MENU_HEIGHT = 200 + +# Physics constants +MIN_VELOCITY = 1000 +MAX_VELOCITY = 2000 +BALL_GROUND_THRESHOLD = 100 +GOAL_DETECTION_THRESHOLD = 40 + +# Default trial counts +DEFAULT_TRIAL_OPTIONS = [100, 50, 25, 10] +DEFAULT_NUM_TRIALS = 100 diff --git a/RLBotPack/RLDojo/Dojo/custom_playlist.py b/RLBotPack/RLDojo/Dojo/custom_playlist.py new file mode 100644 index 00000000..790f1b0e --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/custom_playlist.py @@ -0,0 +1,288 @@ +""" +Custom playlist creation and management system for Dojo. + +This module provides functionality for users to create, edit, and save +custom playlists that persist between sessions. +""" + +import json +import os +from typing import List, Dict, Any, Optional, Tuple +from playlist import Playlist, ScenarioConfig, PlaylistSettings, PlayerRole +from scenario import OffensiveMode, DefensiveMode +from menu import MenuRenderer, UIElement +from pydantic import BaseModel, Field, ValidationError +from custom_scenario import CustomScenario, get_custom_scenarios + +class CustomPlaylistManager: + def __init__(self, renderer, main_menu_renderer): + self.renderer = renderer + self.main_menu_renderer = main_menu_renderer + # Current playlist being created/edited + self.current_playlist_name = "" + self.current_scenarios = [] + self.current_custom_scenarios = [] + self.current_boost_range = [12, 100] # Default boost range + self.current_timeout = 7.0 + self.current_rule_zero = False + + def load_custom_playlists(self): + """Load custom playlists from disk and return a list of all custom playlists""" + custom_playlists = {} + for file in os.listdir(_get_custom_playlists_path()): + if file.endswith(".json"): + with open(os.path.join(_get_custom_playlists_path(), file), "r") as f: + custom_playlists[file.replace(".json", "")] = Playlist.model_validate_json(f.read()) + return custom_playlists + + def create_playlist_creation_menu(self): + """Create the main playlist creation menu""" + menu = MenuRenderer(self.renderer, columns=1, render_function=self._render_playlist_details) + menu.add_element(UIElement("Create Custom Playlist", header=True)) + menu.add_element(UIElement("Set Playlist Name", submenu=self._create_name_input_menu(), display_value_function=self.get_current_playlist_name)) + menu.add_element(UIElement("Add PresetScenarios", submenu=self._create_scenario_selection_menu())) + menu.add_element(UIElement("Add Custom Scenario", submenu=self._create_custom_scenario_selection_menu())) + menu.add_element(UIElement("Set Boost Range", submenu=self._create_boost_range_menu(), display_value_function=self.get_current_playlist_boost_range)) + menu.add_element(UIElement("Set Timeout", submenu=self._create_timeout_menu(), display_value_function=self.get_current_playlist_timeout)) + menu.add_element(UIElement("Toggle Rule Zero", function=self._toggle_rule_zero, display_value_function=self.get_current_playlist_rule_zero)) + menu.add_element(UIElement("Save Playlist", function=self._save_current_playlist)) + menu.add_element(UIElement("Cancel", function=self._cancel_playlist_creation)) + return menu + + ### Element value retrieval functions + def get_current_playlist_name(self): + return self.current_playlist_name + + + def get_current_playlist_boost_range(self): + return self.current_boost_range + + def get_current_playlist_timeout(self): + return self.current_timeout + + def get_current_playlist_rule_zero(self): + return self.current_rule_zero + + def _render_playlist_details(self): + # Create a playlist out of current settings + playlist = Playlist( + name=self.current_playlist_name, + description=f"Custom playlist with {len(self.current_scenarios)} scenarios", + scenarios=self.current_scenarios.copy(), + custom_scenarios=self.current_custom_scenarios.copy(), + settings=PlaylistSettings(timeout=self.current_timeout, shuffle=True, boost_range=self.current_boost_range, rule_zero=self.current_rule_zero) + ) + playlist.render_details(self.renderer) + + def _create_name_input_menu(self): + """Create menu for setting playlist name""" + menu = MenuRenderer(self.renderer, columns=1, text_input=True, text_input_callback=self._set_playlist_name) + return menu + + def _create_scenario_selection_menu(self): + """Create menu for selecting scenarios to add""" + menu = MenuRenderer(self.renderer, columns=3, show_selections=True, render_function=self._render_playlist_details) + + # Column 1: Offensive modes + menu.add_element(UIElement("Offensive Mode", header=True), column=0) + for mode in OffensiveMode: + menu.add_element(UIElement( + mode.name.replace('_', ' ').title(), + function=self._set_temp_offensive_mode, + function_args=mode, + chooseable=True, + ), column=0) + + # Column 2: Defensive modes + menu.add_element(UIElement("Defensive Mode", header=True), column=1) + for mode in DefensiveMode: + menu.add_element(UIElement( + mode.name.replace('_', ' ').title(), + function=self._set_temp_defensive_mode, + function_args=mode, + chooseable=True, + ), column=1) + + # Column 3: Player role and actions + menu.add_element(UIElement("Player Role", header=True), column=2) + menu.add_element(UIElement("Offense", function=self._set_temp_player_role, function_args=PlayerRole.OFFENSE, chooseable=True), column=2) + menu.add_element(UIElement("Defense", function=self._set_temp_player_role, function_args=PlayerRole.DEFENSE, chooseable=True), column=2) + menu.add_element(UIElement("", header=True), column=2) # Spacer + menu.add_element(UIElement("Add Scenario", function=self._add_current_scenario), column=2) + + + return menu + + def _create_custom_scenario_selection_menu(self): + """Create menu for selecting custom scenarios to add""" + menu = MenuRenderer(self.renderer, columns=2, show_selections=True, render_function=self._render_playlist_details) + custom_scenarios = get_custom_scenarios() + + # Column 1: Custom scenarios + for scenario_name in custom_scenarios: + menu.add_element(UIElement(scenario_name, function=self._add_custom_scenario, function_args=scenario_name)) + + # Column 2: Player role and actions + return menu + + def _create_boost_range_menu(self): + """Create menu for setting boost range""" + menu = MenuRenderer(self.renderer, columns=2, render_function=self._render_playlist_details) + + # Column 1: Min boost + menu.add_element(UIElement("Min Boost", header=True), column=0) + for boost in [0, 12, 20, 30, 40, 50, 60, 70]: + menu.add_element(UIElement( + str(boost), + function=self._set_min_boost, + function_args=boost + ), column=0) + + # Column 2: Max boost + menu.add_element(UIElement("Max Boost", header=True), column=1) + for boost in [50, 60, 70, 80, 90, 100]: + menu.add_element(UIElement( + str(boost), + function=self._set_max_boost, + function_args=boost + ), column=1) + + return menu + + def _create_timeout_menu(self): + """Create menu for setting timeout""" + menu = MenuRenderer(self.renderer, columns=1, render_function=self._render_playlist_details) + menu.add_element(UIElement("Set Timeout (seconds)", header=True)) + + for timeout in [5.0, 7.0, 10.0, 15.0, 20.0, 30.0]: + menu.add_element(UIElement( + f"{timeout}s", + function=self._set_timeout, + function_args=timeout + )) + + return menu + + # Temporary variables for scenario creation + temp_offensive_mode = None + temp_defensive_mode = None + temp_player_role = None + + def _set_playlist_name(self, name): + self.current_playlist_name = name + print(f"Playlist name set to: {name}") + + def _set_temp_offensive_mode(self, mode): + self.temp_offensive_mode = mode + print(f"Selected offensive mode: {mode.name}") + + def _set_temp_defensive_mode(self, mode): + self.temp_defensive_mode = mode + print(f"Selected defensive mode: {mode.name}") + + def _set_temp_player_role(self, role): + self.temp_player_role = role + print(f"Selected player role: {role.name}") + + def _add_current_scenario(self): + """Add the currently selected scenario configuration""" + if self.temp_offensive_mode and self.temp_defensive_mode and self.temp_player_role: + scenario = ScenarioConfig( + offensive_mode=self.temp_offensive_mode, + defensive_mode=self.temp_defensive_mode, + player_role=self.temp_player_role + ) + self.current_scenarios.append(scenario) + print(f"Added scenario: {self.temp_offensive_mode.name} vs {self.temp_defensive_mode.name} ({self.temp_player_role.name})") + + # Reset temp variables + self.temp_offensive_mode = None + self.temp_defensive_mode = None + self.temp_player_role = None + + # Exit the submenu + if self.main_menu_renderer: + self.main_menu_renderer.handle_back_key() + else: + print("Please select offensive mode, defensive mode, and player role first") + + + + def _set_min_boost(self, boost): + """Set minimum boost value""" + self.current_boost_range = (boost, max(boost + 10, self.current_boost_range[1])) + print(f"Set boost range: {self.current_boost_range}") + + def _set_max_boost(self, boost): + """Set maximum boost value""" + self.current_boost_range = (min(boost - 10, self.current_boost_range[0]), boost) + print(f"Set boost range: {self.current_boost_range}") + + def _set_timeout(self, timeout): + """Set scenario timeout""" + self.current_timeout = timeout + print(f"Set timeout: {timeout}s") + + def _toggle_rule_zero(self): + """Toggle rule zero setting""" + self.current_rule_zero = not self.current_rule_zero + print(f"Rule zero: {'ON' if self.current_rule_zero else 'OFF'}") + + + def _save_current_playlist(self): + """Save the currently configured playlist to file, and register it in the playlist registry""" + if not self.current_playlist_name: + print("Please set a playlist name first") + return + + # Register the playlist in the playlist registry + # and save it to file + playlist = Playlist( + name=self.current_playlist_name, + description=f"Custom playlist with {len(self.current_scenarios)} scenarios", + scenarios=self.current_scenarios.copy(), + custom_scenarios=self.current_custom_scenarios.copy(), + settings=PlaylistSettings(timeout=self.current_timeout, shuffle=True, boost_range=self.current_boost_range, rule_zero=self.current_rule_zero) + ) + + with open(os.path.join(_get_custom_playlists_path(), f"{self.current_playlist_name}.json"), "w") as f: + f.write(playlist.model_dump_json()) + + def _cancel_playlist_creation(self): + """Cancel playlist creation and reset""" + self._reset_current_playlist() + print("Playlist creation cancelled") + + def _reset_current_playlist(self): + """Reset current playlist creation data""" + self.current_playlist_name = "" + self.current_scenarios = [] + self.current_custom_scenarios = [] + self.current_boost_range = [12, 100] + self.current_timeout = 7.0 + self.current_rule_zero = False + self.temp_offensive_mode = None + self.temp_defensive_mode = None + self.temp_player_role = None + + def _add_custom_scenario(self, scenario_name): + """Add a custom scenario""" + self.current_custom_scenarios.append(CustomScenario.load(scenario_name)) + print(f"Added custom scenario: {scenario_name}") + + def get_custom_playlists(self): + """Get all custom playlists""" + # Load all custom playlists from disk + custom_playlists = {} + for file in os.listdir(_get_custom_playlists_path()): + if file.endswith(".json"): + with open(os.path.join(_get_custom_playlists_path(), file), "r") as f: + custom_playlists[file.replace(".json", "")] = Playlist.model_validate_json(f.read()) + return custom_playlists + + +def _get_custom_playlists_path(): + appdata_path = os.path.expandvars("%APPDATA%") + if not os.path.exists(os.path.join(appdata_path, "RLBot", "Dojo", "Playlists")): + os.makedirs(os.path.join(appdata_path, "RLBot", "Dojo", "Playlists")) + return os.path.join(appdata_path, "RLBot", "Dojo", "Playlists") diff --git a/RLBotPack/RLDojo/Dojo/custom_scenario.py b/RLBotPack/RLDojo/Dojo/custom_scenario.py new file mode 100644 index 00000000..fc06eefa --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/custom_scenario.py @@ -0,0 +1,223 @@ +import json +import os +from typing import List, Dict, Any, Optional, Tuple +from pydantic import BaseModel, Field, ValidationError +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator + +class Vector3Model(BaseModel): + x: float = Field(default=0.0) + y: float = Field(default=0.0) + z: float = Field(default=0.0) + +class RotatorModel(BaseModel): + pitch: float = Field(default=0.0) + yaw: float = Field(default=0.0) + roll: float = Field(default=0.0) + +class PhysicsModel(BaseModel): + location: Vector3Model = Field(default_factory=Vector3Model) + rotation: RotatorModel = Field(default_factory=RotatorModel) + velocity: Vector3Model = Field(default_factory=Vector3Model) + angular_velocity: Optional[Vector3Model] = None + +class CarStateModel(BaseModel): + physics: PhysicsModel = Field(default_factory=PhysicsModel) + boost_amount: float = Field(default=0.0) + jumped: bool = Field(default=False) + double_jumped: bool = Field(default=False) + +class BallStateModel(BaseModel): + physics: PhysicsModel = Field(default_factory=PhysicsModel) + +class TypedGameState(BaseModel): + cars: Dict[int, CarStateModel] = Field(default_factory=dict) + ball: Optional[BallStateModel] = None + + @classmethod + def from_game_state(cls, game_state: GameState) -> 'TypedGameState': + """Convert RLBot GameState to TypedGameState""" + cars = {} + if game_state.cars is not None: + for idx, car in game_state.cars.items(): + if car is None: + continue + + cars[idx] = CarStateModel( + # Check all members of car.physics are not None + physics=PhysicsModel( + location=Vector3Model( + x=car.physics.location.x if car.physics.location is not None else 0.0, + y=car.physics.location.y if car.physics.location is not None else 0.0, + z=car.physics.location.z if car.physics.location is not None else 0.0 + ), + rotation=RotatorModel( + pitch=car.physics.rotation.pitch if car.physics.rotation is not None else 0.0, + yaw=car.physics.rotation.yaw if car.physics.rotation is not None else 0.0, + roll=car.physics.rotation.roll if car.physics.rotation is not None else 0.0 + ), + velocity=Vector3Model( + x=car.physics.velocity.x if car.physics.velocity is not None else 0.0, + y=car.physics.velocity.y if car.physics.velocity is not None else 0.0, + z=car.physics.velocity.z if car.physics.velocity is not None else 0.0 + ), + angular_velocity=Vector3Model( + x=car.physics.angular_velocity.x if car.physics.angular_velocity is not None else 0.0, + y=car.physics.angular_velocity.y if car.physics.angular_velocity is not None else 0.0, + z=car.physics.angular_velocity.z if car.physics.angular_velocity is not None else 0.0 + ) + ), + boost_amount=car.boost_amount if car.boost_amount is not None else 0.0, + jumped=car.jumped if car.jumped is not None else False, + double_jumped=car.double_jumped if car.double_jumped is not None else False + ) + + ball = None + if game_state.ball is not None: + ball = BallStateModel( + physics=PhysicsModel( + location=Vector3Model( + x=game_state.ball.physics.location.x if game_state.ball.physics.location is not None else 0.0, + y=game_state.ball.physics.location.y if game_state.ball.physics.location is not None else 0.0, + z=game_state.ball.physics.location.z if game_state.ball.physics.location is not None else 0.0 + ), + rotation=RotatorModel( + pitch=game_state.ball.physics.rotation.pitch if game_state.ball.physics.rotation is not None else 0.0, + yaw=game_state.ball.physics.rotation.yaw if game_state.ball.physics.rotation is not None else 0.0, + roll=game_state.ball.physics.rotation.roll if game_state.ball.physics.rotation is not None else 0.0 + ), + velocity=Vector3Model( + x=game_state.ball.physics.velocity.x if game_state.ball.physics.velocity is not None else 0.0, + y=game_state.ball.physics.velocity.y if game_state.ball.physics.velocity is not None else 0.0, + z=game_state.ball.physics.velocity.z if game_state.ball.physics.velocity is not None else 0.0 + ), + angular_velocity=Vector3Model( + x=game_state.ball.physics.angular_velocity.x if game_state.ball.physics.angular_velocity is not None else 0.0, + y=game_state.ball.physics.angular_velocity.y if game_state.ball.physics.angular_velocity is not None else 0.0, + z=game_state.ball.physics.angular_velocity.z if game_state.ball.physics.angular_velocity is not None else 0.0 + ) + ) + ) + + return cls(cars=cars, ball=ball) + + def to_game_state(self) -> GameState: + """Convert TypedGameState back to RLBot GameState""" + cars = {} + for idx, car in self.cars.items(): + cars[idx] = CarState( + physics=Physics( + location=Vector3( + x=car.physics.location.x, + y=car.physics.location.y, + z=car.physics.location.z + ), + rotation=Rotator( + pitch=car.physics.rotation.pitch, + yaw=car.physics.rotation.yaw, + roll=car.physics.rotation.roll + ), + velocity=Vector3( + x=car.physics.velocity.x, + y=car.physics.velocity.y, + z=car.physics.velocity.z + ), + angular_velocity=Vector3( + x=car.physics.angular_velocity.x, + y=car.physics.angular_velocity.y, + z=car.physics.angular_velocity.z + ) + ), + boost_amount=car.boost_amount, + jumped=car.jumped, + double_jumped=car.double_jumped + ) + + ball = None + if self.ball is not None: + ball = BallState( + physics=Physics( + location=Vector3( + x=self.ball.physics.location.x, + y=self.ball.physics.location.y, + z=self.ball.physics.location.z + ), + rotation=Rotator( + pitch=self.ball.physics.rotation.pitch, + yaw=self.ball.physics.rotation.yaw, + roll=self.ball.physics.rotation.roll + ), + velocity=Vector3( + x=self.ball.physics.velocity.x, + y=self.ball.physics.velocity.y, + z=self.ball.physics.velocity.z + ), + angular_velocity=Vector3( + x=self.ball.physics.angular_velocity.x, + y=self.ball.physics.angular_velocity.y, + z=self.ball.physics.angular_velocity.z + ) + ) + ) + + return GameState(cars=cars, ball=ball) + +class CustomScenario(BaseModel): + """A custom scenario that can be saved to and loaded from disk. + + Attributes: + name: The name of the scenario + game_state: The game state for this scenario + """ + name: str + game_state: TypedGameState + + @classmethod + def from_rlbot_game_state(cls, name: str, game_state: GameState) -> 'CustomScenario': + """Create a CustomScenario from an RLBot GameState""" + return cls( + name=name, + game_state=TypedGameState.from_game_state(game_state) + ) + + def to_rlbot_game_state(self) -> GameState: + """Convert this scenario back to an RLBot GameState""" + return self.game_state.to_game_state() + + def save(self) -> None: + """Save this scenario to disk""" + if not self.name: + raise ValueError("Scenario must have a name before saving") + + # Ensure the scenarios directory exists + os.makedirs(_get_custom_scenarios_path(), exist_ok=True) + + # Save to file + file_path = os.path.join(_get_custom_scenarios_path(), f"{self.name}.json") + with open(file_path, "w") as f: + f.write(self.model_dump_json(indent=2)) + + @classmethod + def load(cls, name: str) -> 'CustomScenario': + """Load a specific scenario by name""" + file_path = os.path.join(_get_custom_scenarios_path(), f"{name}.json") + if not os.path.exists(file_path): + raise FileNotFoundError(f"No scenario found with name '{name}'") + + with open(file_path, "r") as f: + return cls.model_validate_json(f.read()) + + + +def get_custom_scenarios(): + """Get all custom scenarios""" + custom_scenarios = {} + for file in os.listdir(_get_custom_scenarios_path()): + if file.endswith(".json"): + custom_scenarios[file.replace(".json", "")] = CustomScenario.load(file.replace(".json", "")) + return custom_scenarios + +def _get_custom_scenarios_path(): + appdata_path = os.path.expandvars("%APPDATA%") + if not os.path.exists(os.path.join(appdata_path, "RLBot", "Dojo", "Scenarios")): + os.makedirs(os.path.join(appdata_path, "RLBot", "Dojo", "Scenarios")) + return os.path.join(appdata_path, "RLBot", "Dojo", "Scenarios") diff --git a/RLBotPack/RLDojo/Dojo/dojo.cfg b/RLBotPack/RLDojo/Dojo/dojo.cfg new file mode 100644 index 00000000..7f7c6726 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/dojo.cfg @@ -0,0 +1,12 @@ +[Locations] +script_file = ./dojo.py +name = Dojo +requirements_file = ./requirements.txt + +[Details] +developer = SmoothRik +description = Train against bots. Requires: State Setting, Rendering, 120fps, Disable goal reset +fun_fact = Inspired by 50/50 Minigame. +github = https://github.com/ecolsen7/Dojo +language = Python +tags = 1v1, enables-Dojo, training diff --git a/RLBotPack/RLDojo/Dojo/dojo.py b/RLBotPack/RLDojo/Dojo/dojo.py new file mode 100644 index 00000000..97f9ff86 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/dojo.py @@ -0,0 +1,742 @@ +import numpy as np +import keyboard +import string +from rlbot.agents.base_script import BaseScript +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator + +# Import our new modular components +from game_state import DojoGameState, GymMode, ScenarioPhase, RacePhase, CarIndex, CUSTOM_SELECTION_LIST, CUSTOM_MODES +from game_modes import ScenarioMode, RaceMode +from ui_renderer import UIRenderer +from menu import MenuRenderer, UIElement +from scenario import Scenario, OffensiveMode, DefensiveMode +import constants +import modifier +import utils +from race_record import RaceRecord, RaceRecords, get_race_records +from custom_playlist import CustomPlaylistManager +from playlist import PlaylistRegistry, PlayerRole +from custom_scenario import CustomScenario, get_custom_scenarios + + +class Dojo(BaseScript): + """ + Dojo training application for RLBot. + + A modular training system with multiple game modes: + - Scenario-based training with offensive and defensive modes + - Race mode for speed training + - Custom sandbox mode for creating scenarios + - Menu system for easy configuration + """ + + def __init__(self): + super().__init__("Dojo") + + # Initialize core components + self.game_state = DojoGameState() + self.ui_renderer = None # Will be initialized after renderer is available + + # Game modes + self.scenario_mode = None + self.race_mode = None + self.current_mode = None + + # Menu system + self.menu_renderer = None + self.preset_mode_menu = None + self.race_mode_menu = None + self.playlist_menu = None + + # Custom playlist manager + self.custom_playlist_manager = None + self.playlist_registry = None # Will be initialized after game interface is available + + # Internal state + self.rlbot_game_state = None + + def run(self): + """Main game loop""" + while True: + # Wait for and get the game tick packet + packet = self.wait_game_tick_packet() + packet = self.get_game_tick_packet() + + # Update game state + self._update_game_state(packet) + + # Initialize components on first tick + if self.game_state.ticks == 1: + self._initialize_components() + + # Update current game mode + if self.current_mode: + self.current_mode.update(packet) + + # Render UI + self._render_ui() + + def _update_game_state(self, packet): + """Update the game state with packet information""" + self.game_state.cur_time = packet.game_info.seconds_elapsed + self.game_state.ticks += 1 + # self.game_state.paused = packet.game_info.paused + self.game_state.paused = False + + # Check for disable goal reset mutator on first tick + if self.game_state.ticks == 1: + match_settings = self.get_match_settings() + mutators = match_settings.MutatorSettings() + if mutators.RespawnTimeOption() == 3: + self.game_state.disable_goal_reset = True + + def _initialize_components(self): + """Initialize all components that require the game interface""" + + # Initialize UI renderer + self.ui_renderer = UIRenderer(self.game_interface.renderer, self.game_state) + + # Initialize custom playlist manager and playlist registry + self.custom_playlist_manager = CustomPlaylistManager(renderer=self.game_interface.renderer, main_menu_renderer=self.menu_renderer) + self.playlist_registry = PlaylistRegistry(self.game_interface.renderer) + self.playlist_registry.set_custom_playlist_manager(self.custom_playlist_manager) + + # Initialize game modes + self.scenario_mode = ScenarioMode(self.game_state, self.game_interface) + self.race_mode = RaceMode(self.game_state, self.game_interface) + self.current_mode = self.scenario_mode + + # Set up custom playlist manager with scenario mode + self.scenario_mode.set_playlist_registry(self.playlist_registry) + + # Initialize menu system + self._setup_menus() + + self.custom_playlist_manager.main_menu_renderer = self.menu_renderer + + # Set up keyboard handlers + self._setup_keyboard_handlers() + + # Set initial pause time + self.game_state.pause_time = constants.DEFAULT_PAUSE_TIME + + def _setup_menus(self): + """Set up all menu systems""" + # Main menu + self.menu_renderer = MenuRenderer(self.game_interface.renderer) + self.menu_renderer.is_root = True + self.menu_renderer.add_element(UIElement('Main Menu', header=True)) + self.menu_renderer.add_element(UIElement('Reset Score', function=self._clear_score)) + self.menu_renderer.add_element(UIElement('Freeze Scenario', function=self._toggle_freeze_scenario)) + + # Preset mode menu + self.preset_mode_menu = MenuRenderer(self.game_interface.renderer, columns=3) + self.preset_mode_menu.add_element(UIElement('Offensive Mode', header=True), column=0) + for mode in OffensiveMode: + self.preset_mode_menu.add_element( + UIElement(mode.name, function=self._select_offensive_mode, function_args=mode, chooseable=True), + column=0 + ) + self.preset_mode_menu.add_element(UIElement('Defensive Mode', header=True), column=1) + for mode in DefensiveMode: + self.preset_mode_menu.add_element( + UIElement(mode.name, function=self._select_defensive_mode, function_args=mode, chooseable=True), + column=1 + ) + # Third column for player role + self.preset_mode_menu.add_element(UIElement('Player Role', header=True), column=2) + self.preset_mode_menu.add_element(UIElement('Offense', function=self._set_player_role, function_args=PlayerRole.OFFENSE, chooseable=True), column=2) + self.preset_mode_menu.add_element(UIElement('Defense', function=self._set_player_role, function_args=PlayerRole.DEFENSE, chooseable=True), column=2) + self.preset_mode_menu.add_element(UIElement('', header=True), column=2) # Spacer + self.preset_mode_menu.add_element(UIElement('Confirm Scenario', function=self._handle_back), column=2) + self.menu_renderer.add_element(UIElement('Load Preset Scenario', submenu=self.preset_mode_menu)) + + # Custom scenario selection menu + custom_scenario_selection_menu = self.create_custom_scenario_selection_menu() + self.menu_renderer.add_element(UIElement('Load Custom Scenario', submenu=custom_scenario_selection_menu, submenu_refresh_function=self.create_custom_scenario_selection_menu)) + + # Playlist menu + self.playlist_menu = self.create_playlist_menu() + self.menu_renderer.add_element(UIElement('Select Playlist', submenu=self.playlist_menu, submenu_refresh_function=self.create_playlist_menu)) + + # Custom playlist creation menu + if self.custom_playlist_manager: + custom_playlist_menu = self.custom_playlist_manager.create_playlist_creation_menu() + self.menu_renderer.add_element(UIElement('Create Custom Playlist', submenu=custom_playlist_menu)) + + # Custom scenario creation menu + self.custom_scenario_creation_menu = MenuRenderer(self.game_interface.renderer, columns=1, render_function=self._render_custom_sandbox_ui, disable_menu_render=True) + self.custom_scenario_creation_menu.add_element(UIElement('Create Custom Scenario', header=True)) + custom_scenario_starting_point_menu = self.create_custom_scenario_starting_point_menu() + self.menu_renderer.add_element(UIElement('Create Custom Scenario', submenu=custom_scenario_starting_point_menu, submenu_refresh_function=self.create_custom_scenario_starting_point_menu)) + + # Race mode menu + self.race_mode_menu = MenuRenderer(self.game_interface.renderer) + self.race_mode_menu.add_element(UIElement('Number of Trials', header=True)) + for option in constants.DEFAULT_TRIAL_OPTIONS: + self.race_mode_menu.add_element( + UIElement(str(option), function=self._set_race_mode, function_args=option) + ) + self.menu_renderer.add_element(UIElement('Race Mode', submenu=self.race_mode_menu)) + + def _setup_keyboard_handlers(self): + """Set up all keyboard hotkeys""" + keyboard.add_hotkey('m', self._toggle_menu) + keyboard.add_hotkey('left', self._handle_left) + keyboard.add_hotkey('right', self._handle_right) + keyboard.add_hotkey('down', self._handle_down) + keyboard.add_hotkey('up', self._handle_up) + keyboard.add_hotkey('tab', self._handle_tab) + keyboard.add_hotkey('b', self._handle_back) + keyboard.add_hotkey('enter', self._enter_handler) + keyboard.add_hotkey('1', self._handle_custom_trial) + + # For all other letters, submit the letter as a text input + for letter in string.ascii_lowercase: + self._add_hotkey_with_arg(letter, self._handle_text_input, letter) + + # Also allow underscores and dashes in text input + self._add_hotkey_with_arg('_', self._handle_text_input, '_') + self._add_hotkey_with_arg('-', self._handle_text_input, '-') + + # Allow backspace in text input + keyboard.add_hotkey('backspace', self._handle_backspace) + + ### Keyboard handler utilities + def _add_hotkey_with_arg(self, hotkey, function, function_args): + def wrapper(): + function(function_args) + keyboard.add_hotkey(hotkey, wrapper) + + ### Keyboard handlers + def _enter_handler(self): + """Handle enter key""" + if self.menu_renderer.is_in_text_input_mode(): + self._complete_text_input() + elif self.game_state.is_in_custom_mode(): + self._next_custom_step() + else: + self._enter_menu_element() + + def _enter_menu_element(self): + """Enter the currently selected menu element""" + if self.menu_renderer.is_in_text_input_mode(): + return + + self.menu_renderer.enter_element() + + def _complete_text_input(self): + """Complete text input""" + if not self.menu_renderer.is_in_text_input_mode(): + print(f"Not in text input mode, ignoring enter") + return + + self.menu_renderer.complete_text_input() + self.menu_renderer.handle_back_key() + + def _handle_tab(self): + """Handle tab key -- if in custom mode, cycle through the custom selection list""" + if self.menu_renderer.is_in_text_input_mode(): + return + if self.game_state.is_in_custom_mode(): + self._custom_cycle_selection() + + def _handle_left(self): + """Handle left arrow key""" + if self.game_state.is_in_custom_mode(): + self._custom_left_handler() + else: + self.menu_renderer.move_to_prev_column() + + def _handle_right(self): + """Handle right arrow key""" + if self.game_state.is_in_custom_mode(): + self._custom_right_handler() + else: + self.menu_renderer.move_to_next_column() + + def _handle_down(self): + """Handle down arrow key""" + if self.game_state.is_in_custom_mode(): + self._custom_down_handler() + else: + self.menu_renderer.select_next_element() + + def _handle_up(self): + """Handle up arrow key""" + if self.game_state.is_in_custom_mode(): + self._custom_up_handler() + else: + self.menu_renderer.select_last_element() + + def _handle_back(self): + """Handle back key - either for menu navigation or custom mode""" + # If in text input mode, no-op + if self.menu_renderer.is_in_text_input_mode(): + return + + if self.game_state.is_in_custom_mode(): + self._prev_custom_step() + else: + self.menu_renderer.handle_back_key() + + def _handle_custom_trial(self): + """Handle custom trial key""" + self.game_state.game_phase = ScenarioPhase.CUSTOM_TRIAL + self.current_mode = self.scenario_mode + + def _custom_cycle_selection(self): + """Cycle through the custom selection list""" + if self.game_state.custom_selection_index < len(CUSTOM_SELECTION_LIST) - 1: + self.game_state.custom_selection_index += 1 + else: + self.game_state.custom_selection_index = 0 + + self.game_state.custom_leftright_selection = CUSTOM_SELECTION_LIST[self.game_state.custom_selection_index][0] + self.game_state.custom_updown_selection = CUSTOM_SELECTION_LIST[self.game_state.custom_selection_index][1] + + def _handle_backspace(self): + """Handle backspace in text input""" + if self.game_state.is_in_custom_mode(): + self._prev_custom_step() + else: + if not self.menu_renderer.is_in_text_input_mode(): + print(f"Not in text input mode, ignoring backspace") + return + + self.menu_renderer.handle_text_backspace() + + def _handle_text_input(self, key): + """Handle text input if in text input mode""" + if not self.menu_renderer.is_in_text_input_mode(): + print(f"Not in text input mode, ignoring key: {key}") + return + + self.menu_renderer.handle_text_input(key) + + def _render_custom_sandbox_ui(self): + """Render the custom sandbox UI""" + + if self.game_state.game_phase not in CUSTOM_MODES: + return + + rlbot_game_state = None + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + + # Determine object name + object_name = "" + if self.game_state.game_phase == ScenarioPhase.CUSTOM_OFFENSE: + object_name = "Offensive Car" + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_BALL: + object_name = "Ball" + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_DEFENSE: + object_name = "Defensive Car" + + # Instruction text + text = f"""Custom Mode Sandbox: {object_name} +[tab] cycle movement parameters +[enter] next object [offensive car -> ball -> defensive car] +[backspace] previous object +[1] try scenario""" + + self.renderer.begin_rendering() + + # Main instruction box + self.renderer.draw_rect_2d( + constants.CUSTOM_MODE_MENU_START_X, constants.CUSTOM_MODE_MENU_START_Y, + constants.CUSTOM_MODE_MENU_WIDTH, constants.CUSTOM_MODE_MENU_HEIGHT, + True, self.renderer.black() + ) + self.renderer.draw_string_2d( + constants.CUSTOM_MODE_MENU_START_X, constants.CUSTOM_MODE_MENU_START_Y, + 1, 1, text, self.renderer.white() + ) + + # Controls box + controls_start_y = constants.CUSTOM_MODE_MENU_START_Y + constants.CUSTOM_MODE_MENU_HEIGHT + 100 + self.renderer.draw_rect_2d( + constants.CUSTOM_MODE_MENU_START_X, controls_start_y, + constants.CONTROLS_MENU_WIDTH, constants.CONTROLS_MENU_HEIGHT, + True, self.renderer.black() + ) + + controls_text = f"""Controls (use arrow keys) + ^ +{self.game_state.custom_updown_selection.name} + -{self.game_state.custom_leftright_selection.name}< >+{self.game_state.custom_leftright_selection.name} + v -{self.game_state.custom_updown_selection.name}""" + + self.renderer.draw_string_2d( + constants.CUSTOM_MODE_MENU_START_X, controls_start_y, + 1, 1, controls_text, self.renderer.white() + ) + + # Render velocity vectors + self.ui_renderer.render_velocity_vectors(rlbot_game_state) + + self.renderer.end_rendering() + + def _render_ui(self): + """Render all UI elements""" + if self.ui_renderer: + # Render main UI + self.ui_renderer.render_main_ui() + + # Render custom sandbox UI if in custom mode + if self.game_state.is_in_custom_mode(): + rlbot_game_state = None + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + # self.ui_renderer.render_custom_sandbox_ui(rlbot_game_state) + + # Render menu if in menu mode + if self.game_state.game_phase in [ScenarioPhase.MENU, RacePhase.MENU, + ScenarioPhase.CUSTOM_OFFENSE, ScenarioPhase.CUSTOM_BALL, ScenarioPhase.CUSTOM_DEFENSE]: + self.menu_renderer.render_menu() + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_NAMING: + self.menu_renderer.render_menu() + + # Menu action handlers + def _clear_score(self): + """Clear both human and bot scores""" + self.game_state.clear_score() + + + def _toggle_freeze_scenario(self): + """Toggle scenario freezing""" + self.game_state.toggle_freeze_scenario() + + def _set_custom_scenario_name(self, name): + """Set the custom scenario name""" + if self.game_state.game_phase == ScenarioPhase.CUSTOM_NAMING: + self.game_state.custom_scenario_name = name + + # Get the current game state and record it to file + rlbot_game_state = None + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + if rlbot_game_state: + custom_scenario = CustomScenario.from_rlbot_game_state(name, rlbot_game_state) + custom_scenario.save() + + + # todo put some state saving here so it doesn't setup the wrong scenario? + self.scenario_mode.set_custom_scenario(custom_scenario) + self.game_state.game_phase = ScenarioPhase.SETUP + self.current_mode = self.scenario_mode + self.current_mode.clear_playlist() + self.current_mode._set_next_game_state() + + def _select_offensive_mode(self, mode): + """Select offensive mode""" + print(f"Selecting offensive mode: {mode}") + self.game_state.offensive_mode = mode + if self.game_state.game_phase != ScenarioPhase.MENU: + self.game_state.game_phase = ScenarioPhase.SETUP + self.current_mode = self.scenario_mode + self.game_state.gym_mode = GymMode.SCENARIO + self.current_mode.clear_playlist() + self.current_mode._set_next_game_state() + + + def _select_defensive_mode(self, mode): + """Select defensive mode""" + print(f"Selecting defensive mode: {mode}") + self.game_state.defensive_mode = mode + if self.game_state.game_phase != ScenarioPhase.MENU: + self.game_state.game_phase = ScenarioPhase.SETUP + self.current_mode = self.scenario_mode + self.game_state.gym_mode = GymMode.SCENARIO + self.current_mode.clear_playlist() + self.current_mode._set_next_game_state() + + def _set_player_role(self, role): + """Set the player role""" + if role == PlayerRole.OFFENSE: + self.game_state.player_offense = True + else: + self.game_state.player_offense = False + self.current_mode = self.scenario_mode + self.game_state.gym_mode = GymMode.SCENARIO + self.current_mode.clear_playlist() + self.current_mode._set_next_game_state() + + def _set_race_mode(self, trials): + """Set race mode with specified number of trials""" + print(f"Setting race mode with {trials} trials") + self.game_state.gym_mode = GymMode.RACE + self.game_state.game_phase = RacePhase.INIT + self.game_state.num_trials = trials + self.game_state.race_mode_records = get_race_records() + + # Switch to race mode + if self.current_mode: + self.current_mode.cleanup() + self.current_mode = self.race_mode + + def _toggle_menu(self): + """Toggle menu visibility""" + if self.game_state.gym_mode == GymMode.RACE: + if self.game_state.game_phase == RacePhase.MENU: + self.game_state.game_phase = RacePhase.EXITING_MENU + else: + self.game_state.game_phase = RacePhase.MENU + elif self.game_state.gym_mode == GymMode.SCENARIO: + if self.game_state.game_phase == ScenarioPhase.MENU: + self.game_state.game_phase = ScenarioPhase.EXITING_MENU + else: + self.game_state.game_phase = ScenarioPhase.MENU + + # Custom mode handlers + def _custom_down_handler(self): + """Handle down input in custom mode""" + object_to_modify = self._get_custom_object_to_modify() + if not object_to_modify: + return + + if self.game_state.custom_updown_selection.name == 'Y': + modifier.modify_object_y(object_to_modify, -100) + elif self.game_state.custom_updown_selection.name == 'Z': + modifier.modify_object_z(object_to_modify, -100) + elif self.game_state.custom_updown_selection.name == 'PITCH': + modifier.modify_pitch(object_to_modify, increase=True) + elif self.game_state.custom_updown_selection.name == 'VELOCITY': + modifier.modify_velocity(object_to_modify, -0.1) + + # Update the game state + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + if rlbot_game_state: + self.game_interface.set_game_state(rlbot_game_state) + + def _custom_up_handler(self): + """Handle up input in custom mode""" + object_to_modify = self._get_custom_object_to_modify() + if not object_to_modify: + return + + if self.game_state.custom_updown_selection.name == 'Y': + modifier.modify_object_y(object_to_modify, 100) + elif self.game_state.custom_updown_selection.name == 'Z': + modifier.modify_object_z(object_to_modify, 100) + elif self.game_state.custom_updown_selection.name == 'PITCH': + modifier.modify_pitch(object_to_modify, increase=False) + elif self.game_state.custom_updown_selection.name == 'VELOCITY': + modifier.modify_velocity(object_to_modify, 0.1) + + # Update the game state + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + if rlbot_game_state: + self.game_interface.set_game_state(rlbot_game_state) + + def _custom_left_handler(self): + """Handle left input in custom mode""" + object_to_modify = self._get_custom_object_to_modify() + if not object_to_modify: + return + + if self.game_state.custom_leftright_selection.name == 'X': + modifier.modify_object_x(object_to_modify, 50) + elif self.game_state.custom_leftright_selection.name == 'YAW': + modifier.modify_yaw(object_to_modify, increase=False) + elif self.game_state.custom_leftright_selection.name == 'ROLL': + modifier.modify_roll(object_to_modify, increase=False) + elif self.game_state.custom_leftright_selection.name == 'BOOST': + modifier.modify_boost(object_to_modify, increase=True) + + # Update the game state + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + if rlbot_game_state: + self.game_interface.set_game_state(rlbot_game_state) + + def _custom_right_handler(self): + """Handle right input in custom mode""" + object_to_modify = self._get_custom_object_to_modify() + if not object_to_modify: + return + + if self.game_state.custom_leftright_selection.name == 'X': + modifier.modify_object_x(object_to_modify, -50) + elif self.game_state.custom_leftright_selection.name == 'YAW': + modifier.modify_yaw(object_to_modify, increase=True) + elif self.game_state.custom_leftright_selection.name == 'ROLL': + modifier.modify_roll(object_to_modify, increase=True) + elif self.game_state.custom_leftright_selection.name == 'BOOST': + modifier.modify_boost(object_to_modify, increase=False) + + # Update the game state + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + if rlbot_game_state: + self.game_interface.set_game_state(rlbot_game_state) + + def _get_custom_object_to_modify(self): + """Get the object to modify based on current custom phase""" + rlbot_game_state = None + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + + if not rlbot_game_state: + return None + + if self.game_state.game_phase == ScenarioPhase.CUSTOM_OFFENSE: + return rlbot_game_state.cars.get(CarIndex.HUMAN.value) + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_BALL: + return rlbot_game_state.ball + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_DEFENSE: + return rlbot_game_state.cars.get(CarIndex.BOT.value) + return None + + def _next_custom_step(self): + """Move to next step in custom mode creation""" + if self.game_state.game_phase == ScenarioPhase.CUSTOM_OFFENSE: + self.game_state.game_phase = ScenarioPhase.CUSTOM_BALL + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_BALL: + self.game_state.game_phase = ScenarioPhase.CUSTOM_DEFENSE + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_DEFENSE: + self.game_state.game_phase = ScenarioPhase.CUSTOM_NAMING + self.menu_renderer.render_text_input_menu(self._set_custom_scenario_name) + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_NAMING: + # Save the custom scenario + rlbot_game_state = None + if hasattr(self.current_mode, 'get_rlbot_game_state'): + rlbot_game_state = self.current_mode.get_rlbot_game_state() + + if rlbot_game_state: + scenario = Scenario.FromGameState(rlbot_game_state) + self.game_state.scenario_history.append(scenario) + self.game_state.freeze_scenario_index = len(self.game_state.scenario_history) - 1 + self.game_state.game_phase = ScenarioPhase.MENU + + def _prev_custom_step(self): + """Move to previous step in custom mode creation""" + if self.game_state.game_phase == ScenarioPhase.CUSTOM_OFFENSE: + self.game_state.game_phase = ScenarioPhase.MENU + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_BALL: + self.game_state.game_phase = ScenarioPhase.CUSTOM_OFFENSE + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_DEFENSE: + self.game_state.game_phase = ScenarioPhase.CUSTOM_BALL + elif self.game_state.game_phase == ScenarioPhase.CUSTOM_NAMING: + self.game_state.game_phase = ScenarioPhase.CUSTOM_DEFENSE + + def create_custom_scenario_selection_menu(self): + """Create custom scenario creation submenu""" + custom_scenarios = get_custom_scenarios() + + custom_scenario_selection_menu = MenuRenderer(self.game_interface.renderer, columns=1) + custom_scenario_selection_menu.add_element(UIElement("Select Custom Scenario", header=True)) + for scenario_name in custom_scenarios: + custom_scenario_selection_menu.add_element(UIElement(scenario_name, function=self.load_custom_scenario, function_args=scenario_name)) + return custom_scenario_selection_menu + + def create_custom_scenario_starting_point_menu(self): + """Create custom scenario starting point submenu""" + custom_scenarios = get_custom_scenarios() + + custom_scenario_selection_menu = MenuRenderer(self.game_interface.renderer, columns=1) + custom_scenario_selection_menu.add_element(UIElement("Select Custom Scenario", header=True)) + custom_scenario_selection_menu.add_element(UIElement("From Scratch", function=self._set_from_scratch_scenario, submenu=self.custom_scenario_creation_menu)) + for scenario_name in custom_scenarios: + custom_scenario_selection_menu.add_element(UIElement(scenario_name, function=self._start_from_custom_scenario, function_args=scenario_name, submenu=self.custom_scenario_creation_menu)) + return custom_scenario_selection_menu + + def _start_from_custom_scenario(self, scenario_name): + """Start from a custom scenario""" + print(f"Starting from custom scenario: {scenario_name}") + custom_scenario = CustomScenario.load(scenario_name) + game_state = custom_scenario.to_rlbot_game_state() + self.scenario_mode.rlbot_game_state = game_state + self.game_interface.set_game_state(game_state) + self.game_state.game_phase = ScenarioPhase.CUSTOM_OFFENSE + + def _set_from_scratch_scenario(self): + # Setup a blank scenario with player on offense + # Set up initial car positions + car_states = {} + + # Spawn the player car in the middle of the map toward their net + player_car_state = CarState( + physics=Physics( + location=Vector3(0, -400, 10), + velocity=Vector3(0, 0, 0), + rotation=Rotator(yaw=np.pi/2, pitch=0, roll=0) + ), + boost_amount=44 + ) + + # Place the bot toward their net facing the middle + bot_car_state = CarState( + physics=Physics( + location=Vector3(0, 400, 10), + velocity=Vector3(0, 0, 0), + rotation=Rotator(yaw=-np.pi/2, pitch=0, roll=0) + ), + boost_amount=44 + ) + + # Spawn the ball in the middle of the map + ball_state = BallState( + physics=Physics( + location=Vector3(0, 0, 200), + velocity=Vector3(0, 0, 50) + ) + ) + car_states[CarIndex.HUMAN.value] = player_car_state + car_states[CarIndex.BOT.value] = bot_car_state + + self.scenario_mode.rlbot_game_state = GameState(cars=car_states, ball=ball_state) + self.game_interface.set_game_state(self.scenario_mode.rlbot_game_state) + self.game_state.game_phase = ScenarioPhase.CUSTOM_OFFENSE + + def load_custom_scenario(self, scenario_name): + """Load a custom scenario""" + custom_scenario = CustomScenario.load(scenario_name) + self.scenario_mode.set_custom_scenario(custom_scenario) + self.game_state.game_phase = ScenarioPhase.SETUP + self.current_mode = self.scenario_mode + self.current_mode.clear_playlist() + + def create_playlist_menu(self): + """Create playlist selection submenu""" + # Refresh custom playlists to include any newly created ones + self.playlist_registry.refresh_custom_playlists() + + playlist_menu = MenuRenderer(self.game_interface.renderer, columns=1) + playlist_menu.add_element(UIElement("Select Playlist", header=True)) + + # Add each playlist as a menu option + for playlist_name in self.playlist_registry.list_playlists(): + print(f"Playlist name: {playlist_name}") + print(f"Retrieved playlist: {self.playlist_registry.get_playlist(playlist_name)}") + playlist = self.playlist_registry.get_playlist(playlist_name) + playlist_menu.add_element(UIElement( + f"{playlist.name}", + function=self.set_playlist, + function_args=playlist_name + )) + + return playlist_menu + + def set_playlist(self, playlist_name): + """Set the active playlist and return to game""" + print(f"Setting playlist: {playlist_name}") + self.scenario_mode.set_playlist(playlist_name) + self.game_state.gym_mode = GymMode.SCENARIO + self.game_state.game_phase = ScenarioPhase.SETUP + + # Switch to scenario mode if not already + if self.current_mode != self.scenario_mode: + if self.current_mode: + self.current_mode.cleanup() + self.current_mode = self.scenario_mode + + def cleanup(self): + """Clean up keyboard handlers""" + keyboard.unhook_all() + +# Entry point +if __name__ == "__main__": + script = Dojo() + script.run() diff --git a/RLBotPack/RLDojo/Dojo/game_modes/__init__.py b/RLBotPack/RLDojo/Dojo/game_modes/__init__.py new file mode 100644 index 00000000..511fd8bb --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/game_modes/__init__.py @@ -0,0 +1,3 @@ +from .base_mode import BaseGameMode +from .scenario_mode import ScenarioMode +from .race_mode import RaceMode diff --git a/RLBotPack/RLDojo/Dojo/game_modes/base_mode.py b/RLBotPack/RLDojo/Dojo/game_modes/base_mode.py new file mode 100644 index 00000000..8d756667 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/game_modes/base_mode.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from game_state import DojoGameState + + +class BaseGameMode(ABC): + """Abstract base class for all game modes in Dojo""" + + def __init__(self, game_state: 'DojoGameState', game_interface): + self.game_state = game_state + self.game_interface = game_interface + + @abstractmethod + def update(self, packet) -> None: + """Update the game mode with the current packet""" + pass + + @abstractmethod + def initialize(self) -> None: + """Initialize the game mode""" + pass + + @abstractmethod + def cleanup(self) -> None: + """Clean up resources when switching away from this mode""" + pass + + def set_game_state(self, game_state): + """Helper method to set the RLBot game state""" + self.game_interface.set_game_state(game_state) + + def goal_scored(self, packet) -> bool: + """Check if a goal was scored in the last tick""" + team_scores = tuple(map(lambda x: x.score, packet.teams)) + score_diff = max(team_scores) - min(team_scores) + + if score_diff != self.game_state.scoreDiff_prev: + self.game_state.scoreDiff_prev = score_diff + return True + return False + + def get_team_scored(self, packet) -> int: + """Determine which team scored""" + from game_state import CarIndex + + team_scores = tuple(map(lambda x: x.score, packet.teams)) + human_score = team_scores[CarIndex.HUMAN.value] + bot_score = team_scores[CarIndex.BOT.value] + + team = CarIndex.HUMAN.value if human_score > self.game_state.score_human_prev else CarIndex.BOT.value + + self.game_state.score_human_prev = human_score + self.game_state.score_bot_prev = bot_score + return team diff --git a/RLBotPack/RLDojo/Dojo/game_modes/race_mode.py b/RLBotPack/RLDojo/Dojo/game_modes/race_mode.py new file mode 100644 index 00000000..89022eb5 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/game_modes/race_mode.py @@ -0,0 +1,191 @@ +import numpy as np +import time +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator + +from .base_mode import BaseGameMode +from game_state import RacePhase, CarIndex +import race +from race_record import RaceRecord, RaceRecords, store_race_records + + +class RaceMode(BaseGameMode): + """Handles race-based training mode""" + + def __init__(self, game_state, game_interface): + super().__init__(game_state, game_interface) + self.race = None + self.rlbot_game_state = None + self.last_menu_phase_time = 0 + + def initialize(self): + """Initialize race mode""" + np.random.seed(0) + self.game_state.human_score = 0 + self.game_state.bot_score = 0 + self.game_state.started_time = self.game_state.cur_time + self.game_state.game_phase = RacePhase.SETUP + + # Set up initial car positions + car_states = {} + + # Spawn the player car in the middle of the map + player_car_state = CarState( + physics=Physics( + location=Vector3(0, 0, 0), + velocity=Vector3(0, 0, 0), + rotation=Rotator(0, 0, 0) + ) + ) + + # Tuck the bot above the map + bot_car_state = CarState( + physics=Physics( + location=Vector3(0, 0, 2500), + velocity=Vector3(0, 0, 0), + rotation=Rotator(0, 0, 0) + ) + ) + + car_states[CarIndex.HUMAN.value] = player_car_state + car_states[CarIndex.BOT.value] = bot_car_state + + self.rlbot_game_state = GameState(cars=car_states) + self.set_game_state(self.rlbot_game_state) + + def cleanup(self): + """Clean up race mode resources""" + self.race = None + + def update(self, packet): + """Update race mode based on current game phase""" + if self.game_state.paused: + return + + phase_handlers = { + RacePhase.INIT: self._handle_init_phase, + RacePhase.SETUP: self._handle_setup_phase, + RacePhase.ACTIVE: self._handle_active_phase, + RacePhase.MENU: self._handle_menu_phase, + RacePhase.EXITING_MENU: self._handle_menu_exiting_phase, + RacePhase.FINISHED: self._handle_finished_phase, + } + + handler = phase_handlers.get(self.game_state.game_phase) + if handler: + handler(packet) + + def _handle_init_phase(self, packet): + """Handle initialization phase""" + self.initialize() + + def _handle_setup_phase(self, packet): + """Handle setup phase - create new race""" + self.race = race.Race() + ball_state = self.race.BallState() + + self.rlbot_game_state = GameState(ball=ball_state) + self.set_game_state(self.rlbot_game_state) + self.game_state.game_phase = RacePhase.ACTIVE + + def _handle_active_phase(self, packet): + """Handle active race phase""" + # Check if the current ball location has moved significantly + if self._ball_moved_significantly(packet): + self.game_state.human_score += 1 + self.game_state.game_phase = RacePhase.SETUP + + if self.game_state.human_score >= self.game_state.num_trials: + self.game_state.game_phase = RacePhase.FINISHED + return + + # Continue setting the ball location to the race ball location + self._update_game_state(packet) + + def _handle_menu_phase(self, packet): + """Handle menu phase""" + self.set_game_state(self.rlbot_game_state) + self.last_menu_phase_time = time.time() + + def _handle_menu_exiting_phase(self, packet): + """Unfreeze game state after a 3 second countdown""" + # For each second, render a countdown from 3 to 1 + if time.time() - self.last_menu_phase_time > 3: + self.game_state.game_phase = RacePhase.ACTIVE + else: + self.game_interface.renderer.begin_rendering() + self.game_interface.renderer.draw_string_2d(850, 200, 15, 15, str(3 - int(time.time() - self.last_menu_phase_time)), self.game_interface.renderer.white()) + self.game_interface.renderer.end_rendering() + self.set_game_state(self.rlbot_game_state) + + def _handle_finished_phase(self, packet): + """Handle finished phase - save records and restart""" + self.set_game_state(self.rlbot_game_state) + + # Save the record + if self.game_state.human_score >= self.game_state.num_trials: + total_time = self.game_state.cur_time - self.game_state.started_time + print(f"Race completed in {total_time} seconds") + + record = RaceRecord( + number_of_trials=self.game_state.num_trials, + time_to_finish=float(total_time) + ) + self.game_state.race_mode_records.set_record(record) + store_race_records(self.game_state.race_mode_records) + + time.sleep(10) + self.game_state.game_phase = RacePhase.INIT + + def _ball_moved_significantly(self, packet) -> bool: + """Check if the ball has moved significantly from its target position""" + if not self.rlbot_game_state or not self.rlbot_game_state.ball: + return False + + target_pos = self.rlbot_game_state.ball.physics.location + current_pos = packet.game_ball.physics.location + + return (abs(target_pos.x - current_pos.x) > 2 or + abs(target_pos.y - current_pos.y) > 2 or + abs(target_pos.z - current_pos.z) > 2) + + def _update_game_state(self, packet): + """Update the game state with current car position and race ball position""" + ball_state = self.race.BallState() + car_states = {} + + # Preserve human car state + human_car = packet.game_cars[CarIndex.HUMAN.value] + human_car_state = CarState( + physics=Physics( + location=Vector3( + human_car.physics.location.x, + human_car.physics.location.y, + human_car.physics.location.z + ), + velocity=Vector3( + human_car.physics.velocity.x, + human_car.physics.velocity.y, + human_car.physics.velocity.z + ), + rotation=Rotator( + human_car.physics.rotation.pitch, + human_car.physics.rotation.yaw, + human_car.physics.rotation.roll + ) + ) + ) + + # Keep bot tucked away + bot_car_state = CarState( + physics=Physics( + location=Vector3(0, 0, 2500), + velocity=Vector3(0, 0, 0), + rotation=Rotator(0, 0, 0) + ) + ) + + car_states[CarIndex.HUMAN.value] = human_car_state + car_states[CarIndex.BOT.value] = bot_car_state + + self.rlbot_game_state = GameState(cars=car_states, ball=ball_state) + self.set_game_state(self.rlbot_game_state) diff --git a/RLBotPack/RLDojo/Dojo/game_modes/scenario_mode.py b/RLBotPack/RLDojo/Dojo/game_modes/scenario_mode.py new file mode 100644 index 00000000..12ea6918 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/game_modes/scenario_mode.py @@ -0,0 +1,247 @@ +import numpy as np +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator + +from .base_mode import BaseGameMode +from game_state import ScenarioPhase, CarIndex, CUSTOM_MODES +from scenario import Scenario, OffensiveMode, DefensiveMode +from constants import BACK_WALL, GOAL_DETECTION_THRESHOLD, BALL_GROUND_THRESHOLD, FREE_GOAL_TIMEOUT +from playlist import PlaylistRegistry, PlayerRole +import utils +import time +from custom_scenario import CustomScenario + +class ScenarioMode(BaseGameMode): + """Handles scenario-based training mode""" + + def __init__(self, game_state, game_interface): + super().__init__(game_state, game_interface) + self.rlbot_game_state = None + self.prev_time = 0 + self.playlist_registry = None # Will be set via set_playlist_registry + self.current_playlist = None + self.last_menu_phase_time = 0 + self.custom_mode_active = False + self.custom_scenario = None + self.custom_trial_active = False + self.trial_start_time = 0 + + def set_custom_scenario(self, scenario): + """Set the custom scenario""" + self.custom_scenario = scenario + self.custom_mode_active = True + + def set_playlist_registry(self, registry): + """Set the playlist registry to use""" + self.playlist_registry = registry + + def clear_playlist(self): + """Clear the active playlist""" + self.current_playlist = None + + def set_playlist(self, playlist_name): + """Set the active playlist""" + if not self.playlist_registry: + print("Error: Playlist registry not set") + return + + self.current_playlist = self.playlist_registry.get_playlist(playlist_name) + if self.current_playlist: + self.game_state.timeout = self.current_playlist.settings.timeout + self.game_state.rule_zero_mode = self.current_playlist.settings.rule_zero + + def initialize(self): + """Initialize scenario mode""" + np.random.seed(0) + self.game_state.started_time = self.game_state.cur_time + self.game_state.game_phase = ScenarioPhase.SETUP + + if self.game_state.free_goal_mode: + self.set_playlist("Free Goal") + self.game_state.rule_zero_mode = False + + def cleanup(self): + """Clean up scenario mode resources""" + pass + + def update(self, packet): + """Update scenario mode based on current game phase""" + if self.game_state.paused: + return + + phase_handlers = { + ScenarioPhase.INIT: self._handle_init_phase, + ScenarioPhase.SETUP: self._handle_setup_phase, + ScenarioPhase.MENU: self._handle_menu_phase, + ScenarioPhase.EXITING_MENU: self._handle_menu_exiting_phase, + ScenarioPhase.PAUSED: self._handle_paused_phase, + ScenarioPhase.ACTIVE: self._handle_active_phase, + ScenarioPhase.CUSTOM_OFFENSE: self._handle_custom_phase, + ScenarioPhase.CUSTOM_BALL: self._handle_custom_phase, + ScenarioPhase.CUSTOM_DEFENSE: self._handle_custom_phase, + ScenarioPhase.CUSTOM_TRIAL: self._handle_custom_trial_phase, + ScenarioPhase.CUSTOM_NAMING: self._handle_custom_phase, + } + + handler = phase_handlers.get(self.game_state.game_phase) + if handler: + handler(packet) + + def get_rlbot_game_state(self): + """Get the current RLBot game state""" + return self.rlbot_game_state + + def _handle_init_phase(self, packet): + """Handle initialization phase""" + self.initialize() + + def _handle_setup_phase(self, packet): + """Handle setup phase - create new scenario""" + if self.current_playlist: + self._setup_playlist_mode() + + self._set_next_game_state() + self.prev_time = self.game_state.cur_time + self.game_state.game_phase = ScenarioPhase.PAUSED + + def _handle_menu_phase(self, packet): + """Handle menu phase - freeze game state""" + if self.rlbot_game_state: + self.set_game_state(self.rlbot_game_state) + self.last_menu_phase_time = time.time() + + def _handle_menu_exiting_phase(self, packet): + """Unfreeze game state after a 3 second countdown""" + # For each second, render a countdown from 3 to 1 + if time.time() - self.last_menu_phase_time > 3: + self.game_state.game_phase = ScenarioPhase.ACTIVE + + # Reset prev time so we don't instantly timeout + self.prev_time = self.game_state.cur_time + else: + self.game_interface.renderer.begin_rendering() + self.game_interface.renderer.draw_string_2d(850, 200, 15, 15, str(3 - int(time.time() - self.last_menu_phase_time)), self.game_interface.renderer.white()) + self.game_interface.renderer.end_rendering() + self.set_game_state(self.rlbot_game_state) + + def _handle_paused_phase(self, packet): + """Handle paused phase - wait before starting scenario""" + time_elapsed = self.game_state.cur_time - self.prev_time + if (time_elapsed < self.game_state.pause_time or + self.goal_scored(packet) or + packet.game_info.is_kickoff_pause): + if self.rlbot_game_state: + self.set_game_state(self.rlbot_game_state) + else: + self.game_state.game_phase = ScenarioPhase.ACTIVE + + def _handle_active_phase(self, packet): + """Handle active scenario phase""" + # Handle goal reset disabled mode + if self.game_state.disable_goal_reset: + if self._check_ball_in_goal(packet): + return + + # Handle kickoff pause + if packet.game_info.is_kickoff_pause: + self.game_state.game_phase = ScenarioPhase.SETUP + return + + # Handle timeout + time_elapsed = self.game_state.cur_time - self.prev_time + if time_elapsed > self.game_state.timeout: + if (packet.game_ball.physics.location.z < BALL_GROUND_THRESHOLD or + not self.game_state.rule_zero_mode): + self._award_defensive_goal() + self.game_state.game_phase = ScenarioPhase.SETUP + self.game_state.scored_time = self.game_state.cur_time + + def _handle_custom_phase(self, packet): + """Handle custom sandbox phases""" + if self.rlbot_game_state: + self.set_game_state(self.rlbot_game_state) + + def _handle_custom_trial_phase(self, packet): + if not self.custom_trial_active: + self.custom_trial_active = True + self.trial_start_time = self.game_state.cur_time + + if self.game_state.cur_time - self.trial_start_time > 3.0: + self.custom_trial_active = False + self.game_state.game_phase = ScenarioPhase.CUSTOM_OFFENSE + return + + def _setup_playlist_mode(self): + """Setup scenario based on current playlist""" + self.custom_mode_active = False + scenario_config, is_custom = self.current_playlist.get_next_scenario() + if scenario_config and not is_custom: + self.game_state.offensive_mode = scenario_config.offensive_mode + self.game_state.defensive_mode = scenario_config.defensive_mode + self.game_state.player_offense = (scenario_config.player_role == PlayerRole.OFFENSE) + elif scenario_config and is_custom: + self.custom_scenario = scenario_config + self.custom_mode_active = True + + def _set_next_game_state(self): + """Create and set the next scenario game state""" + if not self.game_state.freeze_scenario and not self.custom_mode_active: + print(f"Setting next game state: {self.game_state.offensive_mode}, {self.game_state.defensive_mode}") + + # Get boost range from current playlist if available + boost_range = None + if self.current_playlist and self.current_playlist.settings.boost_range: + boost_range = self.current_playlist.settings.boost_range + print(f"Using playlist boost range: {boost_range}") + + scenario = Scenario(self.game_state.offensive_mode, self.game_state.defensive_mode, boost_range=boost_range) + if self.game_state.player_offense: + scenario.Mirror() + + self.game_state.scenario_history.append(scenario) + self.game_state.freeze_scenario_index = len(self.game_state.scenario_history) - 1 + else: + if self.custom_mode_active: + scenario = Scenario.FromGameState(self.custom_scenario.to_rlbot_game_state()) + else: + scenario = self.game_state.scenario_history[self.game_state.freeze_scenario_index] + + self.rlbot_game_state = scenario.GetGameState() + self.set_game_state(self.rlbot_game_state) + + def _check_ball_in_goal(self, packet) -> bool: + """Check if ball is in goal and award points accordingly""" + ball_y = packet.game_ball.physics.location.y + + + # Check if ball is in blue goal (back wall is blue) + # Bot scored + if ball_y < BACK_WALL - GOAL_DETECTION_THRESHOLD: + self.game_state.bot_score += 1 + self.game_state.game_phase = ScenarioPhase.SETUP + return True + + # Check if ball is in orange goal (negate back wall) + # Human scored + elif ball_y > (-BACK_WALL + GOAL_DETECTION_THRESHOLD): + self.game_state.human_score += 1 + self.game_state.game_phase = ScenarioPhase.SETUP + return True + + # Check for actual goal scored + if self.goal_scored(packet): + team_scored = self.get_team_scored(packet) + if team_scored == CarIndex.HUMAN.value: + self.game_state.human_score += 1 + else: + self.game_state.bot_score += 1 + self.game_state.game_phase = ScenarioPhase.SETUP + return True + + return False + + def _award_defensive_goal(self): + """Award a goal to the defensive team""" + if self.game_state.player_offense: + self.game_state.bot_score += 1 + else: + self.game_state.human_score += 1 diff --git a/RLBotPack/RLDojo/Dojo/game_state.py b/RLBotPack/RLDojo/Dojo/game_state.py new file mode 100644 index 00000000..0f65dac4 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/game_state.py @@ -0,0 +1,146 @@ +from enum import Enum +from dataclasses import dataclass +from typing import List, Optional +from scenario import Scenario, OffensiveMode, DefensiveMode +from race_record import RaceRecord, RaceRecords + + +class CustomUpDownSelection(Enum): + Y = 1 + Z = 2 + PITCH = 3 + VELOCITY = 4 + + +class CustomLeftRightSelection(Enum): + X = 1 + YAW = 2 + ROLL = 3 + BOOST = 4 + +# List of the left/right + up/down states +CUSTOM_SELECTION_LIST = [ + [CustomLeftRightSelection.X, CustomUpDownSelection.Y], + [CustomLeftRightSelection.YAW, CustomUpDownSelection.Z], + [CustomLeftRightSelection.ROLL, CustomUpDownSelection.PITCH], + [CustomLeftRightSelection.BOOST, CustomUpDownSelection.VELOCITY], +] + +class GymMode(Enum): + SCENARIO = 1 + RACE = 2 + + +class ScenarioPhase(Enum): + INIT = -2 + PAUSED = -1 + SETUP = 0 + ACTIVE = 1 + MENU = 2 + EXITING_MENU = 3 + CUSTOM_OFFENSE = 4 + CUSTOM_BALL = 5 + CUSTOM_DEFENSE = 6 + CUSTOM_NAMING = 7 + CUSTOM_TRIAL = 8 + FINISHED = 9 + + +class RacePhase(Enum): + INIT = -1 + SETUP = 0 + ACTIVE = 1 + MENU = 2 + EXITING_MENU = 3 + FINISHED = 4 + + +class CarIndex(Enum): + BLUE = 0 + ORANGE = 1 + HUMAN = 0 + BOT = 1 + + +CUSTOM_MODES = [ + ScenarioPhase.CUSTOM_OFFENSE, + ScenarioPhase.CUSTOM_BALL, + ScenarioPhase.CUSTOM_DEFENSE +] + + +@dataclass +class DojoGameState: + """Centralized game state for the Dojo application""" + # Game mode and phase + gym_mode: GymMode = GymMode.SCENARIO + game_phase: ScenarioPhase = ScenarioPhase.SETUP + + # Scenario settings + offensive_mode: OffensiveMode = OffensiveMode.POSSESSION + defensive_mode: DefensiveMode = DefensiveMode.NEAR_SHADOW + player_offense: bool = True + freeze_scenario: bool = False + freeze_scenario_index: int = 0 + scenario_history: List[Scenario] = None + + # Custom mode selections + custom_updown_selection: CustomUpDownSelection = CustomUpDownSelection.Y + custom_leftright_selection: CustomLeftRightSelection = CustomLeftRightSelection.X + custom_selection_index: int = 0 + + # Scores and timing + human_score: int = 0 + bot_score: int = 0 + timeout: float = 10.0 + pause_time: float = 1.0 + cur_time: float = 0.0 + scored_time: float = 0.0 + started_time: float = 0.0 + + # Game settings + disable_goal_reset: bool = False + rule_zero_mode: bool = False + num_trials: int = 100 + race_mode_records: Optional[RaceRecords] = None + + # Internal state tracking + scoreDiff_prev: int = 0 + score_human_prev: int = 0 + score_bot_prev: int = 0 + prev_ticks: int = 0 + ticks: int = 0 + paused: bool = False + + def __post_init__(self): + if self.scenario_history is None: + self.scenario_history = [] + + def clear_score(self): + """Reset both human and bot scores to zero""" + self.human_score = 0 + self.bot_score = 0 + + def toggle_mirror(self): + """Toggle the player role state""" + self.player_offense = not self.player_offense + + def toggle_freeze_scenario(self): + """Toggle scenario freezing""" + self.freeze_scenario = not self.freeze_scenario + + def is_in_custom_mode(self) -> bool: + """Check if currently in any custom mode""" + return self.game_phase in CUSTOM_MODES + + def get_time_since_start(self) -> tuple[int, int]: + """Get minutes and seconds since start""" + total_seconds = self.cur_time - self.started_time + minutes = int(total_seconds // 60) + seconds = int(total_seconds % 60) + return minutes, seconds + + def get_previous_record(self) -> Optional[float]: + if self.race_mode_records is None: + return None + return self.race_mode_records.get_previous_record(self.num_trials) diff --git a/RLBotPack/RLDojo/Dojo/logo.png b/RLBotPack/RLDojo/Dojo/logo.png new file mode 100644 index 00000000..8b5d4d95 Binary files /dev/null and b/RLBotPack/RLDojo/Dojo/logo.png differ diff --git a/RLBotPack/RLDojo/Dojo/menu.py b/RLBotPack/RLDojo/Dojo/menu.py new file mode 100644 index 00000000..9320299d --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/menu.py @@ -0,0 +1,394 @@ +import numpy as np +from enum import Enum +import keyboard + +from rlbot.agents.base_script import BaseScript +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator, GameInfoState +from scenario import Scenario, OffensiveMode, DefensiveMode +import utils + +units_x_per_char = 11 +units_y_per_line = 40 + +class UIElement(): + ''' Each element consist of a text and a function to call when the element is clicked ''' + def __init__(self, text, function=None, function_args=None, + submenu=None, header=False, display_value_function=None, chooseable=False, spacer=False, + submenu_refresh_function=None): + self.text = text + self.function = function + self.function_args = function_args + self.selected = False + self.entered = False + self.submenu = submenu + self.header = header + self.display_value_function = display_value_function + self.chooseable = chooseable + self.chosen = False + self.spacer = spacer + self.submenu_refresh_function = submenu_refresh_function + + def get_display_value(self): + if self.display_value_function: + return self.display_value_function() + return None + + def back(self): + self.entered = False + + def enter(self): + self.entered = True + if self.submenu_refresh_function: + self.submenu = self.submenu_refresh_function() + + +class MenuRenderer(): + def __init__(self, renderer, columns=1, text_input=False, + text_input_callback=None, render_function=None, show_selections=False, disable_menu_render=False): + self.renderer = renderer + # Each column has its own list of elements + self.elements = [[] for _ in range(columns)] + self.columns = columns + self.active_column = 0 + # Add scroll offset for each column to handle long lists + self.scroll_offset = [0 for _ in range(columns)] + self.is_root = False + self.is_text_input_menu = text_input + self.text_input_value = "" + self.text_input_callback = text_input_callback + self.show_selections = show_selections + self.disable_menu_render = disable_menu_render + + # Allows for an element to be rendered outside of the menu when selected + self.render_function = render_function + + def add_element(self, element, column=0): + self.elements[column].append(element) + + def handle_text_input(self, key): + if self.is_text_input_menu: + self.text_input_value += key + else: + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.handle_text_input(key) + + def handle_text_backspace(self): + if self.is_text_input_menu: + if len(self.text_input_value) > 0: + self.text_input_value = self.text_input_value[:-1] + else: + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.handle_text_backspace() + + def complete_text_input(self): + print("entering complete text input: ", self.is_text_input_menu) + if self.is_text_input_menu: + print("completing text input: ", self.text_input_value) + if self.text_input_callback: + self.text_input_callback(self.text_input_value) + self.text_input_value = "" + self.is_text_input_menu = False + self.text_input_callback = None + else: + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.complete_text_input() + + def _back_deepest_entered_element(self): + has_entered_element = False + for column in range(self.columns): + for element in self.elements[column]: + if element.entered: + has_entered_element = True + deepest_element = element.submenu._back_deepest_entered_element() + if deepest_element: + # Unchoose all of the element's submenu's elements, for all columns + for column in range(element.submenu.columns): + for submenu_element in element.submenu.elements[column]: + submenu_element.chosen = False + element.back() + return False + if not has_entered_element: + return True + return False + + def _get_max_visible_elements(self): + """Calculate maximum number of elements that can fit in the menu""" + MENU_HEIGHT = 500 + available_height = MENU_HEIGHT - 20 # Account for padding + return available_height // units_y_per_line + + def _ensure_selected_visible(self): + """Ensure the selected element is visible by adjusting scroll offset""" + max_visible = self._get_max_visible_elements() + + # Find selected element index + selected_index = -1 + for index, element in enumerate(self.elements[self.active_column]): + if element.selected: + selected_index = index + break + + if selected_index == -1: + return + + # Adjust scroll offset to keep selected element visible + if selected_index < self.scroll_offset[self.active_column]: + # Selected element is above visible area + self.scroll_offset[self.active_column] = selected_index + elif selected_index >= self.scroll_offset[self.active_column] + max_visible: + # Selected element is below visible area + self.scroll_offset[self.active_column] = selected_index - max_visible + 1 + + def select_next_element(self): + # If an element is currently entered, call its select_next_element function + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.select_next_element() + return + for index, element in enumerate(self.elements[self.active_column]): + if element.selected: + element.selected = False + if index < len(self.elements[self.active_column]) - 1: + self.elements[self.active_column][index + 1].selected = True + else: + if not self.elements[self.active_column][0].header: + self.elements[self.active_column][0].selected = True + else: + self.elements[self.active_column][1].selected = True + break + self._ensure_selected_visible() + + def select_last_element(self): + # If an element is currently entered, call its select_last_element function + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.select_last_element() + return + for index, element in enumerate(self.elements[self.active_column]): + if element.selected: + element.selected = False + if index > 0: + if not self.elements[self.active_column][index - 1].header: + self.elements[self.active_column][index - 1].selected = True + else: + if index == 1: + self.elements[self.active_column][len(self.elements[self.active_column]) - 1].selected = True + else: + self.elements[self.active_column][index - 2].selected = True + else: + self.elements[self.active_column][len(self.elements[self.active_column]) - 1].selected = True + break + self._ensure_selected_visible() + + def move_to_next_column(self): + print("move_to_next_column") + for column in range(self.columns): + for element in self.elements[column]: + if element.entered: + element.submenu.move_to_next_column() + return + prev_column = self.active_column + self.active_column += 1 + if self.active_column >= self.columns: + self.active_column = 0 + + # Update selected element + for index, element in enumerate(self.elements[prev_column]): + if element.selected: + if index < len(self.elements[self.active_column]): + self.elements[self.active_column][index].selected = True + else: + self.elements[self.active_column][len(self.elements[self.active_column]) - 1].selected = True + element.selected = False + break + print(self.active_column) + + def move_to_prev_column(self): + for column in range(self.columns): + for element in self.elements[column]: + if element.entered: + element.submenu.move_to_prev_column() + return + prev_column = self.active_column + self.active_column -= 1 + if self.active_column < 0: + self.active_column = self.columns - 1 + + # Update selected element + for index, element in enumerate(self.elements[prev_column]): + if element.selected: + if index < len(self.elements[self.active_column]): + self.elements[self.active_column][index].selected = True + else: + self.elements[self.active_column][len(self.elements[self.active_column]) - 1].selected = True + element.selected = False + break + + def enter_element(self): + # If an element is currently entered, call its enter_element function + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.enter_element() + return + for element in self.elements[self.active_column]: + if element.selected: + if element.function: + if element.function_args: + element.function(element.function_args) + else: + element.function() + if element.submenu: + print("entering submenu: ", element.submenu) + element.enter() + elif element.chooseable: + element.chosen = True + # Unchoose all other elements in the column + for other_element in self.elements[self.active_column]: + if other_element != element: + other_element.chosen = False + break + + def handle_back_key(self): + """Handle the 'b' key press to go back in menus""" + self._back_deepest_entered_element() + + def is_in_text_input_mode(self): + if self.is_text_input_menu: + return True + for column in range(self.columns): + for element in self.elements[column]: + if element.entered: + return element.submenu.is_in_text_input_mode() + return False + + def render_text_input_menu(self, callback): + # Set current entered menu as a text input menu, recursively finding the deepest entered menu + for column in range(self.columns): + for element in self.elements[column]: + if element.entered: + element.submenu.render_text_input_menu(callback) + return + self.text_input_callback = callback + self.is_text_input_menu = True + return + + def render_menu(self): + # If no elements are selected the first time we render the menu, select the first non-header element + if not any(element.selected for element in self.elements[self.active_column]): + for element in self.elements[self.active_column]: + if not element.header: + element.selected = True + break + + # First, check if any submenu is entered + for element in self.elements[self.active_column]: + if element.entered: + element.submenu.render_menu() + return + + # If selected element is a spacer, move to the next element + for element in self.elements[self.active_column]: + if element.selected: + if element.spacer: + self.select_next_element() + return + else: + break + + # Ensure selected element is visible + self._ensure_selected_visible() + + # Draw a rectangle around the menu + MENU_START_X = 20 + MENU_START_Y = 400 + MENU_WIDTH = 500 + MENU_HEIGHT = 500 + COLUMN_WIDTH = MENU_WIDTH / self.columns + max_visible_elements = self._get_max_visible_elements() + + # If menu renderer is disabled, only render the external function + if self.disable_menu_render and self.render_function and not self.is_text_input_menu: + self.render_function() + return + + self.renderer.begin_rendering() + self.renderer.draw_rect_2d(MENU_START_X, MENU_START_Y, MENU_WIDTH, MENU_HEIGHT, False, self.renderer.black()) + print_x = MENU_START_X + 10 + print_y = MENU_START_Y + 10 + text_color = self.renderer.white() + + # Render the list of options, if this menu isn't a text input + if not self.is_text_input_menu: + # If this menu has an external render function, call it in addition to the normal rendering + if self.render_function: + self.render_function() + + for column in range(self.columns): + print_x = MENU_START_X + COLUMN_WIDTH * column + 10 + print_y = MENU_START_Y + 10 + + # Calculate which elements to show based on scroll offset + start_index = self.scroll_offset[column] + end_index = min(start_index + max_visible_elements, len(self.elements[column])) + + # Render only visible elements + for i in range(start_index, end_index): + element = self.elements[column][i] + + display_value = element.get_display_value() + + text = element.text + if display_value != None: + text += ": " + str(display_value) + + if element.chosen: + text += " [x]" + + # If header, draw a smaller rectangle + if element.header: + self.renderer.draw_rect_2d(print_x, print_y - 10, len(element.text) * units_x_per_char, units_y_per_line, False, self.renderer.blue()) + # If selected, draw a rectangle around the element + if element.selected: + self.renderer.draw_rect_2d(print_x, print_y - 10, len(text) * units_x_per_char, units_y_per_line, False, self.renderer.white()) + color = self.renderer.black() + else: + color = text_color + # If header, draw text in green + if element.header: + self.renderer.draw_string_2d(print_x + 5, print_y, 1, 1, element.text, self.renderer.white()) + else: + self.renderer.draw_string_2d(print_x + 5, print_y, 1, 1, text, color) + print_y += units_y_per_line + + # Draw scroll indicators if needed + if len(self.elements[column]) > max_visible_elements: + # Draw scroll up indicator + if self.scroll_offset[column] > 0: + indicator_x = print_x + COLUMN_WIDTH - 30 + indicator_y = MENU_START_Y + 10 + self.renderer.draw_string_2d(indicator_x, indicator_y, 1, 1, "↑", self.renderer.white()) + + # Draw scroll down indicator + if end_index < len(self.elements[column]): + indicator_x = print_x + COLUMN_WIDTH - 30 + indicator_y = MENU_START_Y + MENU_HEIGHT - 30 + self.renderer.draw_string_2d(indicator_x, indicator_y, 1, 1, "↓", self.renderer.white()) + else: + # Prompt user to enter a name for the entity + self.renderer.draw_string_2d(MENU_START_X + 10, MENU_START_Y + 10, 1, 1, "Enter a name:", self.renderer.white()) + + # Display user's current input + self.renderer.draw_string_2d(MENU_START_X + 10, MENU_START_Y + 30, 1, 1, self.text_input_value, self.renderer.white()) + + # Show a cursor + self.renderer.draw_rect_2d(MENU_START_X + 10 + len(self.text_input_value) * units_x_per_char, MENU_START_Y + 30, 2, units_y_per_line, False, self.renderer.white()) + + instruction_text = "Press 'b' to go back" if not self.is_root else "Press 'm' to exit menu" + instruction_x = MENU_START_X + (MENU_WIDTH - len(instruction_text) * units_x_per_char) // 2 + instruction_y = MENU_START_Y + MENU_HEIGHT - 30 + self.renderer.draw_string_2d(instruction_x, instruction_y, 1, 1, instruction_text, self.renderer.white()) + + self.renderer.end_rendering() diff --git a/RLBotPack/RLDojo/Dojo/modifier.py b/RLBotPack/RLDojo/Dojo/modifier.py new file mode 100644 index 00000000..79ee5ff0 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/modifier.py @@ -0,0 +1,129 @@ +import numpy as np +from enum import Enum +import keyboard + +from rlbot.agents.base_script import BaseScript +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator, GameInfoState +from scenario import Scenario, OffensiveMode, DefensiveMode +import utils + +####################################### +### Custom Sandbox Object Modifiers ### +####################################### + +def modify_object_x(object_to_modify, x): + object_to_modify.physics.location.x += x + utils.sanity_check_objects([object_to_modify]) + +def modify_object_y(object_to_modify, y): + object_to_modify.physics.location.y += y + utils.sanity_check_objects([object_to_modify]) + +def modify_object_z(object_to_modify, z): + object_to_modify.physics.location.z += z + utils.sanity_check_objects([object_to_modify]) + +def modify_pitch(object_to_modify, increase): + if utils.hasattrdeep(object_to_modify, 'physics', 'rotation', 'pitch'): + # Pitch should be grid-snapped to 1/16 of a full rotation to ensure we can always get perfect vertical or horizontal pitch + # This function will increase the pitch by 1/16 of a full rotation and lock it to the nearest 1/16 of a full rotation + # Pitch is expressed in radians + # 1/16 of a full rotation is 1/8 * pi radians + # We want to increase the pitch by 1/8 * pi radians and lock it to the nearest 1/8 * pi radians + current_pitch = object_to_modify.physics.rotation.pitch + + # Round to the nearest 1/8 * pi radians + new_pitch = round(current_pitch / (0.125 * np.pi)) * (0.125 * np.pi) + if increase: + new_pitch += (0.125 * np.pi) + else: + new_pitch -= (0.125 * np.pi) + + # Modulo over 2 * pi to ensure we don't go over a full rotation + new_pitch = new_pitch % (2 * np.pi) + + # Set the new pitch + object_to_modify.physics.rotation.pitch = new_pitch + + # Update the velocity to match the new pitch + object_to_modify.physics.velocity = utils.get_velocity_from_rotation(object_to_modify.physics.rotation, 1000, 2000) + else: + # Ball doesn't have rotation, use the velocity components to determine and modify trajectory + yaw = np.arctan2(object_to_modify.physics.velocity.y, object_to_modify.physics.velocity.x) + pitch = np.arctan2(object_to_modify.physics.velocity.z, np.sqrt(object_to_modify.physics.velocity.x**2 + object_to_modify.physics.velocity.y**2)) + + if increase: + pitch += (0.125 * np.pi) + else: + pitch -= (0.125 * np.pi) + + # Convert back to velocity components + object_to_modify.physics.velocity = utils.get_velocity_from_rotation(Rotator(yaw=yaw, pitch=pitch, roll=0), 1000, 2000) + + +def modify_yaw(object_to_modify, increase): + if utils.hasattrdeep(object_to_modify, 'physics', 'rotation', 'yaw'): + # Yaw should be grid-snapped to 1/16 of a full rotation to ensure we can always get perfect horizontal yaw + # This function will increase the yaw by 1/16 of a full rotation and lock it to the nearest 1/16 of a full rotation + # Yaw is expressed in radians + # 1/16 of a full rotation is 1/8 * pi radians + # We want to increase the yaw by 1/8 * pi radians and lock it to the nearest 1/8 * pi radians + current_yaw = object_to_modify.physics.rotation.yaw + new_yaw = round(current_yaw / (0.125 * np.pi)) * (0.125 * np.pi) + if increase: + new_yaw += (0.125 * np.pi) + else: + new_yaw -= (0.125 * np.pi) + + # Modulo over 2 * pi to ensure we don't go over a full rotation + new_yaw = new_yaw % (2 * np.pi) + + object_to_modify.physics.rotation.yaw = new_yaw + object_to_modify.physics.velocity = utils.get_velocity_from_rotation(object_to_modify.physics.rotation, 1000, 2000) + else: + # Ball doesn't have rotation, use the velocity components to determine and modify trajectory + yaw = np.arctan2(object_to_modify.physics.velocity.y, object_to_modify.physics.velocity.x) + pitch = np.arctan2(object_to_modify.physics.velocity.z, np.sqrt(object_to_modify.physics.velocity.x**2 + object_to_modify.physics.velocity.y**2)) + + if increase: + yaw += (0.125 * np.pi) + else: + yaw -= (0.125 * np.pi) + + # Convert back to velocity components + object_to_modify.physics.velocity = utils.get_velocity_from_rotation(Rotator(yaw=yaw, pitch=pitch, roll=0), 1000, 2000) + +def modify_roll(object_to_modify, increase): + if utils.hasattrdeep(object_to_modify, 'physics', 'rotation', 'roll'): + # Roll should be grid-snapped to 1/16 of a full rotation to ensure we can always get perfect horizontal roll + # This function will increase the roll by 1/16 of a full rotation and lock it to the nearest 1/16 of a full rotation + # Roll is expressed in radians + # 1/16 of a full rotation is 1/8 * pi radians + # We want to increase the roll by 1/8 * pi radians and lock it to the nearest 1/8 * pi radians + current_roll = object_to_modify.physics.rotation.roll + new_roll = round(current_roll / (0.125 * np.pi)) * (0.125 * np.pi) + if increase: + new_roll += (0.125 * np.pi) + else: + new_roll -= (0.125 * np.pi) + + object_to_modify.physics.rotation.roll = new_roll + +def modify_velocity(object_to_modify, velocity_percentage_delta): + # Velocity is a 3-dimensional vector, scale each component by the same percentage + x = object_to_modify.physics.velocity.x + y = object_to_modify.physics.velocity.y + z = object_to_modify.physics.velocity.z + + x += x * velocity_percentage_delta + y += y * velocity_percentage_delta + z += z * velocity_percentage_delta + + object_to_modify.physics.velocity = Vector3(x, y, z) + +def modify_boost(object_to_modify, increase): + if utils.hasattrdeep(object_to_modify, 'boost_amount'): + if increase: + object_to_modify.boost_amount += 1 + else: + object_to_modify.boost_amount -= 1 diff --git a/RLBotPack/RLDojo/Dojo/playlist.py b/RLBotPack/RLDojo/Dojo/playlist.py new file mode 100644 index 00000000..df47858e --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/playlist.py @@ -0,0 +1,254 @@ +""" +Playlist system for Dojo training scenarios. + +This module provides a flexible playlist system that allows grouping scenarios +around specific themes or training goals. Playlists can specify: + +- Combinations of offensive and defensive modes +- Player role (offense or defense) +- Custom settings like timeout, shuffle, boost ranges, and rule zero +- Weighted scenario selection + +Boost Range Feature: +- If a playlist specifies boost_range=(min, max), it overrides the default + random boost generation (12-100) that happens in scenario creation +- This allows playlists to focus on specific boost management scenarios +- Examples: Low boost for finishing practice, high boost for mechanical plays + +Rule Zero Feature: +- If a playlist specifies rule_zero=True, scenarios won't end at timeout until + the ball touches the ground, similar to Rocket League's "rule zero" +- This creates more realistic game-ending conditions and allows plays to finish naturally +- Useful for training scenarios where timing and ball control are critical +""" + +from enum import Enum +import numpy as np +from scenario import OffensiveMode, DefensiveMode +from pydantic import BaseModel, Field, ValidationError +from typing import List, Optional, Tuple +from custom_scenario import CustomScenario + +EXTERNAL_MENU_START_X = 1200 +EXTERNAL_MENU_START_Y = 200 +EXTERNAL_MENU_WIDTH = 500 +EXTERNAL_MENU_HEIGHT = 800 + +class PlayerRole(Enum): + OFFENSE = 0 + DEFENSE = 1 + +class ScenarioConfig(BaseModel): + offensive_mode: OffensiveMode + defensive_mode: DefensiveMode + player_role: PlayerRole + weight: float = 1.0 + +class PlaylistSettings(BaseModel): + timeout: float = 7.0 + shuffle: bool = True + loop: bool = True + boost_range: Tuple[int, int] = (12, 100) + rule_zero: bool = False + +class Playlist(BaseModel): + name: str + description: str + scenarios: Optional[List[ScenarioConfig]] = Field(default_factory=list) + custom_scenarios: Optional[List[CustomScenario]] = Field(default_factory=list) + settings: Optional[PlaylistSettings] = Field(default_factory=PlaylistSettings) + offensive_modes: Optional[List[OffensiveMode]] = Field(default_factory=list) + defensive_modes: Optional[List[DefensiveMode]] = Field(default_factory=list) + player_role: Optional[PlayerRole] = None + + + def get_next_scenario(self): + """Get next scenario, considering weights""" + if not self.scenarios and not self.custom_scenarios: + return None + + # Use weighted random selection + # weights = [s.weight for s in self.scenarios] + + total_num_scenarios = len(self.scenarios) + len(self.custom_scenarios) + # scenario = np.random.choice(self.scenarios, p=np.array(weights)/sum(weights)) + # Ignore weights + scenario_index = np.random.randint(0, total_num_scenarios) + is_custom = False + if scenario_index < len(self.scenarios): + scenario = self.scenarios[scenario_index] + else: + is_custom = True + scenario = self.custom_scenarios[scenario_index - len(self.scenarios)] + return scenario, is_custom + + + def render_details(self, renderer): + """Render the playlist details""" + if not renderer: + return + + renderer.draw_rect_2d(EXTERNAL_MENU_START_X, EXTERNAL_MENU_START_Y, EXTERNAL_MENU_WIDTH, EXTERNAL_MENU_HEIGHT, False, renderer.black()) + print_start_x = EXTERNAL_MENU_START_X + 10 + print_start_y = EXTERNAL_MENU_START_Y + 10 + text_color = renderer.white() + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, "Playlist Details", text_color) + print_start_y += 20 + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"Name: {self.name}", text_color) + print_start_y += 20 + num_scenarios = len(self.scenarios) + num_scenarios_text = str(num_scenarios) + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"Scenarios: {num_scenarios_text}", text_color) + print_start_y += 20 + for scenario in self.scenarios: + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"{scenario.offensive_mode.name} vs {scenario.defensive_mode.name}", text_color) + print_start_y += 20 + if self.settings: + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"Boost Range: {self.settings.boost_range[0]}-{self.settings.boost_range[1]}", text_color) + print_start_y += 20 + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"Timeout: {self.settings.timeout}s", text_color) + print_start_y += 20 + if self.custom_scenarios: + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"Custom Scenarios: {len(self.custom_scenarios)}", text_color) + print_start_y += 20 + for scenario in self.custom_scenarios: + renderer.draw_string_2d(print_start_x, print_start_y, 1, 1, f"{scenario.name}", text_color) + print_start_y += 20 + +class PlaylistRegistry: + def __init__(self, renderer=None): + self.playlists = {} + self.custom_playlist_manager = None + self._register_default_playlists() + + def set_custom_playlist_manager(self, manager): + """Set the custom playlist manager to load custom playlists""" + self.custom_playlist_manager = manager + self._load_custom_playlists() + + def _load_custom_playlists(self): + """Load custom playlists from the manager""" + if self.custom_playlist_manager: + custom_playlists = self.custom_playlist_manager.get_custom_playlists() + for name, playlist in custom_playlists.items(): + self.playlists[name] = playlist + + def register_playlist(self, playlist): + self.playlists[playlist.name] = playlist + + def get_playlist(self, name): + return self.playlists.get(name) + + def list_playlists(self): + return list(self.playlists.keys()) + + def refresh_custom_playlists(self): + """Refresh custom playlists from disk""" + if self.custom_playlist_manager: + # Remove existing custom playlists + custom_names = list(self.custom_playlist_manager.get_custom_playlists().keys()) + for name in custom_names: + if name in self.playlists: + del self.playlists[name] + + # Reload custom playlists + self.custom_playlist_manager.load_custom_playlists() + self._load_custom_playlists() + + def _register_default_playlists(self): + # Ground offense - setups for outplaying with ground mechanics + ground_offense = Playlist( + name="Ground Offense", + description="Practice outplaying with ground mechanics", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.POSSESSION, defensive_mode=DefensiveMode.NEAR_SHADOW, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.POSSESSION, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.POSSESSION, defensive_mode=DefensiveMode.NET, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.POSSESSION, defensive_mode=DefensiveMode.FAR_SHADOW, player_role=PlayerRole.OFFENSE), + ], + ) + + # Midfield outplays - for getting past defenders in midfield + midfield_outplays = Playlist( + name="Midfield Outplays", + description="Practice outplaying defenders challenging in midfield", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.BREAKOUT, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL_BREAKOUT, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BACK_CORNER_BREAKOUT, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.OFFENSE), + ], + ) + + # Free Goal (Offense focus) - Low boost for finishing practice + free_goal = Playlist( + name="Free Goal", + description="Practice finishing with minimal defense", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.BACKPASS, defensive_mode=DefensiveMode.RECOVERING, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL_BREAKOUT, defensive_mode=DefensiveMode.RECOVERING, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.PASS, defensive_mode=DefensiveMode.RECOVERING, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.CORNER, defensive_mode=DefensiveMode.RECOVERING, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDE_BACKBOARD_PASS, defensive_mode=DefensiveMode.RECOVERING, player_role=PlayerRole.OFFENSE), + ], + settings=PlaylistSettings(boost_range=(20, 60), rule_zero=True) # Lower boost for finishing practice, rule zero for natural endings + ) + + # Shadow Defense (Defense focus) - Variable boost for realistic defense + shadow_defense = Playlist( + name="Shadow Defense", + description="Practice defensive positioning and timing", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.POSSESSION, defensive_mode=DefensiveMode.NEAR_SHADOW, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BREAKOUT, defensive_mode=DefensiveMode.FAR_SHADOW, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.CARRY, defensive_mode=DefensiveMode.NEAR_SHADOW, player_role=PlayerRole.DEFENSE), + ], + settings=PlaylistSettings(boost_range=(30, 80)) # Moderate boost for defense + ) + + # Reactive Defense - Make saves against passes + reactive_defense = Playlist( + name="Reactive Defense", + description="Practice reactive defense against passes", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.PASS, defensive_mode=DefensiveMode.NET, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.PASS, defensive_mode=DefensiveMode.CORNER, player_role=PlayerRole.DEFENSE), + ], + ) + + # Mechanical (Complex offensive plays) - High boost for mechanics + mechanical = Playlist( + name="Mechanical Offense", + description="Practice complex mechanical plays and wall work", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL, defensive_mode=DefensiveMode.NET, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL_BREAKOUT, defensive_mode=DefensiveMode.NEAR_SHADOW, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BACKPASS, defensive_mode=DefensiveMode.CORNER, player_role=PlayerRole.OFFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BACKPASS, defensive_mode=DefensiveMode.NET, player_role=PlayerRole.OFFENSE), + ], + settings=PlaylistSettings(timeout=10.0, shuffle=True, boost_range=(74, 100)) # High boost for mechanics + ) + + # Front Intercept Defense - Practice intercepting offensive plays + defensive_challenges_mix = Playlist( + name="Defensive Challenges Mix", + description="Practice intercepting and challenging offensive plays from an advanced defensive position", + scenarios=[ + ScenarioConfig(offensive_mode=OffensiveMode.POSSESSION, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BREAKOUT, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.CARRY, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BACKPASS, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.SIDEWALL_BREAKOUT, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ScenarioConfig(offensive_mode=OffensiveMode.BACK_CORNER_BREAKOUT, defensive_mode=DefensiveMode.FRONT_INTERCEPT, player_role=PlayerRole.DEFENSE), + ], + settings=PlaylistSettings(timeout=8.0, shuffle=True, boost_range=(40, 90), rule_zero=True) # Rule zero for realistic challenge timing + ) + + self.register_playlist(ground_offense) + self.register_playlist(midfield_outplays) + self.register_playlist(free_goal) + self.register_playlist(shadow_defense) + self.register_playlist(reactive_defense) + self.register_playlist(mechanical) + self.register_playlist(defensive_challenges_mix) diff --git a/RLBotPack/RLDojo/Dojo/race.py b/RLBotPack/RLDojo/Dojo/race.py new file mode 100644 index 00000000..6af23cb4 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/race.py @@ -0,0 +1,32 @@ +import utils + +import numpy as np +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator, GameInfoState + +class Race: + def __init__(self): + self.ball_state = None + self.player_team = 0 + + valid_start = False + while not valid_start: + # Place the ball in a random location + x_loc = utils.random_between(-4096, 4096) + y_loc = utils.random_between(-5120, 5120) + z_loc = utils.random_between(90, 1954) + ball_velocity = Vector3(0, 0, 0) + + # If the ball is too far from the floor and the sidewalls, try again + dist_from_floor = abs(z_loc) + dist_from_backwall = 5120 - abs(y_loc) + dist_from_sidewall = 4096 - abs(x_loc) + if dist_from_floor < 1000 or dist_from_backwall < 1000 or dist_from_sidewall < 1000: + valid_start = True + + self.ball_state = BallState(Physics(location=Vector3(x_loc, y_loc, z_loc), velocity=ball_velocity)) + + utils.sanity_check_objects([self.ball_state]) + + + def BallState(self): + return self.ball_state \ No newline at end of file diff --git a/RLBotPack/RLDojo/Dojo/race_record.py b/RLBotPack/RLDojo/Dojo/race_record.py new file mode 100644 index 00000000..bf41ffaa --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/race_record.py @@ -0,0 +1,43 @@ +from typing import List, Dict, Optional +from pydantic import BaseModel, Field, ValidationError +import os + + +class RaceRecord(BaseModel): + number_of_trials: int + time_to_finish: float + split_times: List[float] = Field(default_factory=list) + + +class RaceRecords(BaseModel): + records: Dict[int, RaceRecord] + + def set_record(self, race_record: RaceRecord): + self.records[race_record.number_of_trials] = race_record + + def get_previous_record(self, number_of_trials: int) -> Optional[float]: + if number_of_trials not in self.records: + return None + return self.records[number_of_trials].time_to_finish + +def _get_records_base_path(): + appdata_path = os.path.expandvars("%APPDATA%") + if not os.path.exists(os.path.join(appdata_path, "RLBot", "Dojo")): + os.makedirs(os.path.join(appdata_path, "RLBot", "Dojo")) + return os.path.join(appdata_path, "RLBot", "Dojo") + +def _get_race_records_path(): + return os.path.join(_get_records_base_path(), "race_records.json") + +def get_race_records() -> RaceRecords: + if not os.path.exists(_get_race_records_path()): + return RaceRecords(records={}) + with open(_get_race_records_path(), "r") as f: + try: + return RaceRecords.model_validate_json(f.read()) + except ValidationError: + return RaceRecords(records={}) + +def store_race_records(records: RaceRecords): + with open(_get_race_records_path(), "w") as f: + f.write(records.model_dump_json()) diff --git a/RLBotPack/RLDojo/Dojo/requirements.txt b/RLBotPack/RLDojo/Dojo/requirements.txt new file mode 100644 index 00000000..80b910d1 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/requirements.txt @@ -0,0 +1,12 @@ +# Include everything the framework requires +# You will automatically get updates for all versions starting with "1.". +numpy +keyboard + +# This will cause pip to auto-upgrade and stop scaring people with warning messages +pip + +matplotlib +rlbot +pydantic==2.11.5 +pydantic_core==2.33.2 diff --git a/RLBotPack/RLDojo/Dojo/scenario.py b/RLBotPack/RLDojo/Dojo/scenario.py new file mode 100644 index 00000000..0e73fd58 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/scenario.py @@ -0,0 +1,817 @@ +import numpy as np +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator, GameInfoState +import matplotlib.pyplot as plt +from enum import Enum +import utils + +class OffensiveMode(Enum): + POSSESSION = 0 + BREAKOUT = 1 + PASS = 2 + BACKPASS = 3 + CARRY = 4 + CORNER = 5 + SIDEWALL = 6 + LOB_ON_GOAL = 7 + BACKWALL_BOUNCE = 8 + SIDEWALL_BREAKOUT = 9 + BACK_CORNER_BREAKOUT = 10 + BACKBOARD_PASS = 11 + SIDE_BACKBOARD_PASS = 12 + OVER_SHOULDER = 13 + +class DefensiveMode(Enum): + NEAR_SHADOW = 0 + FAR_SHADOW = 1 + NET = 2 + CORNER = 3 + RECOVERING = 4 + FRONT_INTERCEPT = 5 + +class Scenario: + ''' + Scenario represents all initial states of a game mode + Comprised of a BallState and two CarStates (or more, to be added) + ''' + def __init__(self, offensive_mode=None, defensive_mode=None, boost_range=None): + ''' + Create a new scenario based on the game mode + ''' + self.offensive_team = 0 + self.offensive_car_state = CarState() + self.defensive_car_state = CarState() + self.ball_state = BallState() + self.play_yaw = None + match offensive_mode: + case OffensiveMode.POSSESSION: + self.__setup_possession_offense(utils.random_between(-2500, 1500)) + case OffensiveMode.BREAKOUT: + self.__setup_possession_offense(utils.random_between(2000, 3000)) + case OffensiveMode.PASS: + self.__setup_pass_offense() + case OffensiveMode.BACKPASS: + self.__setup_backpass_offense() + case OffensiveMode.CARRY: + self.__setup_carry_offense() + case OffensiveMode.CORNER: + self.__setup_corner_offense() + case OffensiveMode.SIDEWALL: + self.__setup_sidewall_offense() + case OffensiveMode.LOB_ON_GOAL: + self.__setup_lob_on_goal_offense() + case OffensiveMode.BACKBOARD_PASS: + self.__setup_backboard_pass_offense() + case OffensiveMode.BACKWALL_BOUNCE: + self.__setup_backwall_bounce_offense() + case OffensiveMode.SIDEWALL_BREAKOUT: + self.__setup_sidewall_breakout_offense() + case OffensiveMode.BACK_CORNER_BREAKOUT: + self.__setup_backcorner_breakout_offense() + case OffensiveMode.SIDE_BACKBOARD_PASS: + self.__setup_side_backboard_pass_offense() + case OffensiveMode.OVER_SHOULDER: + self.__setup_over_shoulder_offense() + + match defensive_mode: + case DefensiveMode.NEAR_SHADOW: + self.__setup_shadow_defense(utils.random_between(1500, 2500)) + case DefensiveMode.FAR_SHADOW: + self.__setup_shadow_defense(utils.random_between(3000, 4000)) + case DefensiveMode.NET: + self.__setup_net_defense() + case DefensiveMode.CORNER: + self.__setup_corner_defense() + case DefensiveMode.RECOVERING: + self.__setup_recovering_defense() + case DefensiveMode.FRONT_INTERCEPT: + self.__setup_front_intercept_defense() + + + if defensive_mode is not None and offensive_mode is not None: + utils.sanity_check_objects([self.offensive_car_state, self.defensive_car_state, self.ball_state]) + + # Randomize boost level of each car - use boost_range if provided, otherwise default + if boost_range: + min_boost, max_boost = boost_range + self.offensive_car_state.boost_amount = utils.random_between(min_boost, max_boost) + self.defensive_car_state.boost_amount = utils.random_between(min_boost, max_boost) + else: + self.offensive_car_state.boost_amount = utils.random_between(12, 100) + self.defensive_car_state.boost_amount = utils.random_between(12, 100) + + @staticmethod + def FromGameState(game_state): + ''' + Create a new scenario from a game state + ''' + scenario = Scenario() + scenario.offensive_car_state = CarState(physics=game_state.cars[1].physics, boost_amount=game_state.cars[1].boost_amount) + scenario.defensive_car_state = CarState(physics=game_state.cars[0].physics, boost_amount=game_state.cars[0].boost_amount) + scenario.ball_state = BallState(physics=game_state.ball.physics) + utils.sanity_check_objects([scenario.offensive_car_state, scenario.defensive_car_state, scenario.ball_state]) + return scenario + + + def GetGameState(self): + ''' + Set the game state to the scenario + ''' + # Car 0 = Blue, Car 1 = Orange + car_states = {} + if self.offensive_team == 0: + car_states[1] = self.offensive_car_state + car_states[0] = self.defensive_car_state + else: + car_states[0] = self.offensive_car_state + car_states[1] = self.defensive_car_state + return GameState(ball=self.ball_state, cars=car_states) + + + def Mirror(self): + ''' + Mirror the scenario across the Y axis, turning defensive scenarios into offensive scenarios + Involves flipping the Y aspects of the car + ball locations, velocity, and yaw + ''' + self.offensive_car_state.physics.location.y = -self.offensive_car_state.physics.location.y + self.defensive_car_state.physics.location.y = -self.defensive_car_state.physics.location.y + self.ball_state.physics.location.y = -self.ball_state.physics.location.y + + self.offensive_car_state.physics.rotation.yaw = -self.offensive_car_state.physics.rotation.yaw + self.defensive_car_state.physics.rotation.yaw = -self.defensive_car_state.physics.rotation.yaw + + self.offensive_car_state.physics.velocity.y = -self.offensive_car_state.physics.velocity.y + self.defensive_car_state.physics.velocity.y = -self.defensive_car_state.physics.velocity.y + self.ball_state.physics.velocity.y = -self.ball_state.physics.velocity.y + + self.offensive_team = 1 - self.offensive_team + + def Draw(self): + ''' + Plot the scenario against a simulated field, for debugging purposes + ''' + plt.figure() + # Rocket League uses a coordinate system (X, Y, Z), where Z is upwards. Note also that negative Y is towards Blue's goal (team 0). + + # Floor: 0 + # Center field: (0, 0) + # Side wall: x=±4096 + # Side wall length: 7936 + # Back wall: y=±5120 + # Back wall length: 5888 + # Ceiling: z=2044 + # Goal height: z=642.775 + # Goal center-to-post: 892.755 + # Goal depth: 880 + # Corner wall length: 1629.174 + # The corner planes intersect the axes at ±8064 at a 45 degrees angle + + # Draw the field + + # Add vertical lines from at X=-4096 and X=4096, each from Y=-5120 to Y=5120 + # Stop 1152 units short from each wall to leave room for the corners + corner_start = 5120 - 1152 + plt.plot([-4096, -4096], [-corner_start, corner_start], 'k-') + plt.plot([4096, 4096], [-corner_start, corner_start], 'k-') + + # Add horizontal lines from at Y=-5120 and Y=5120, each from X=-4096 to X=4096 + corner_start = 4096 - 1152 + plt.plot([-corner_start, corner_start], [-5120, -5120], 'k-') + plt.plot([-corner_start, corner_start], [5120, 5120], 'k-') + + # Draw lines representing the corners + # Top left goes from X=-4096, Y=(5120-1152) to X=(-4096+1152), Y=-5120 + plt.plot([-4096, -4096+1152], [5120-1152, 5120], 'k-') + # Top right goes from X=4096, Y=(5120-1152) to X=(4096-1152), Y=-5120 + plt.plot([4096, 4096-1152], [5120-1152, 5120], 'k-') + # Bottom left goes from X=-4096, Y=-5120 to X=(-4096+1152), Y=(-5120+1152) + plt.plot([-4096, -4096+1152], [-5120+1152, -5120], 'k-') + # Bottom right goes from X=4096, Y=-5120 to X=(4096-1152), Y=(-5120+1152) + plt.plot([4096, 4096-1152], [-5120+1152, -5120], 'k-') + + # Goal extends from -893 to +893 in X, and 880 past the goal line in Y, which is at Y=+-5120 + plt.plot([-893, -893], [-5120-880, -5120], 'k-') + plt.plot([893, 893], [-5120-880, -5120], 'k-') + plt.plot([-893, 893], [-5120-880, -5120-880], 'k-') + + plt.plot([-893, -893], [5120, 5120+880], 'k-') + plt.plot([893, 893], [5120, 5120+880], 'k-') + plt.plot([-893, 893], [5120+880, 5120+880], 'k-') + + + # Draw a dotted line across the center of the field at Y=0, make it opaque + plt.plot([-4096, 4096], [0, 0], 'k--', alpha=0.5) + + # Draw the offensive car as a blue triangle + # Car is 200 units long, 100 units wide + # Draw an arrow from center -100 to center +100 units in the direction of the car's yaw + car_length = 200 + car_width = 100 + offensive_x_component = car_length * np.cos(self.offensive_car_state.physics.rotation.yaw) + offensive_y_component = car_width * np.sin(self.offensive_car_state.physics.rotation.yaw) + offensive_arrow_x_start = self.offensive_car_state.physics.location.x + offensive_arrow_y_start = self.offensive_car_state.physics.location.y + + plt.arrow(offensive_arrow_x_start, + offensive_arrow_y_start, + offensive_x_component, + offensive_y_component, + head_width=200, head_length=400, fc='b', ec='b', length_includes_head=True) + + defensive_x_component = car_length * np.cos(self.defensive_car_state.physics.rotation.yaw) + defensive_y_component = car_width * np.sin(self.defensive_car_state.physics.rotation.yaw) + defensive_arrow_x_start = self.defensive_car_state.physics.location.x + defensive_arrow_y_start = self.defensive_car_state.physics.location.y + plt.arrow(defensive_arrow_x_start, + defensive_arrow_y_start, + defensive_x_component, + defensive_y_component, + head_width=200, head_length=400, fc='r', ec='r', length_includes_head=True) + + # plt.plot(self.offensive_car_state.physics.location.x, self.offensive_car_state.physics.location.y, 'bo', markersize=10) + # Draw the defensive car as a red triangle + # plt.plot(self.defensive_car_state.physics.location.x, self.defensive_car_state.physics.location.y, 'ro', markersize=10) + + # Draw the ball as a gray circle + plt.plot(self.ball_state.physics.location.x, self.ball_state.physics.location.y, 'ko', markersize=10) + + # Draw the offensive car's velocity vector + plt.arrow(self.offensive_car_state.physics.location.x, self.offensive_car_state.physics.location.y, + self.offensive_car_state.physics.velocity.x, self.offensive_car_state.physics.velocity.y, + head_width=50, head_length=50, fc='b', ec='b') + + # Draw the defensive car's velocity vector + plt.arrow(self.defensive_car_state.physics.location.x, self.defensive_car_state.physics.location.y, + self.defensive_car_state.physics.velocity.x, self.defensive_car_state.physics.velocity.y, + head_width=50, head_length=50, fc='r', ec='r') + + # Draw the ball's velocity vector + plt.arrow(self.ball_state.physics.location.x, self.ball_state.physics.location.y, + self.ball_state.physics.velocity.x, self.ball_state.physics.velocity.y, + head_width=50, head_length=50, fc='k', ec='k') + + # Enforce same scale on both axes + ax = plt.gca() + ax.get_xaxis().get_major_formatter().set_scientific(False) + ax.get_yaxis().get_major_formatter().set_scientific(False) + plt.axis('equal') + + plt.show() + + def __setup_possession_offense(self, y_location): + self.play_yaw, play_yaw_mir = utils.get_play_yaw() + + # Add a small random angle to the yaw of each car + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + ball_velocity = utils.get_velocity_from_yaw(self.play_yaw, min_velocity=800, max_velocity=1200) + + offensive_x_location = utils.random_between(-2000, 2000) + offensive_y_location = y_location + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + # Ball should be ~600 units "in front" of offensive car, with 200 variance in either direction + ball_offset = 600 + ball_x_location = offensive_x_location + (ball_offset * np.cos(offensive_car_yaw)) + utils.random_between(-100, 100) + ball_y_location = offensive_y_location + (ball_offset * np.sin(offensive_car_yaw)) + utils.random_between(-100, 100) + + ball_z_location = 93 + utils.random_between(0, 200) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + def __setup_backpass_offense(self): + # Mostly the same as breakout, but ball is heading toward the offensive car + self.__setup_possession_offense(y_location=utils.random_between(2000, 3000)) + + # Ball should be ~600 units "in front" of offensive car, with 200 variance in either direction + ball_offset = 3000 + ball_x_location = self.offensive_car_state.physics.location.x + (ball_offset * np.cos(self.offensive_car_state.physics.rotation.yaw)) + utils.random_between(-100, 100) + ball_y_location = self.offensive_car_state.physics.location.y + (ball_offset * np.sin(self.offensive_car_state.physics.rotation.yaw)) + utils.random_between(-100, 100) + + ball_z_location = 93 + utils.random_between(0, 200) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # Ball should be heading in front of the offensive car + # calculate 1500 total units in the direction the offensive car is facing + x_component = 0 * np.cos(self.offensive_car_state.physics.rotation.yaw) + y_component = 0 * np.sin(self.offensive_car_state.physics.rotation.yaw) + ball_target_x_location = self.offensive_car_state.physics.location.x + x_component + ball_target_y_location = self.offensive_car_state.physics.location.y + y_component + delta_x = ball_target_x_location - ball_x_location + delta_y = ball_target_y_location - ball_y_location + velocity_magnitude = utils.random_between(0.4, 0.5) + + ball_velocity = Vector3(delta_x*velocity_magnitude, delta_y*velocity_magnitude, utils.random_between(-300, 300)) + + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + def __setup_lob_on_goal_offense(self): + # Yaw is going to be toward the goal, that's going to be 1.5pi + self.play_yaw = 1.5*np.pi + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + offensive_car_x = utils.random_between(-500, 500) + offensive_car_y = utils.random_between(-1000, 1000) + offensive_car_position = Vector3(offensive_car_x, offensive_car_y, 17) + + # Ball should be ahead of offensive car, flying toward back wall + ball_x_location = offensive_car_x + ball_y_location = offensive_car_y + 1000 + ball_z_location = 93 + utils.random_between(1000, 1600) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # X should be opposite direction of starting position + ball_velocity_x = utils.random_between(400, 500) * (ball_x_location > 0) + ball_velocity_y = utils.random_between(-3000, -2000) + ball_velocity_z = utils.random_between(0, 300) + ball_velocity = Vector3(ball_velocity_x, ball_velocity_y, ball_velocity_z) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + + + def __setup_carry_offense(self): + # Mostly same as possession, but ball starts on top of the car + self.play_yaw, play_yaw_mir = utils.get_play_yaw() + + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + ball_velocity = utils.get_velocity_from_yaw(self.play_yaw, min_velocity=800, max_velocity=1200) + + offensive_x_location = utils.random_between(-2000, 2000) + offensive_y_location = utils.random_between(-2500, 2500) + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + ball_position = Vector3(offensive_x_location, offensive_y_location - 200, 400) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + def __setup_backcorner_breakout_offense(self): + # Play yaw is going to be slightly toward the side wall but mostly toward the net + self.play_yaw = utils.random_between(0.5, 1.5) + + # Offensive car should be not too far from the side wall + offensive_x_location = utils.SIDE_WALL - utils.random_between(500, 1500) + + # Sidewall setups should be pretty far from the goal + offensive_y_location = utils.random_between(0, 2500) + + # Add a small random angle to the yaw of each car + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=1000, max_velocity=1500) + ball_velocity = utils.get_velocity_from_yaw(self.play_yaw, min_velocity=1000, max_velocity=1500) + + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + # Ball should be ~600 units "in front" of offensive car, with 200 variance in either direction + ball_offset = 600 + ball_x_location = offensive_x_location + (ball_offset * np.cos(offensive_car_yaw)) + utils.random_between(-100, 100) + ball_y_location = offensive_y_location + (ball_offset * np.sin(offensive_car_yaw)) + utils.random_between(-100, 100) + + ball_z_location = 93 + utils.random_between(0, 30) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_offense_x() + + def __setup_sidewall_breakout_offense(self): + # Play yaw is going to be slightly toward the side wall but mostly toward the net + self.play_yaw = utils.random_between(-0.5, -1.5) + + # Offensive car should be not too far from the side wall + offensive_x_location = utils.SIDE_WALL - utils.random_between(500, 1500) + + # Sidewall setups should be pretty far from the goal + offensive_y_location = utils.random_between(0, 2500) + + # Add a small random angle to the yaw of each car + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=1000, max_velocity=1500) + ball_velocity = utils.get_velocity_from_yaw(self.play_yaw, min_velocity=1000, max_velocity=1500) + + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + # Ball should be ~600 units "in front" of offensive car, with 200 variance in either direction + ball_offset = 600 + ball_x_location = offensive_x_location + (ball_offset * np.cos(offensive_car_yaw)) + utils.random_between(-100, 100) + ball_y_location = offensive_y_location + (ball_offset * np.sin(offensive_car_yaw)) + utils.random_between(-100, 100) + + ball_z_location = 93 + utils.random_between(0, 30) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_offense_x() + + def __setup_corner_offense(self): + # Offensive car starts heading toward the corner + # This will be ~1000 units from the back wall, ~500 units from the side wall + # X side should be randomized + offensive_x_location = utils.random_between(utils.SIDE_WALL - 500, utils.SIDE_WALL - 1000) + offensive_y_location = utils.random_between(utils.BACK_WALL + 1500, utils.BACK_WALL + 2500) + + offensive_x_target = utils.SIDE_WALL - 2000 + offensive_y_target = utils.BACK_WALL + 500 + + # Yaw should be facing halfway between the corner boost and back post + offensive_car_yaw = np.arctan2(offensive_y_target - offensive_y_location, offensive_x_target - offensive_x_location) + self.play_yaw = offensive_car_yaw + + # Velocity should be toward the existing yaw + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + # Ball should be ~600 units "in front" of offensive car, with 200 variance in either direction + ball_offset = 600 + ball_x_location = offensive_x_location + (ball_offset * np.cos(offensive_car_yaw)) + utils.random_between(-100, 100) + ball_y_location = offensive_y_location + (ball_offset * np.sin(offensive_car_yaw)) + utils.random_between(-100, 100) + + ball_z_location = 93 + utils.random_between(0, 200) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + ball_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_offense_x() + + def __setup_pass_offense(self): + self.play_yaw, play_yaw_mir = utils.get_play_yaw() + + # Add a small random angle to the yaw of each car + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + + # Get the starting velocity from the yaw + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, 800, 1200) + + # Get the starting position of each car + # Want to randomize between: + # - X: -2000 to 2000 + # - Y: -2500 to 0 + offensive_x_location = utils.random_between(-2000, 2000) + offensive_y_location = utils.random_between(-1000, 1500) + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + # Ball should start from the wall on the opposite X side as the offensive car + if offensive_x_location < 0: + ball_x_location = 3500 + else: + ball_x_location = -3500 + + # Ball should start close to the goal + ball_y_location = utils.random_between(-4500, -3500) + ball_z_location = 93 + utils.random_between(0, 2000) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # Ball should be heading in front of the offensive car + # calculate 1500 total units in the direction the offensive car is facing + x_component = 1500 * np.cos(offensive_car_yaw) + y_component = 1500 * np.sin(offensive_car_yaw) + ball_target_x_location = offensive_x_location + x_component + ball_target_y_location = offensive_y_location + y_component + delta_x = ball_target_x_location - ball_x_location + delta_y = ball_target_y_location - ball_y_location + velocity_magnitude = utils.random_between(0.4, 0.5) + + # Cap Y velocity component, or else it goes past the offensive car sometimes + ball_velocity = Vector3(delta_x*velocity_magnitude, min(delta_y*velocity_magnitude, 750), utils.random_between(0, 300)) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + def __setup_sidewall_offense(self): + # Play yaw is going to be slightly toward the net but mostly toward the side wall + self.play_yaw = utils.random_between(5.8, 6.2) + + # Offensive car should be 1000 units from the side wall + offensive_x_location = utils.SIDE_WALL - utils.random_between(1500, 2500) + + # Sidewall setups shouldn't be too close to the goal + offensive_y_location = utils.random_between(-1500, 1500) + + # Add a small random angle to the yaw of each car + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + ball_velocity = utils.get_velocity_from_yaw(self.play_yaw, min_velocity=1500, max_velocity=2000) + + offensive_car_position = Vector3(offensive_x_location, offensive_y_location, 17) + + # Ball should be ~600 units "in front" of offensive car, with 200 variance in either direction + ball_offset = 600 + ball_x_location = offensive_x_location + (ball_offset * np.cos(offensive_car_yaw)) + utils.random_between(-100, 100) + ball_y_location = offensive_y_location + (ball_offset * np.sin(offensive_car_yaw)) + utils.random_between(-100, 100) + + ball_z_location = 93 + utils.random_between(0, 30) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_offense_x() + + def __setup_backboard_pass_offense(self): + # Yaw is going to be toward the goal, that's going to be 1.5pi + self.play_yaw = 1.5*np.pi + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + offensive_car_x = utils.random_between(-500, 500) + offensive_car_y = utils.random_between(-1000, 1000) + offensive_car_position = Vector3(offensive_car_x, offensive_car_y, 17) + + # Ball starts close to back wall, flying toward back wall + ball_x_location = utils.SIDE_WALL - utils.random_between(1000, 2000) + ball_y_location = utils.BACK_WALL + utils.random_between(2000, 3000) + ball_z_location = 93 + utils.random_between(500, 1200) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # X should be opposite direction of starting position + ball_velocity_x = utils.random_between(400, 500) * (ball_x_location > 0) + ball_velocity_y = utils.random_between(-3000, -2000) + ball_velocity_z = utils.random_between(0, 300) + ball_velocity = Vector3(ball_velocity_x, ball_velocity_y, ball_velocity_z) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_offense_x() + + def __setup_backwall_bounce_offense(self): + # Yaw is going to be toward the goal, that's going to be 1.5pi + self.play_yaw = 1.5*np.pi + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + offensive_car_x = utils.random_between(-500, 500) + offensive_car_y = utils.random_between(-1000, 1000) + offensive_car_position = Vector3(offensive_car_x, offensive_car_y, 17) + + # Ball should be ahead of offensive car, flying toward back wall + ball_x_location = offensive_car_x + ball_y_location = offensive_car_y - 1000 + ball_z_location = 93 + utils.random_between(1000, 2000) + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # X should be opposite direction of starting position + ball_velocity_x = utils.random_between(400, 500) * (ball_x_location > 0) + ball_velocity_y = utils.random_between(-2000, -3000) + ball_velocity_z = utils.random_between(300, 500) + ball_velocity = Vector3(ball_velocity_x, ball_velocity_y, ball_velocity_z) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + def __setup_side_backboard_pass_offense(self): + """ + Setup a side backboard pass scenario where: + - Ball starts near one side wall, bounces off the backboard to the side of the net + - Offensive car starts on the same side as the ball, pointing toward the opponent's goal + """ + # Offensive car yaw is toward the goal (1.5*pi) + self.play_yaw = 1.5*np.pi + offensive_car_yaw = self.play_yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + + # Offensive car velocity toward the goal + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=800, max_velocity=1200) + + # Randomly choose which side the ball starts on (left or right) + ball_side = np.random.choice([-1, 1]) # -1 for left side, 1 for right side + + # Ball starts near the side wall on one side + ball_x_location = ball_side * (utils.SIDE_WALL - utils.random_between(200, 800)) + ball_y_location = utils.BACK_WALL + utils.random_between(2500, 3500) # Near the back wall + ball_z_location = 93 + utils.random_between(300, 800) # Lower height for more realistic backboard pass + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # Ball velocity: heading toward the backboard, but angled to bounce to the opposite side + # The ball should bounce off the backboard and head toward the side of the net opposite from where it started + ball_velocity_x = -ball_side * utils.random_between(1200, 1600) # Toward opposite side - increased speed + ball_velocity_y = utils.random_between(-2500, -3000) # Toward the backboard/goal - increased speed + ball_velocity_z = utils.random_between(-50, 300) # Slight vertical component - increased range + ball_velocity = Vector3(ball_velocity_x, ball_velocity_y, ball_velocity_z) + + # Offensive car starts on the same side as the ball + offensive_car_x = ball_side * utils.random_between(1000, 2000) # Same side as ball + offensive_car_y = utils.random_between(0, 1000) # Positioned to follow up on the pass + offensive_car_position = Vector3(offensive_car_x, offensive_car_y, 17) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + def __setup_over_shoulder_offense(self): + """ + Setup an over-the-shoulder scenario where: + - Offensive car is positioned in the offensive half, facing toward the goal + - Ball comes from behind/above the car (from the defensive end) + - Ball trajectory goes "over the shoulder" of the offensive car + - Car needs to turn around or adjust to receive/intercept the ball + """ + # Offensive car yaw is toward the goal (1.5*pi) + self.play_yaw = 1.5*np.pi + offensive_car_yaw = self.play_yaw + utils.random_between(-0.2*np.pi, 0.2*np.pi) + + # Offensive car velocity toward the goal + offensive_car_velocity = utils.get_velocity_from_yaw(offensive_car_yaw, min_velocity=1000, max_velocity=1400) + + # Offensive car should be roughly in the middle, but distinctly on one X side + x_side = np.random.choice([-1, 1]) + offensive_car_x = x_side * utils.random_between(2000, 2500) + offensive_car_y = utils.random_between(-500, 1500) + offensive_car_position = Vector3(offensive_car_x, offensive_car_y, 17) + + # Ball starts from behind the car (defensive end), elevated + # Position it "over the shoulder" - behind and to one side + # Ball should be on the opposite X side as the offensive car + shoulder_side = -x_side + + # Ball starts behind the car and elevated + # Ball should be distinctly on the opposite X side as the offensive car + ball_x_location = offensive_car_x + shoulder_side * utils.random_between(1500, 2000) # To the side + ball_y_location = offensive_car_y + utils.random_between(1500, 3000) # Behind the car (toward defensive end) + ball_z_location = 93 + utils.random_between(400, 1200) # Elevated + ball_position = Vector3(ball_x_location, ball_y_location, ball_z_location) + + # Ball velocity: should be roughly going toward the goal, but slightly toward the side of the offensive car + target_x = offensive_car_x + shoulder_side * utils.random_between(500, 1000) # Opposite side from start + target_y = offensive_car_y - utils.random_between(800, 1500) # In front of the car + + # Calculate velocity to reach the target area + delta_x = target_x - ball_x_location + delta_y = target_y - ball_y_location + + # Normalize and scale the horizontal velocity + horizontal_distance = np.sqrt(delta_x**2 + delta_y**2) + velocity_magnitude = utils.random_between(2800, 2900) + + ball_velocity_x = (delta_x / horizontal_distance) * velocity_magnitude + ball_velocity_y = (delta_y / horizontal_distance) * velocity_magnitude + ball_velocity_z = utils.random_between(100, 400) # Slight downward trajectory + + ball_velocity = Vector3(ball_velocity_x, ball_velocity_y, ball_velocity_z) + + self.offensive_car_state = CarState(boost_amount=100, physics=Physics(location=offensive_car_position, rotation=Rotator(yaw=offensive_car_yaw, pitch=0, roll=0), velocity=offensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + self.ball_state = BallState(Physics(location=ball_position, velocity=ball_velocity)) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_offense_x() + + def __setup_shadow_defense(self, distance_from_offensive_car): + ''' + Setup the shadow defense scenario + Shadow defense is based off of offensive car stats + ''' + + # Add a small random angle to the yaw of each car + defensive_car_yaw = self.offensive_car_state.physics.rotation.yaw + utils.random_between(-0.1*np.pi, 0.1*np.pi) + + # Get the starting velocity from the yaw + defensive_car_velocity = utils.get_velocity_from_yaw(defensive_car_yaw, min_velocity=800, max_velocity=1200) + + # Defensive location should be +-300 X units away from offensive car, and given distance away towards the goal + defensive_x_location = utils.random_between(self.offensive_car_state.physics.location.x - 300, self.offensive_car_state.physics.location.x + 300) + defensive_y_location = self.offensive_car_state.physics.location.y - distance_from_offensive_car + + defensive_car_position = Vector3(defensive_x_location, defensive_y_location, 17) + + self.defensive_car_state = CarState(boost_amount=100, physics=Physics(location=defensive_car_position, rotation=Rotator(yaw=defensive_car_yaw, pitch=0, roll=0), velocity=defensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + def __setup_net_defense(self): + # Car is stationary + defensive_car_velocity = Vector3(0, 0, 0) + + # Let's do -200 to 200 range for X, Y is -5600 (or +5600 if mirrored) + defensive_x_location = utils.random_between(-200, 200) + defensive_y_location = -5600 + + defensive_car_position = Vector3(defensive_x_location, defensive_y_location, 27) + + # In net mode, defensive car yaw should be facing the offensive car + # Get the difference between the defensive car and the offensive car + defensive_car_x = defensive_x_location + defensive_car_y = defensive_y_location + offensive_car_x = self.offensive_car_state.physics.location.x + offensive_car_y = self.offensive_car_state.physics.location.y + radians_to_offensive_car = np.arctan2(offensive_car_y - defensive_car_y, offensive_car_x - defensive_car_x) + defensive_car_yaw = radians_to_offensive_car + + self.defensive_car_state = CarState(boost_amount=100, physics=Physics(location=defensive_car_position, rotation=Rotator(yaw=defensive_car_yaw, pitch=0, roll=0), velocity=defensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + def __setup_corner_defense(self): + # Defensive car should be heading around the defensive corner + + # This will be ~1000 units from the back wall, ~500 units from the side wall + # X side should be randomized + defensive_x_location = utils.random_between(utils.SIDE_WALL - 500, utils.SIDE_WALL - 1000) + defensive_y_location = utils.random_between(utils.BACK_WALL + 1500, utils.BACK_WALL + 2500) + + defensive_x_target = utils.SIDE_WALL - 2000 + defensive_y_target = utils.BACK_WALL + 500 + + # Yaw should be facing halfway between the corner boost and back post + defensive_car_yaw = np.arctan2(defensive_y_target - defensive_y_location, defensive_x_target - defensive_x_location) + + # Velocity should be toward the existing yaw + defensive_car_velocity = utils.get_velocity_from_yaw(defensive_car_yaw, min_velocity=800, max_velocity=1200) + defensive_car_position = Vector3(defensive_x_location, defensive_y_location, 17) + + # Car is stationary + self.defensive_car_state = CarState(boost_amount=100, physics=Physics(location=defensive_car_position, rotation=Rotator(yaw=defensive_car_yaw, pitch=0, roll=0), velocity=defensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + # Flip X position, velocity, and yaw randomly 50% of the time + self.__randomly_mirror_defensive_x() + + def __setup_recovering_defense(self): + # Defensive car should be "past" the offensive car and ball, with little chance to make it back in time + # Should be heading toward the opposite net + defensive_car_yaw = 1.5*np.pi + defensive_car_velocity = utils.get_velocity_from_yaw(defensive_car_yaw, min_velocity=100, max_velocity=300) + + # Add some z variance + defensive_car_z_location = utils.random_between(100, 300) + + # Y will be past the offensive car + defensive_car_y_location = self.offensive_car_state.physics.location.y + utils.random_between(500, 1000) + + # X will be toward the middle relative to the offensive car + defensive_car_x_location = self.offensive_car_state.physics.location.x / 2 + + defensive_car_position = Vector3(defensive_car_x_location, defensive_car_y_location, defensive_car_z_location) + + self.defensive_car_state = CarState(boost_amount=100, physics=Physics(location=defensive_car_position, rotation=Rotator(yaw=defensive_car_yaw, pitch=0, roll=0), velocity=defensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + def __setup_front_intercept_defense(self): + """ + Setup a front intercept defense scenario where: + - Defensive car starts 2000 Y units in front of the offensive car + - ± 500 units X away from the offensive car + - Facing the offensive car + """ + # Calculate defensive car position relative to offensive car + offensive_car_x = self.offensive_car_state.physics.location.x + offensive_car_y = self.offensive_car_state.physics.location.y + + # Position defensive car 2000 units in front (toward the goal) of offensive car + defensive_car_y = offensive_car_y - 3000 + + # Position defensive car ± 500 units X away from offensive car + x_offset = utils.random_between(-500, 500) + defensive_car_x = offensive_car_x + x_offset + + defensive_car_position = Vector3(defensive_car_x, defensive_car_y, 17) + + # Calculate yaw to face the offensive car + delta_x = offensive_car_x - defensive_car_x + delta_y = offensive_car_y - defensive_car_y + defensive_car_yaw = np.arctan2(delta_y, delta_x) + + # Set velocity toward the offensive car with some randomization + defensive_car_velocity = utils.get_velocity_from_yaw(defensive_car_yaw, min_velocity=800, max_velocity=1200) + + self.defensive_car_state = CarState(boost_amount=100, physics=Physics(location=defensive_car_position, rotation=Rotator(yaw=defensive_car_yaw, pitch=0, roll=0), velocity=defensive_car_velocity, + angular_velocity=Vector3(0, 0, 0))) + + + def __randomly_mirror_offense_x(self): + if np.random.random() < 0.5: + self.offensive_car_state.physics.location.x = -self.offensive_car_state.physics.location.x + self.offensive_car_state.physics.velocity.x = -self.offensive_car_state.physics.velocity.x + self.offensive_car_state.physics.rotation.yaw = (2*np.pi-self.offensive_car_state.physics.rotation.yaw) + np.pi + self.ball_state.physics.location.x = -self.ball_state.physics.location.x + self.ball_state.physics.velocity.x = -self.ball_state.physics.velocity.x + + def __randomly_mirror_defensive_x(self): + if np.random.random() < 0.5: + self.defensive_car_state.physics.location.x = -self.defensive_car_state.physics.location.x + self.defensive_car_state.physics.velocity.x = -self.defensive_car_state.physics.velocity.x + self.defensive_car_state.physics.rotation.yaw = (2*np.pi-self.defensive_car_state.physics.rotation.yaw) + np.pi diff --git a/RLBotPack/RLDojo/Dojo/simulation.py b/RLBotPack/RLDojo/Dojo/simulation.py new file mode 100644 index 00000000..3d9bf0d4 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/simulation.py @@ -0,0 +1,12 @@ +import matplotlib.pyplot as plt +from scenario import Scenario, OffensiveMode, DefensiveMode + +# You can use this __name__ == '__main__' thing to ensure that the script doesn't start accidentally if you +# merely reference its module from somewhere +if __name__ == "__main__": + # scenario = Scenario(GameMode.SHADOW) + scenario = Scenario(OffensiveMode.PASS, DefensiveMode.CORNER) + scenario.Draw() + scenario.Mirror() + scenario.Draw() + plt.show() diff --git a/RLBotPack/RLDojo/Dojo/ui_renderer.py b/RLBotPack/RLDojo/Dojo/ui_renderer.py new file mode 100644 index 00000000..521dc158 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/ui_renderer.py @@ -0,0 +1,122 @@ +from typing import Optional +from game_state import DojoGameState, GymMode, ScenarioPhase, CUSTOM_MODES +from constants import ( + SCORE_BOX_START_X, SCORE_BOX_START_Y, SCORE_BOX_WIDTH, SCORE_BOX_HEIGHT, + CUSTOM_MODE_MENU_START_X, CUSTOM_MODE_MENU_START_Y, CUSTOM_MODE_MENU_WIDTH, CUSTOM_MODE_MENU_HEIGHT, + CONTROLS_MENU_WIDTH, CONTROLS_MENU_HEIGHT +) +import utils + + +class UIRenderer: + """Handles all UI rendering for the Dojo application""" + + def __init__(self, renderer, game_state: DojoGameState): + self.renderer = renderer + self.game_state = game_state + + def render_main_ui(self): + """Render the main UI elements (score, time, etc.)""" + if self.game_state.game_phase in [ScenarioPhase.MENU, *CUSTOM_MODES]: + return + + minutes, seconds = self.game_state.get_time_since_start() + seconds_str = f"{seconds:02d}" + + # Prepare text content + text = "Welcome to the Dojo. Press 'm' to enter menu" + previous_record = "No record" + + if self.game_state.gym_mode == GymMode.SCENARIO: + scores = f"Human: {self.game_state.human_score} Bot: {self.game_state.bot_score}" + total_score = f"Total: {self.game_state.human_score + self.game_state.bot_score}" + time_since_start = f"Time: {minutes}:{seconds_str}" + offensive_mode_name = f"Offensive Mode: {self.game_state.offensive_mode.name}" + defensive_mode_name = f"Defensive Mode: {self.game_state.defensive_mode.name}" + player_role_name = "offense" if self.game_state.player_offense else "defense" + player_role_string = f"Player Role: {player_role_name}" + previous_record = "" + game_phase_name = f"Game Phase: {self.game_state.game_phase.name}" + elif self.game_state.gym_mode == GymMode.RACE: + scores = f"Completed: {self.game_state.human_score}" + total_score = f"Out of: {self.game_state.num_trials}" + time_since_start = f"Time: {minutes}:{seconds_str}" + previous_record_data = self.game_state.get_previous_record() + if previous_record_data: + prev_minutes = int(previous_record_data // 60) + prev_seconds = int(previous_record_data % 60) + previous_record = f"Previous Record: {prev_minutes}:{prev_seconds:02d}" + + # Render UI elements + self.renderer.begin_rendering() + + # Main instruction text + self.renderer.draw_string_2d(20, 50, 1, 1, text, self.renderer.yellow()) + + # Score box content + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 10, + 1, 1, scores, self.renderer.white() + ) + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 40, + 1, 1, total_score, self.renderer.white() + ) + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 70, + 1, 1, time_since_start, self.renderer.white() + ) + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 100, + 1, 1, previous_record, self.renderer.white() + ) + if self.game_state.gym_mode == GymMode.SCENARIO: + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 130, + 1, 1, offensive_mode_name, self.renderer.white() + ) + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 160, + 1, 1, defensive_mode_name, self.renderer.white() + ) + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 190, + 1, 1, player_role_string, self.renderer.white() + ) + self.renderer.draw_string_2d( + SCORE_BOX_START_X + 10, SCORE_BOX_START_Y + 220, + 1, 1, game_phase_name, self.renderer.white() + ) + self.renderer.end_rendering() + + + + def render_velocity_vectors(self, rlbot_game_state): + """Render velocity vectors for all objects in custom mode""" + if not rlbot_game_state: + return + + from game_state import CarIndex + + # Human car velocity vector + if CarIndex.HUMAN.value in rlbot_game_state.cars: + human_car = rlbot_game_state.cars[CarIndex.HUMAN.value] + human_start = utils.vector3_to_list(human_car.physics.location) + human_end_vector = utils.add_vector3(human_car.physics.location, human_car.physics.velocity) + human_end = utils.vector3_to_list(human_end_vector) + self.renderer.draw_line_3d(human_start, human_end, self.renderer.white()) + + # Ball velocity vector + if rlbot_game_state.ball: + ball_start = utils.vector3_to_list(rlbot_game_state.ball.physics.location) + ball_end_vector = utils.add_vector3(rlbot_game_state.ball.physics.location, rlbot_game_state.ball.physics.velocity) + ball_end = utils.vector3_to_list(ball_end_vector) + self.renderer.draw_line_3d(ball_start, ball_end, self.renderer.white()) + + # Bot car velocity vector + if CarIndex.BOT.value in rlbot_game_state.cars: + bot_car = rlbot_game_state.cars[CarIndex.BOT.value] + bot_start = utils.vector3_to_list(bot_car.physics.location) + bot_end_vector = utils.add_vector3(bot_car.physics.location, bot_car.physics.velocity) + bot_end = utils.vector3_to_list(bot_end_vector) + self.renderer.draw_line_3d(bot_start, bot_end, self.renderer.white()) diff --git a/RLBotPack/RLDojo/Dojo/utils.py b/RLBotPack/RLDojo/Dojo/utils.py new file mode 100644 index 00000000..cf343c73 --- /dev/null +++ b/RLBotPack/RLDojo/Dojo/utils.py @@ -0,0 +1,108 @@ +import numpy as np +from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator, GameInfoState + +SIDE_WALL=4096 +# From perspective of default scenario - blue team defending +BLUE_WALL=-5120 +ORANGE_WALL=5120 + +BACK_WALL=BLUE_WALL + +def hasattrdeep(obj, *names): + for name in names: + if not hasattr(obj, name): + return False + obj = getattr(obj, name) + return True + + +def add_vector3(vector1, vector2): + return Vector3(vector1.x + vector2.x, vector1.y + vector2.y, vector1.z + vector2.z) + +def vector3_to_list(vector3): + return [vector3.x, vector3.y, vector3.z] + + +def get_play_yaw(): + rand1 = np.random.random() + if rand1 < 1/7: + play_yaw = -np.pi * 0.25 + elif rand1 < 2/7: + play_yaw = -np.pi * 0.375 + elif rand1 < 5/7: + play_yaw = -np.pi * 0.5 + elif rand1 < 6/7: + play_yaw = -np.pi * 0.625 + elif rand1 < 7/7: + play_yaw = -np.pi * 0.75 + # 50% parallel/mirrored yaw compared to other team + if np.random.random() < 0.5: + play_yaw_mir = play_yaw-np.pi + else: + play_yaw_mir = -play_yaw + return play_yaw, play_yaw_mir + +def random_between(min_value, max_value): + return min_value + np.random.random() * (max_value - min_value) + +def get_velocity_from_yaw(yaw, min_velocity, max_velocity): + # yaw is in radians, use this to get the ratio of x/y velocity + # X = cos(yaw) + # Y = sin(yaw) + # Z = 0 + velocity_factor = random_between(min_velocity, max_velocity) + velocity_x = velocity_factor * np.cos(yaw) + velocity_y = velocity_factor * np.sin(yaw) + return Vector3(velocity_x, velocity_y, 0) + +# Rotation consists of pitch, yaw, roll +# Yaw is on the x/y plane +# Pitch is radians above/below the x/y plane +# Roll is irrelevant +# We want to convert this to a velocity vector +def get_velocity_from_rotation(rotation, min_velocity, max_velocity): + # Get the yaw from the rotation + yaw = rotation.yaw + # Get the pitch from the rotation + pitch = rotation.pitch + + velocity_factor = random_between(min_velocity, max_velocity) + velocity_x = (velocity_factor * np.cos(yaw)) * np.cos(pitch) + velocity_y = (velocity_factor * np.sin(yaw)) * np.cos(pitch) + velocity_z = velocity_factor * np.sin(pitch) + return Vector3(velocity_x, velocity_y, velocity_z) + +def sanity_check_objects(objects): + '''If any of the objects have been placed outside of the map, move them to the nearest edge of the map''' + # Back wall is biased toward the negative end, which makes this math a little fucky + for object in objects: + if object.physics.location.x < -SIDE_WALL: + object.physics.location.x = -(SIDE_WALL-100) + elif object.physics.location.x > SIDE_WALL: + object.physics.location.x = SIDE_WALL-100 + if object.physics.location.y > -BACK_WALL: + # Make an exception if in the goal, which is between -/+893 x + if not (object.physics.location.x > -893 and object.physics.location.x < 893): + object.physics.location.y = -(BACK_WALL+100) + elif object.physics.location.y < BACK_WALL: + # Make an exception if in the goal, which is between -/+893 x + if not (object.physics.location.x > -893 and object.physics.location.x < 893): + object.physics.location.y = BACK_WALL+100 + + # Also account for corners, which is going to suck + # Corners start 1152 units in from the side walls and back walls + # That translates to 4096 - 1152 = 2944 in X axis + # And 5120 - 1152 = 3968 in Y axis + # So if the object is outside of both of those, move it inside + if object.physics.location.x > 2944 and object.physics.location.y > 3968: + object.physics.location.x = 2944 + object.physics.location.y = 3968 + elif object.physics.location.x < -2944 and object.physics.location.y > 3968: + object.physics.location.x = -2944 + object.physics.location.y = 3968 + elif object.physics.location.x > 2944 and object.physics.location.y < -3968: + object.physics.location.x = 2944 + object.physics.location.y = -3968 + elif object.physics.location.x < -2944 and object.physics.location.y < -3968: + object.physics.location.x = -2944 + object.physics.location.y = -3968 diff --git a/RLBotPack/RLDojo/README.md b/RLBotPack/RLDojo/README.md new file mode 100644 index 00000000..55dca7b2 --- /dev/null +++ b/RLBotPack/RLDojo/README.md @@ -0,0 +1,82 @@ +| [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/4bHnGBm2Dbw/0.jpg)](https://www.youtube.com/watch?v=4bHnGBm2Dbw) | +|:--:| +| *Demo video of RLDojo* | + +# Overview / TL;DR + +Free play, training packs, and custom maps are great - but winning real games requires reading, reacting to, and outplaying your opponents. That’s why, in real sports, practicing specific scenarios against other players is a critical component of training. + +This type of training is sorely missing in Rocket League, so I made RLDojo to let you practice customizable drills against RLBot bots (like Nexto) for the first time. + +# Features + +### Preset Scenarios + +RLDojo comes with a handful of preset / “hardcoded” offensive and defensive setups or “scenarios” which comprise a situation that the player can either play out as the offensive car or the defensive car. + +These presets are designed to be mixed and matched in order to cover a large variety of game-like situations in which you would need to either outplay a defender or defend an attacker. They each have a small amount of randomness baked in to cover more flavors of similar scenarios in game. + +These scenarios are timed (to 7 seconds by default), where a point will be awarded to the defender if the attacker does not score before the timeout. + +### Custom Scenario Creator + +In addition to the preset scenarios, I also built a way for you to create your own scenarios, by manually setting the physics of the cars and ball to start a scenario, similar to making training packs (but more flexible, as you can change the rotation of cars and set their velocity). + +### Playlist Mode + +Playlists allow you to combine multiple types of scenarios (preset or custom) into… well, playlists. This allows you to group multiple scenarios by theme, e.g. maybe you want to work on a few different types of shadow defense or ground-based offense. + +RLDojo comes with a few pre-defined playlists for you to try out, or you can create your own. + +Special shoutout: `Open Goal` mode is one of my favorites, which emulates a defender chasing you down as you have a free shot on goal from a variety of positions. As we all know, the open goal is the hardest shot in the game, and this actually does a pretty good job feeling like the real things. + +### Race Mode + +This mode is a surprise fan-favorite (okay fine there’s just 1 fan - the only other RLDojo alpha user - my buddy Mike). + +In Race Mode, the ball will spawn in a random location (seeded so that the sequence is always the same), and the player tries to get to the ball as fast as possible. The ball will spawn elsewhere once touched, which will repeat 100 times (number of trials is selectable). + +Your fastest time will be recorded and displayed on future attempts, and it is insanely addicting to try to get this time lower and lower. + +While initially created just for fun, it turns out this is an incredible exercise for practical efficiency of movement. Rings maps are great, but Race Mode is much more useful for in-game movement imo. + +# Background + +As someone who got pretty serious about ranking up a few years ago, I’ve tried out just about every training tool that exists, from training packs to dozens of Bakkesmod plugins and custom maps. + +I’ve also gone deep down the rabbithole of content tailored around improving gamesense (shoutout Flakes and Aircharged), and became obsessed with winning games through an emphasis on defense and decision-making. + +Trying to improve at these skills made it glaringly obvious that Rocket League’s existing suite of tools are missing an entire dimension of practice: drilling scenarios repeatedly against other players. + +For example: + +- How can you practice shadow defense without an opponent attacking? +- How can you get better at taking 50/50s without someone on the other side of the ball? +- How can you react to and save a redirecting shot, if training packs can only send a ball from one point? + +The goal of RLDojo is to make these scenarios (and infinitely more) possible to train repeatedly! + +# Installation +Installation guide here: https://www.youtube.com/watch?v=1GbHdYeG1cc + +To get RLDojo up and running: +1. Install RLBot: [rlbot.org](https://rlbot.org/) +2. In RLBotGUI, go to `+Add` -> `Download Bot Pack` (this will download the 'standard' bots) +3. Download the latest RLDojo release: https://github.com/ecolsen7/RLDojo/releases and extract it +4. In RLBotGUI, go to `+Add` -> `Load Folder` and select the RLDojo folder that you just downloaded/created +5. In RLBotGUI, find `Dojo` under the `Scripts` section + - If there is a yellow triangle next to `Dojo`, click it to install any needed packages + - Enable `Dojo` by clicking the toggle +6. In RLBotGUI, click the `Mutators` option at the bottom. Change `Match Length` to "Unlimited", and `Respawn Time` to "Disable Goal Reset" +7. In RLBotGUI, click the `Extras` option at the bottom. Select the following: +8. image +9. Make sure "Human" is on the Blue team, and add any bot (I recommend starting with `Necto`) to the Orange team. +10. Hit `Launch Rocket League and start match`. +11. Have fun! + + +# How much does it cost? + +It’s free! My motivation for making this is that I love this game, and I want to see it and its competitive community thrive. + +If you feel particularly inclined to give back, feel free to follow me on [Twitch](https://www.twitch.tv/smoothrik) and/or [Youtube](https://www.youtube.com/@smooth_rik)! If that’s not enough, you can [buy me a coffee](https://buymeacoffee.com/ecolsen74)