Module botroyale.logic.battle_manager
Home of BattleManager
.
Expand source code Browse git
"""Home of `botroyale.logic.battle_manager.BattleManager`."""
from typing import Optional, Literal
from botroyale.logic.battle import Battle
from botroyale.api.gui import BattleAPI, Tile, Control, ControlMenu
from botroyale.util.time import ping, pong
from botroyale.util import settings
from botroyale.util.hexagon import Hex, Hexagon
from botroyale.logic.state import State
from botroyale.logic import UNIT_COLORS, get_tile_info, get_tile_info_unit
STEP_RATE = settings.get("battle.default_step_rate")
STEP_RATES = settings.get("battle.toggle_step_rates")
LOGIC_DEBUG = settings.get("logging.battle")
MAP_CENTER = Hex(0, 0)
PanelMode = Literal["turns", "timers"]
class BattleManager(Battle, BattleAPI):
"""A GUI interface for `botroyale.logic.battle.Battle`.
Provides methods for parsing, formatting, and displaying information about
the battle, as well as GUI-related controls and display getters.
While it can be used as an extension of `botroyale.logic.battle.Battle`, this class
can behave surprisingly different than the base class. This is because it
keeps track of a replay index, allowing to "look at" past states. Therefore
it is recommended to be familiar with `BattleManager.set_replay_index`.
"""
show_coords = False
step_interval_ms = 1000 / STEP_RATE
def __init__(self, gui_mode: Optional[bool] = False, **kwargs):
"""Initialize the class.
Args:
kwargs: Keyword arguments for `botroyale.logic.battle.Battle.__init__`
gui_mode: If True, will set arguments appropriate for the GUI.
"""
if gui_mode:
kwargs["only_bot_turn_states"] = False
kwargs["enable_logging"] = LOGIC_DEBUG
Battle.__init__(self, **kwargs)
BattleAPI.__init__(self)
self.__replay_index = 0
self.autoplay: bool = False
self.__last_step = ping()
self.unit_colors = [
UNIT_COLORS[bot.COLOR_INDEX % len(UNIT_COLORS)] for bot in self.bots
]
self.unit_sprites = [bot.SPRITE for bot in self.bots]
self.__panel_mode: PanelMode = "turns"
# Replay
def set_replay_index(
self,
index: Optional[int] = None,
index_delta: Optional[int] = None,
apply_vfx: bool = True,
disable_autoplay: bool = True,
):
"""Set the state index of the replay.
Will play missing states until *index* is reached or the game is over.
*index* defaults to the index of the last state in history, unless
*index_delta* is provided in which case it defaults to the current
replay index.
Args:
index: Index of state to go to.
index_delta: Number to add to index.
apply_vfx: Queue vfx of the state we are going to.
disable_autoplay: Disables autoplay.
"""
if index is None:
if index_delta is None:
index = self.history_size - 1
else:
index = self.replay_index
index = index % self.history_size
if index_delta:
index += index_delta
index = max(0, index)
# Play states until we reach the index (and cap index at last state)
if self.history_size <= index:
missing_state_count = index - self.history_size + 1
self.play_states(missing_state_count)
index = min(index, self.history_size - 1)
# Set the index
apply_vfx = apply_vfx and index != self.__replay_index
self.__replay_index = index
if apply_vfx:
self.add_state_vfx(index, redraw_last_steps=True)
self._highlight_current_unit()
if disable_autoplay:
self.autoplay = False
@property
def replay_mode(self) -> bool:
"""Is `BattleManager.replay_index` set to a past state."""
return self.replay_index != self.history_size - 1
@property
def replay_index(self) -> int:
"""The state in history we are looking at.
Methods that use state information will look at the state in
`BattleManager.replay_index` rather than the last state.
"""
return self.__replay_index
@property
def replay_state(self) -> State:
"""The state at index of `BattleManager.replay_index`."""
return self.history[self.replay_index]
def play_all(self, *args, **kwargs):
"""Overrides the parent method in order to set the replay_index.
See: `BattleManager.replay_index`.
"""
super().play_all(*args, **kwargs)
self.set_replay_index()
def set_to_next_round(self, backwards: bool = False):
"""Set the replay to the next "end of round" state (or game over).
If backwards is set, it will search for a previous state.
See: `botroyale.logic.state.State.end_of_round`.
"""
delta = 1 if not backwards else -1
if self.replay_state.end_of_round:
self.set_replay_index(index_delta=1 if not backwards else -1)
while not self.replay_state.end_of_round:
if self.replay_state.game_over and not backwards:
break
self.set_replay_index(index_delta=delta)
def preplay(self):
"""Play the entire battle, then set the replay index to the start.
See: `BattleManager.replay_index`.
"""
self.play_all()
self.flush_vfx()
self.set_replay_index(0)
# Info strings
def get_info_str(self, state_index: Optional[int] = None) -> str:
"""A multiline summary string of the current game state."""
if state_index is None:
state_index = self.replay_index
strs = [
f"{self._get_status_str(state_index)}",
"",
]
if self.__panel_mode == "turns":
strs.extend(
[
f"{self._get_turn_order_str(state_index)}",
"",
f"{self._get_last_action_str(state_index)}",
]
)
elif self.__panel_mode == "timers":
strs.append(f"{self.get_timer_str()}")
else:
raise ValueError(f"Unknown panel mode: {self.__panel_mode}")
return "\n".join(strs)
def get_timer_str(self) -> str:
"""Multiline string of bot calculation times.
Unlike other methods in this class, the result of this method does not
consider `BattleManager.replay_mode`. The times shown are updated to the
latest state in the battle. This is because
`botroyale.logic.battle.Battle.bot_timer` is updated in place on each
new state.
"""
strs = [
" Bot Calculation Times (ms)",
"--------------------------------------",
" Bot Mean Max",
"",
]
if self.replay_mode:
strs.insert(0, "Times are not live!\n\n")
for bot in self.bots:
mean_block_time = self.bot_timer.mean(bot.id)
max_block_time = self.bot_timer.max(bot.id)
strs.append(
"".join(
[
f"{bot.gui_label:<20}",
f'{f"{mean_block_time:,.1f}":>8} ',
f'{f"{max_block_time:,.1f}":>8}',
]
)
)
return "\n".join(strs)
def _get_status_str(self, state_index: int) -> str:
state = self.history[state_index]
autoplay = "Playing" if self.autoplay else "Paused"
status = f"{autoplay} <= {1000 / self.step_interval_ms:.2f} steps/s"
if state.game_over:
winner_str = "draw!"
winner_id = state.winner
if winner_id is not None:
winner = self.bots[winner_id]
winner_str = f"{winner.gui_label} won!"
status = f"GAME OVER : {winner_str}"
return "\n".join(
[
status,
"",
f"Step:{state.step_count:^5}Turn:{state.turn_count:^4}"
f"Round:{state.round_count:^3}RoD: {state.death_radius:>2}",
]
)
def _get_last_action_str(self, state_index: int) -> str:
state = self.history[state_index]
if state.step_count == 0:
last_step_str = "[i]New game[/i]"
last_action_str = "[i]Started new game[/i]"
else:
last_state = self.history[state_index - 1]
last_step_str = f"[i]{self.get_state_str(last_state).capitalize()}[/i]"
if last_state.end_of_round:
last_action_str = f"[i]Started round {state.round_count}[/i]"
else:
last_action_str = f"{state.last_action}"
if not state.is_last_action_legal:
last_action_str = f"[i]ILLEGAL[/i] {last_action_str}"
return "\n".join(
[
f"Last step: {last_step_str}",
f"Last action: {last_action_str}",
]
)
def _get_turn_order_str(self, state_index: int) -> str:
state = self.history[state_index]
unit_strs = [
"____________ Current turn ____________",
]
if state.current_unit is None:
r = f"{state.round_count:>2} -> {state.round_count+1:>2}"
unit_strs.append(f" NEW ROUND {r}")
else:
unit_strs.append(f"{self.get_unit_str(state.current_unit, state_index)}")
unit_strs.append("\n________ Next turns in round _________")
unit_strs.extend(
self.get_unit_str(unit_id, state_index)
for unit_id in state.round_remaining_turns[1:]
)
unit_strs.append("\n________ Awaiting next round _________")
unit_strs.extend(
self.get_unit_str(unit_id, state_index)
for unit_id in state.next_round_order
if unit_id in state.round_done_turns
)
unit_strs.append("\n______________ Dead __________________")
unit_strs.extend(
self.get_unit_str(unit_id, state_index)
for unit_id in reversed(state.death_order)
)
return "\n".join(unit_strs)
def get_unit_str(self, unit_id: int, state_index: Optional[int] = None) -> str:
"""A single line string with info on a unit."""
state = self.replay_state if state_index is None else self.history[state_index]
bot = self.bots[unit_id]
name_label = f"{bot.gui_label[:20]:<20}"
alive = state.alive_mask[unit_id]
if not alive:
return f"[s]{name_label}[/s] died @ step #{state.casualties[unit_id]:^4}"
ap = round(state.ap[unit_id])
ap_spent = round(state.round_ap_spent[unit_id])
return f"{name_label} {ap:>3} AP {ap_spent:>3} used"
# Other
def toggle_autoplay(self, set_to: Optional[bool] = None):
"""Toggles autoplay."""
if self.replay_state.game_over:
self.autoplay = False
return
if set_to is None:
set_to = not self.autoplay
self.autoplay = set_to
self.__last_step = ping()
self.logger("Auto playing..." if self.autoplay else "Paused autoplay...")
def set_step_rate(self, step_rate: float):
"""Determines how many steps to play at most per second during autoplay.
In practice, this will be limited by FPS and blocking time of bots.
"""
assert 0 < step_rate
self.step_interval_ms = 1000 / step_rate
self.__last_step = ping()
def add_state_vfx(self, state_index: int, redraw_last_steps: bool = False):
"""Add all vfx of state_index to queue.
Also clear existing vfx and add vfx from last steps if `redraw_last_steps`.
"""
if redraw_last_steps:
self.clear_vfx()
start_index = max(0, state_index - 1) if redraw_last_steps else state_index
for index in range(start_index, state_index + 1):
for effect in self.history[index].effects:
self.add_vfx(effect.name, effect.origin, effect.target)
def _highlight_current_unit(self):
current_uid = self.replay_state.current_unit
if current_uid is not None:
pos = self.replay_state.positions[current_uid]
self.add_vfx("highlight", pos, steps=1)
def toggle_coords(self, set_to: Optional[bool] = None):
"""Toggle whether to show coordinates on tiles."""
if set_to is None:
set_to = not self.show_coords
self.show_coords = set_to
def set_panel_mode(self, set_to: Optional[PanelMode] = None):
"""Sets the info panel mode. One of: 'turns', 'timers'. Default: 'turns'."""
if set_to is None:
set_to = "turns"
assert set_to in ["turns", "timers"]
self.__panel_mode = set_to
# GUI API
def update(self):
"""Performs autoplay.
Overrides: `botroyale.api.gui.BattleAPI.update`.
"""
if self.replay_state.game_over:
self.autoplay = False
if not self.autoplay:
return
time_delta = pong(self.__last_step)
if time_delta >= self.step_interval_ms:
self.set_replay_index(index_delta=1, disable_autoplay=False)
leftover = time_delta - self.step_interval_ms
self.__last_step = ping() - leftover
def get_time(self) -> int:
"""Step count.
Overrides: `botroyale.api.gui.BattleAPI.get_time`.
"""
return self.replay_state.step_count
def get_controls(self) -> ControlMenu:
"""Return controls for playing, autoplaying, replay index, and more.
Overrides: `botroyale.api.gui.BattleAPI.get_controls`.
"""
return {
"Battle": [
Control("Autoplay", self.toggle_autoplay, "spacebar"),
Control("Preplay <!!!>", self.preplay, "^+ p"),
*[
Control(
f"Set speed {r}", lambda r=r: self.set_step_rate(r), f"{i + 1}"
)
for i, r in enumerate(STEP_RATES[:5])
],
],
"Replay": [
Control("Battle start", lambda: self.set_replay_index(0), "^+ left"),
Control("Battle end <!!!>", lambda: self.play_all(), "^+ right"),
Control("Live", lambda: self.set_replay_index(), "^ l"),
Control(
"Next step", lambda: self.set_replay_index(index_delta=1), "right"
),
Control(
"Prev step", lambda: self.set_replay_index(index_delta=-1), "left"
),
Control(
"+10 steps",
lambda: self.set_replay_index(index_delta=10),
"+ right",
),
Control(
"-10 steps",
lambda: self.set_replay_index(index_delta=-10),
"+ left",
),
Control("Next round", lambda: self.set_to_next_round(), "^ right"),
Control(
"Prev round",
lambda: self.set_to_next_round(backwards=True),
"^ left",
),
],
"Display": [
Control("Turn order", lambda: self.set_panel_mode("turns"), "^ o"),
Control(
"Calculation timers", lambda: self.set_panel_mode("timers"), "^ p"
),
Control("Map coordinates", self.toggle_coords, "^+ d"),
],
}
# Info panel
def get_info_panel_text(self) -> str:
"""Multiline summary of the game at the current `BattleManager.replay_index`.
Overrides: `botroyale.api.gui.BattleAPI.get_info_panel_text`.
Returns:
Return value of `BattleManager.get_info_str`.
"""
return self.get_info_str(self.replay_index)
def get_info_panel_color(self) -> tuple[float, float, float]:
"""Green-ish color when live, blue-ish color when in replay mode.
See: `BattleManager.replay_mode`.
Overrides: `botroyale.api.gui.BattleAPI.get_info_panel_color`.
"""
if self.replay_mode:
# Blue-ish
return 0.1, 0.25, 0.2
# Green-ish
return 0.15, 0.3, 0.05
# Tile map
def get_gui_tile_info(self, hex: Hexagon) -> Tile:
"""Return a `botroyale.api.gui.Tile` for *hex* at the current replay state.
See: `BattleManager.replay_state`
Overrides: `botroyale.api.gui.BattleAPI.get_gui_tile_info`.
"""
state = self.replay_state
tile, bg = get_tile_info(hex, state)
sprite, color, text = get_tile_info_unit(
hex,
state,
self.unit_sprites,
self.unit_colors,
)
if self.show_coords:
text = f"{hex.x},{hex.y}"
return Tile(
tile=tile,
bg=bg,
color=color,
sprite=sprite,
text=text,
)
def get_map_size_hint(self) -> int:
"""Tracks `botroyale.logic.state.State.death_radius`.
Overrides: `botroyale.api.gui.BattleAPI.get_map_size_hint`.
"""
death_radius = self.replay_state.death_radius
if self.replay_state.round_count == 0:
death_radius -= 1
return max(5, death_radius)
def handle_hex_click(self, hex: Hexagon, button: str, mods: str):
"""Handles a tile being clicked on in the tilemap.
Overrides: `botroyale.api.gui.BattleAPI.handle_hex_click`.
"""
click = f"{mods} {button}"
self.logger(f"Clicked {click} on: {hex}")
# Normal click and Control click: info
if mods == "" or mods == "^":
if button == "left":
# Show targets of a plate
p = self.replay_state.get_plate(hex)
if p:
for t in p.targets:
self.add_vfx("highlight", t, steps=1)
# Shift click: mark
elif mods == "+":
vfx = {"left": "green", "right": "red"}.get(button, "blue")
self.add_vfx(f"mark-{vfx}", hex, steps=1)
# Alt click: bot debug
elif mods == "!":
if hex in self.replay_state.positions:
unit_id = self.replay_state.positions.index(hex)
vfx_seq = self.bots[unit_id].gui_click(hex, button, mods)
if vfx_seq is not None:
for vfx_kwargs in vfx_seq:
vfx_kwargs["steps"] = 1
self.add_vfx(**vfx_kwargs)
Classes
class BattleManager (gui_mode: Optional[bool] = False, **kwargs)
-
A GUI interface for
Battle
.Provides methods for parsing, formatting, and displaying information about the battle, as well as GUI-related controls and display getters.
While it can be used as an extension of
Battle
, this class can behave surprisingly different than the base class. This is because it keeps track of a replay index, allowing to "look at" past states. Therefore it is recommended to be familiar withBattleManager.set_replay_index()
.Initialize the class.
Args
kwargs
- Keyword arguments for
Battle
gui_mode
- If True, will set arguments appropriate for the GUI.
Expand source code Browse git
class BattleManager(Battle, BattleAPI): """A GUI interface for `botroyale.logic.battle.Battle`. Provides methods for parsing, formatting, and displaying information about the battle, as well as GUI-related controls and display getters. While it can be used as an extension of `botroyale.logic.battle.Battle`, this class can behave surprisingly different than the base class. This is because it keeps track of a replay index, allowing to "look at" past states. Therefore it is recommended to be familiar with `BattleManager.set_replay_index`. """ show_coords = False step_interval_ms = 1000 / STEP_RATE def __init__(self, gui_mode: Optional[bool] = False, **kwargs): """Initialize the class. Args: kwargs: Keyword arguments for `botroyale.logic.battle.Battle.__init__` gui_mode: If True, will set arguments appropriate for the GUI. """ if gui_mode: kwargs["only_bot_turn_states"] = False kwargs["enable_logging"] = LOGIC_DEBUG Battle.__init__(self, **kwargs) BattleAPI.__init__(self) self.__replay_index = 0 self.autoplay: bool = False self.__last_step = ping() self.unit_colors = [ UNIT_COLORS[bot.COLOR_INDEX % len(UNIT_COLORS)] for bot in self.bots ] self.unit_sprites = [bot.SPRITE for bot in self.bots] self.__panel_mode: PanelMode = "turns" # Replay def set_replay_index( self, index: Optional[int] = None, index_delta: Optional[int] = None, apply_vfx: bool = True, disable_autoplay: bool = True, ): """Set the state index of the replay. Will play missing states until *index* is reached or the game is over. *index* defaults to the index of the last state in history, unless *index_delta* is provided in which case it defaults to the current replay index. Args: index: Index of state to go to. index_delta: Number to add to index. apply_vfx: Queue vfx of the state we are going to. disable_autoplay: Disables autoplay. """ if index is None: if index_delta is None: index = self.history_size - 1 else: index = self.replay_index index = index % self.history_size if index_delta: index += index_delta index = max(0, index) # Play states until we reach the index (and cap index at last state) if self.history_size <= index: missing_state_count = index - self.history_size + 1 self.play_states(missing_state_count) index = min(index, self.history_size - 1) # Set the index apply_vfx = apply_vfx and index != self.__replay_index self.__replay_index = index if apply_vfx: self.add_state_vfx(index, redraw_last_steps=True) self._highlight_current_unit() if disable_autoplay: self.autoplay = False @property def replay_mode(self) -> bool: """Is `BattleManager.replay_index` set to a past state.""" return self.replay_index != self.history_size - 1 @property def replay_index(self) -> int: """The state in history we are looking at. Methods that use state information will look at the state in `BattleManager.replay_index` rather than the last state. """ return self.__replay_index @property def replay_state(self) -> State: """The state at index of `BattleManager.replay_index`.""" return self.history[self.replay_index] def play_all(self, *args, **kwargs): """Overrides the parent method in order to set the replay_index. See: `BattleManager.replay_index`. """ super().play_all(*args, **kwargs) self.set_replay_index() def set_to_next_round(self, backwards: bool = False): """Set the replay to the next "end of round" state (or game over). If backwards is set, it will search for a previous state. See: `botroyale.logic.state.State.end_of_round`. """ delta = 1 if not backwards else -1 if self.replay_state.end_of_round: self.set_replay_index(index_delta=1 if not backwards else -1) while not self.replay_state.end_of_round: if self.replay_state.game_over and not backwards: break self.set_replay_index(index_delta=delta) def preplay(self): """Play the entire battle, then set the replay index to the start. See: `BattleManager.replay_index`. """ self.play_all() self.flush_vfx() self.set_replay_index(0) # Info strings def get_info_str(self, state_index: Optional[int] = None) -> str: """A multiline summary string of the current game state.""" if state_index is None: state_index = self.replay_index strs = [ f"{self._get_status_str(state_index)}", "", ] if self.__panel_mode == "turns": strs.extend( [ f"{self._get_turn_order_str(state_index)}", "", f"{self._get_last_action_str(state_index)}", ] ) elif self.__panel_mode == "timers": strs.append(f"{self.get_timer_str()}") else: raise ValueError(f"Unknown panel mode: {self.__panel_mode}") return "\n".join(strs) def get_timer_str(self) -> str: """Multiline string of bot calculation times. Unlike other methods in this class, the result of this method does not consider `BattleManager.replay_mode`. The times shown are updated to the latest state in the battle. This is because `botroyale.logic.battle.Battle.bot_timer` is updated in place on each new state. """ strs = [ " Bot Calculation Times (ms)", "--------------------------------------", " Bot Mean Max", "", ] if self.replay_mode: strs.insert(0, "Times are not live!\n\n") for bot in self.bots: mean_block_time = self.bot_timer.mean(bot.id) max_block_time = self.bot_timer.max(bot.id) strs.append( "".join( [ f"{bot.gui_label:<20}", f'{f"{mean_block_time:,.1f}":>8} ', f'{f"{max_block_time:,.1f}":>8}', ] ) ) return "\n".join(strs) def _get_status_str(self, state_index: int) -> str: state = self.history[state_index] autoplay = "Playing" if self.autoplay else "Paused" status = f"{autoplay} <= {1000 / self.step_interval_ms:.2f} steps/s" if state.game_over: winner_str = "draw!" winner_id = state.winner if winner_id is not None: winner = self.bots[winner_id] winner_str = f"{winner.gui_label} won!" status = f"GAME OVER : {winner_str}" return "\n".join( [ status, "", f"Step:{state.step_count:^5}Turn:{state.turn_count:^4}" f"Round:{state.round_count:^3}RoD: {state.death_radius:>2}", ] ) def _get_last_action_str(self, state_index: int) -> str: state = self.history[state_index] if state.step_count == 0: last_step_str = "[i]New game[/i]" last_action_str = "[i]Started new game[/i]" else: last_state = self.history[state_index - 1] last_step_str = f"[i]{self.get_state_str(last_state).capitalize()}[/i]" if last_state.end_of_round: last_action_str = f"[i]Started round {state.round_count}[/i]" else: last_action_str = f"{state.last_action}" if not state.is_last_action_legal: last_action_str = f"[i]ILLEGAL[/i] {last_action_str}" return "\n".join( [ f"Last step: {last_step_str}", f"Last action: {last_action_str}", ] ) def _get_turn_order_str(self, state_index: int) -> str: state = self.history[state_index] unit_strs = [ "____________ Current turn ____________", ] if state.current_unit is None: r = f"{state.round_count:>2} -> {state.round_count+1:>2}" unit_strs.append(f" NEW ROUND {r}") else: unit_strs.append(f"{self.get_unit_str(state.current_unit, state_index)}") unit_strs.append("\n________ Next turns in round _________") unit_strs.extend( self.get_unit_str(unit_id, state_index) for unit_id in state.round_remaining_turns[1:] ) unit_strs.append("\n________ Awaiting next round _________") unit_strs.extend( self.get_unit_str(unit_id, state_index) for unit_id in state.next_round_order if unit_id in state.round_done_turns ) unit_strs.append("\n______________ Dead __________________") unit_strs.extend( self.get_unit_str(unit_id, state_index) for unit_id in reversed(state.death_order) ) return "\n".join(unit_strs) def get_unit_str(self, unit_id: int, state_index: Optional[int] = None) -> str: """A single line string with info on a unit.""" state = self.replay_state if state_index is None else self.history[state_index] bot = self.bots[unit_id] name_label = f"{bot.gui_label[:20]:<20}" alive = state.alive_mask[unit_id] if not alive: return f"[s]{name_label}[/s] died @ step #{state.casualties[unit_id]:^4}" ap = round(state.ap[unit_id]) ap_spent = round(state.round_ap_spent[unit_id]) return f"{name_label} {ap:>3} AP {ap_spent:>3} used" # Other def toggle_autoplay(self, set_to: Optional[bool] = None): """Toggles autoplay.""" if self.replay_state.game_over: self.autoplay = False return if set_to is None: set_to = not self.autoplay self.autoplay = set_to self.__last_step = ping() self.logger("Auto playing..." if self.autoplay else "Paused autoplay...") def set_step_rate(self, step_rate: float): """Determines how many steps to play at most per second during autoplay. In practice, this will be limited by FPS and blocking time of bots. """ assert 0 < step_rate self.step_interval_ms = 1000 / step_rate self.__last_step = ping() def add_state_vfx(self, state_index: int, redraw_last_steps: bool = False): """Add all vfx of state_index to queue. Also clear existing vfx and add vfx from last steps if `redraw_last_steps`. """ if redraw_last_steps: self.clear_vfx() start_index = max(0, state_index - 1) if redraw_last_steps else state_index for index in range(start_index, state_index + 1): for effect in self.history[index].effects: self.add_vfx(effect.name, effect.origin, effect.target) def _highlight_current_unit(self): current_uid = self.replay_state.current_unit if current_uid is not None: pos = self.replay_state.positions[current_uid] self.add_vfx("highlight", pos, steps=1) def toggle_coords(self, set_to: Optional[bool] = None): """Toggle whether to show coordinates on tiles.""" if set_to is None: set_to = not self.show_coords self.show_coords = set_to def set_panel_mode(self, set_to: Optional[PanelMode] = None): """Sets the info panel mode. One of: 'turns', 'timers'. Default: 'turns'.""" if set_to is None: set_to = "turns" assert set_to in ["turns", "timers"] self.__panel_mode = set_to # GUI API def update(self): """Performs autoplay. Overrides: `botroyale.api.gui.BattleAPI.update`. """ if self.replay_state.game_over: self.autoplay = False if not self.autoplay: return time_delta = pong(self.__last_step) if time_delta >= self.step_interval_ms: self.set_replay_index(index_delta=1, disable_autoplay=False) leftover = time_delta - self.step_interval_ms self.__last_step = ping() - leftover def get_time(self) -> int: """Step count. Overrides: `botroyale.api.gui.BattleAPI.get_time`. """ return self.replay_state.step_count def get_controls(self) -> ControlMenu: """Return controls for playing, autoplaying, replay index, and more. Overrides: `botroyale.api.gui.BattleAPI.get_controls`. """ return { "Battle": [ Control("Autoplay", self.toggle_autoplay, "spacebar"), Control("Preplay <!!!>", self.preplay, "^+ p"), *[ Control( f"Set speed {r}", lambda r=r: self.set_step_rate(r), f"{i + 1}" ) for i, r in enumerate(STEP_RATES[:5]) ], ], "Replay": [ Control("Battle start", lambda: self.set_replay_index(0), "^+ left"), Control("Battle end <!!!>", lambda: self.play_all(), "^+ right"), Control("Live", lambda: self.set_replay_index(), "^ l"), Control( "Next step", lambda: self.set_replay_index(index_delta=1), "right" ), Control( "Prev step", lambda: self.set_replay_index(index_delta=-1), "left" ), Control( "+10 steps", lambda: self.set_replay_index(index_delta=10), "+ right", ), Control( "-10 steps", lambda: self.set_replay_index(index_delta=-10), "+ left", ), Control("Next round", lambda: self.set_to_next_round(), "^ right"), Control( "Prev round", lambda: self.set_to_next_round(backwards=True), "^ left", ), ], "Display": [ Control("Turn order", lambda: self.set_panel_mode("turns"), "^ o"), Control( "Calculation timers", lambda: self.set_panel_mode("timers"), "^ p" ), Control("Map coordinates", self.toggle_coords, "^+ d"), ], } # Info panel def get_info_panel_text(self) -> str: """Multiline summary of the game at the current `BattleManager.replay_index`. Overrides: `botroyale.api.gui.BattleAPI.get_info_panel_text`. Returns: Return value of `BattleManager.get_info_str`. """ return self.get_info_str(self.replay_index) def get_info_panel_color(self) -> tuple[float, float, float]: """Green-ish color when live, blue-ish color when in replay mode. See: `BattleManager.replay_mode`. Overrides: `botroyale.api.gui.BattleAPI.get_info_panel_color`. """ if self.replay_mode: # Blue-ish return 0.1, 0.25, 0.2 # Green-ish return 0.15, 0.3, 0.05 # Tile map def get_gui_tile_info(self, hex: Hexagon) -> Tile: """Return a `botroyale.api.gui.Tile` for *hex* at the current replay state. See: `BattleManager.replay_state` Overrides: `botroyale.api.gui.BattleAPI.get_gui_tile_info`. """ state = self.replay_state tile, bg = get_tile_info(hex, state) sprite, color, text = get_tile_info_unit( hex, state, self.unit_sprites, self.unit_colors, ) if self.show_coords: text = f"{hex.x},{hex.y}" return Tile( tile=tile, bg=bg, color=color, sprite=sprite, text=text, ) def get_map_size_hint(self) -> int: """Tracks `botroyale.logic.state.State.death_radius`. Overrides: `botroyale.api.gui.BattleAPI.get_map_size_hint`. """ death_radius = self.replay_state.death_radius if self.replay_state.round_count == 0: death_radius -= 1 return max(5, death_radius) def handle_hex_click(self, hex: Hexagon, button: str, mods: str): """Handles a tile being clicked on in the tilemap. Overrides: `botroyale.api.gui.BattleAPI.handle_hex_click`. """ click = f"{mods} {button}" self.logger(f"Clicked {click} on: {hex}") # Normal click and Control click: info if mods == "" or mods == "^": if button == "left": # Show targets of a plate p = self.replay_state.get_plate(hex) if p: for t in p.targets: self.add_vfx("highlight", t, steps=1) # Shift click: mark elif mods == "+": vfx = {"left": "green", "right": "red"}.get(button, "blue") self.add_vfx(f"mark-{vfx}", hex, steps=1) # Alt click: bot debug elif mods == "!": if hex in self.replay_state.positions: unit_id = self.replay_state.positions.index(hex) vfx_seq = self.bots[unit_id].gui_click(hex, button, mods) if vfx_seq is not None: for vfx_kwargs in vfx_seq: vfx_kwargs["steps"] = 1 self.add_vfx(**vfx_kwargs)
Ancestors
Class variables
var show_coords
var step_interval_ms
Instance variables
var replay_index : int
-
The state in history we are looking at.
Methods that use state information will look at the state in
BattleManager.replay_index
rather than the last state.Expand source code Browse git
@property def replay_index(self) -> int: """The state in history we are looking at. Methods that use state information will look at the state in `BattleManager.replay_index` rather than the last state. """ return self.__replay_index
var replay_mode : bool
-
Is
BattleManager.replay_index
set to a past state.Expand source code Browse git
@property def replay_mode(self) -> bool: """Is `BattleManager.replay_index` set to a past state.""" return self.replay_index != self.history_size - 1
var replay_state : State
-
The state at index of
BattleManager.replay_index
.Expand source code Browse git
@property def replay_state(self) -> State: """The state at index of `BattleManager.replay_index`.""" return self.history[self.replay_index]
Methods
def add_state_vfx(self, state_index: int, redraw_last_steps: bool = False)
-
Add all vfx of state_index to queue.
Also clear existing vfx and add vfx from last steps if
redraw_last_steps
.Expand source code Browse git
def add_state_vfx(self, state_index: int, redraw_last_steps: bool = False): """Add all vfx of state_index to queue. Also clear existing vfx and add vfx from last steps if `redraw_last_steps`. """ if redraw_last_steps: self.clear_vfx() start_index = max(0, state_index - 1) if redraw_last_steps else state_index for index in range(start_index, state_index + 1): for effect in self.history[index].effects: self.add_vfx(effect.name, effect.origin, effect.target)
def get_controls(self) ‑> dict[str, list[Control]]
-
Return controls for playing, autoplaying, replay index, and more.
Overrides:
BattleAPI.get_controls()
.Expand source code Browse git
def get_controls(self) -> ControlMenu: """Return controls for playing, autoplaying, replay index, and more. Overrides: `botroyale.api.gui.BattleAPI.get_controls`. """ return { "Battle": [ Control("Autoplay", self.toggle_autoplay, "spacebar"), Control("Preplay <!!!>", self.preplay, "^+ p"), *[ Control( f"Set speed {r}", lambda r=r: self.set_step_rate(r), f"{i + 1}" ) for i, r in enumerate(STEP_RATES[:5]) ], ], "Replay": [ Control("Battle start", lambda: self.set_replay_index(0), "^+ left"), Control("Battle end <!!!>", lambda: self.play_all(), "^+ right"), Control("Live", lambda: self.set_replay_index(), "^ l"), Control( "Next step", lambda: self.set_replay_index(index_delta=1), "right" ), Control( "Prev step", lambda: self.set_replay_index(index_delta=-1), "left" ), Control( "+10 steps", lambda: self.set_replay_index(index_delta=10), "+ right", ), Control( "-10 steps", lambda: self.set_replay_index(index_delta=-10), "+ left", ), Control("Next round", lambda: self.set_to_next_round(), "^ right"), Control( "Prev round", lambda: self.set_to_next_round(backwards=True), "^ left", ), ], "Display": [ Control("Turn order", lambda: self.set_panel_mode("turns"), "^ o"), Control( "Calculation timers", lambda: self.set_panel_mode("timers"), "^ p" ), Control("Map coordinates", self.toggle_coords, "^+ d"), ], }
def get_gui_tile_info(self, hex: Hexagon) ‑> Tile
-
Return a
Tile
for hex at the current replay state.See:
BattleManager.replay_state
Overrides:
BattleAPI.get_gui_tile_info()
.Expand source code Browse git
def get_gui_tile_info(self, hex: Hexagon) -> Tile: """Return a `botroyale.api.gui.Tile` for *hex* at the current replay state. See: `BattleManager.replay_state` Overrides: `botroyale.api.gui.BattleAPI.get_gui_tile_info`. """ state = self.replay_state tile, bg = get_tile_info(hex, state) sprite, color, text = get_tile_info_unit( hex, state, self.unit_sprites, self.unit_colors, ) if self.show_coords: text = f"{hex.x},{hex.y}" return Tile( tile=tile, bg=bg, color=color, sprite=sprite, text=text, )
def get_info_panel_color(self) ‑> tuple[float, float, float]
-
Green-ish color when live, blue-ish color when in replay mode.
See:
BattleManager.replay_mode
.Overrides:
BattleAPI.get_info_panel_color()
.Expand source code Browse git
def get_info_panel_color(self) -> tuple[float, float, float]: """Green-ish color when live, blue-ish color when in replay mode. See: `BattleManager.replay_mode`. Overrides: `botroyale.api.gui.BattleAPI.get_info_panel_color`. """ if self.replay_mode: # Blue-ish return 0.1, 0.25, 0.2 # Green-ish return 0.15, 0.3, 0.05
def get_info_panel_text(self) ‑> str
-
Multiline summary of the game at the current
BattleManager.replay_index
.Overrides:
BattleAPI.get_info_panel_text()
.Returns
Return value of
BattleManager.get_info_str()
.Expand source code Browse git
def get_info_panel_text(self) -> str: """Multiline summary of the game at the current `BattleManager.replay_index`. Overrides: `botroyale.api.gui.BattleAPI.get_info_panel_text`. Returns: Return value of `BattleManager.get_info_str`. """ return self.get_info_str(self.replay_index)
def get_info_str(self, state_index: Optional[int] = None) ‑> str
-
A multiline summary string of the current game state.
Expand source code Browse git
def get_info_str(self, state_index: Optional[int] = None) -> str: """A multiline summary string of the current game state.""" if state_index is None: state_index = self.replay_index strs = [ f"{self._get_status_str(state_index)}", "", ] if self.__panel_mode == "turns": strs.extend( [ f"{self._get_turn_order_str(state_index)}", "", f"{self._get_last_action_str(state_index)}", ] ) elif self.__panel_mode == "timers": strs.append(f"{self.get_timer_str()}") else: raise ValueError(f"Unknown panel mode: {self.__panel_mode}") return "\n".join(strs)
def get_map_size_hint(self) ‑> int
-
Tracks
State.death_radius
.Overrides:
BattleAPI.get_map_size_hint()
.Expand source code Browse git
def get_map_size_hint(self) -> int: """Tracks `botroyale.logic.state.State.death_radius`. Overrides: `botroyale.api.gui.BattleAPI.get_map_size_hint`. """ death_radius = self.replay_state.death_radius if self.replay_state.round_count == 0: death_radius -= 1 return max(5, death_radius)
def get_time(self) ‑> int
-
Step count.
Overrides:
BattleAPI.get_time()
.Expand source code Browse git
def get_time(self) -> int: """Step count. Overrides: `botroyale.api.gui.BattleAPI.get_time`. """ return self.replay_state.step_count
def get_timer_str(self) ‑> str
-
Multiline string of bot calculation times.
Unlike other methods in this class, the result of this method does not consider
BattleManager.replay_mode
. The times shown are updated to the latest state in the battle. This is becauseBattle.bot_timer
is updated in place on each new state.Expand source code Browse git
def get_timer_str(self) -> str: """Multiline string of bot calculation times. Unlike other methods in this class, the result of this method does not consider `BattleManager.replay_mode`. The times shown are updated to the latest state in the battle. This is because `botroyale.logic.battle.Battle.bot_timer` is updated in place on each new state. """ strs = [ " Bot Calculation Times (ms)", "--------------------------------------", " Bot Mean Max", "", ] if self.replay_mode: strs.insert(0, "Times are not live!\n\n") for bot in self.bots: mean_block_time = self.bot_timer.mean(bot.id) max_block_time = self.bot_timer.max(bot.id) strs.append( "".join( [ f"{bot.gui_label:<20}", f'{f"{mean_block_time:,.1f}":>8} ', f'{f"{max_block_time:,.1f}":>8}', ] ) ) return "\n".join(strs)
def get_unit_str(self, unit_id: int, state_index: Optional[int] = None) ‑> str
-
A single line string with info on a unit.
Expand source code Browse git
def get_unit_str(self, unit_id: int, state_index: Optional[int] = None) -> str: """A single line string with info on a unit.""" state = self.replay_state if state_index is None else self.history[state_index] bot = self.bots[unit_id] name_label = f"{bot.gui_label[:20]:<20}" alive = state.alive_mask[unit_id] if not alive: return f"[s]{name_label}[/s] died @ step #{state.casualties[unit_id]:^4}" ap = round(state.ap[unit_id]) ap_spent = round(state.round_ap_spent[unit_id]) return f"{name_label} {ap:>3} AP {ap_spent:>3} used"
def handle_hex_click(self, hex: Hexagon, button: str, mods: str)
-
Handles a tile being clicked on in the tilemap.
Overrides:
BattleAPI.handle_hex_click()
.Expand source code Browse git
def handle_hex_click(self, hex: Hexagon, button: str, mods: str): """Handles a tile being clicked on in the tilemap. Overrides: `botroyale.api.gui.BattleAPI.handle_hex_click`. """ click = f"{mods} {button}" self.logger(f"Clicked {click} on: {hex}") # Normal click and Control click: info if mods == "" or mods == "^": if button == "left": # Show targets of a plate p = self.replay_state.get_plate(hex) if p: for t in p.targets: self.add_vfx("highlight", t, steps=1) # Shift click: mark elif mods == "+": vfx = {"left": "green", "right": "red"}.get(button, "blue") self.add_vfx(f"mark-{vfx}", hex, steps=1) # Alt click: bot debug elif mods == "!": if hex in self.replay_state.positions: unit_id = self.replay_state.positions.index(hex) vfx_seq = self.bots[unit_id].gui_click(hex, button, mods) if vfx_seq is not None: for vfx_kwargs in vfx_seq: vfx_kwargs["steps"] = 1 self.add_vfx(**vfx_kwargs)
def play_all(self, *args, **kwargs)
-
Overrides the parent method in order to set the replay_index.
Expand source code Browse git
def play_all(self, *args, **kwargs): """Overrides the parent method in order to set the replay_index. See: `BattleManager.replay_index`. """ super().play_all(*args, **kwargs) self.set_replay_index()
def preplay(self)
-
Play the entire battle, then set the replay index to the start.
Expand source code Browse git
def preplay(self): """Play the entire battle, then set the replay index to the start. See: `BattleManager.replay_index`. """ self.play_all() self.flush_vfx() self.set_replay_index(0)
def set_panel_mode(self, set_to: Optional[Literal['turns', 'timers']] = None)
-
Sets the info panel mode. One of: 'turns', 'timers'. Default: 'turns'.
Expand source code Browse git
def set_panel_mode(self, set_to: Optional[PanelMode] = None): """Sets the info panel mode. One of: 'turns', 'timers'. Default: 'turns'.""" if set_to is None: set_to = "turns" assert set_to in ["turns", "timers"] self.__panel_mode = set_to
def set_replay_index(self, index: Optional[int] = None, index_delta: Optional[int] = None, apply_vfx: bool = True, disable_autoplay: bool = True)
-
Set the state index of the replay.
Will play missing states until index is reached or the game is over.
index defaults to the index of the last state in history, unless index_delta is provided in which case it defaults to the current replay index.
Args
index
- Index of state to go to.
index_delta
- Number to add to index.
apply_vfx
- Queue vfx of the state we are going to.
disable_autoplay
- Disables autoplay.
Expand source code Browse git
def set_replay_index( self, index: Optional[int] = None, index_delta: Optional[int] = None, apply_vfx: bool = True, disable_autoplay: bool = True, ): """Set the state index of the replay. Will play missing states until *index* is reached or the game is over. *index* defaults to the index of the last state in history, unless *index_delta* is provided in which case it defaults to the current replay index. Args: index: Index of state to go to. index_delta: Number to add to index. apply_vfx: Queue vfx of the state we are going to. disable_autoplay: Disables autoplay. """ if index is None: if index_delta is None: index = self.history_size - 1 else: index = self.replay_index index = index % self.history_size if index_delta: index += index_delta index = max(0, index) # Play states until we reach the index (and cap index at last state) if self.history_size <= index: missing_state_count = index - self.history_size + 1 self.play_states(missing_state_count) index = min(index, self.history_size - 1) # Set the index apply_vfx = apply_vfx and index != self.__replay_index self.__replay_index = index if apply_vfx: self.add_state_vfx(index, redraw_last_steps=True) self._highlight_current_unit() if disable_autoplay: self.autoplay = False
def set_step_rate(self, step_rate: float)
-
Determines how many steps to play at most per second during autoplay.
In practice, this will be limited by FPS and blocking time of bots.
Expand source code Browse git
def set_step_rate(self, step_rate: float): """Determines how many steps to play at most per second during autoplay. In practice, this will be limited by FPS and blocking time of bots. """ assert 0 < step_rate self.step_interval_ms = 1000 / step_rate self.__last_step = ping()
def set_to_next_round(self, backwards: bool = False)
-
Set the replay to the next "end of round" state (or game over).
If backwards is set, it will search for a previous state.
See:
State.end_of_round
.Expand source code Browse git
def set_to_next_round(self, backwards: bool = False): """Set the replay to the next "end of round" state (or game over). If backwards is set, it will search for a previous state. See: `botroyale.logic.state.State.end_of_round`. """ delta = 1 if not backwards else -1 if self.replay_state.end_of_round: self.set_replay_index(index_delta=1 if not backwards else -1) while not self.replay_state.end_of_round: if self.replay_state.game_over and not backwards: break self.set_replay_index(index_delta=delta)
def toggle_autoplay(self, set_to: Optional[bool] = None)
-
Toggles autoplay.
Expand source code Browse git
def toggle_autoplay(self, set_to: Optional[bool] = None): """Toggles autoplay.""" if self.replay_state.game_over: self.autoplay = False return if set_to is None: set_to = not self.autoplay self.autoplay = set_to self.__last_step = ping() self.logger("Auto playing..." if self.autoplay else "Paused autoplay...")
def toggle_coords(self, set_to: Optional[bool] = None)
-
Toggle whether to show coordinates on tiles.
Expand source code Browse git
def toggle_coords(self, set_to: Optional[bool] = None): """Toggle whether to show coordinates on tiles.""" if set_to is None: set_to = not self.show_coords self.show_coords = set_to
def update(self)
-
Performs autoplay.
Overrides:
BattleAPI.update()
.Expand source code Browse git
def update(self): """Performs autoplay. Overrides: `botroyale.api.gui.BattleAPI.update`. """ if self.replay_state.game_over: self.autoplay = False if not self.autoplay: return time_delta = pong(self.__last_step) if time_delta >= self.step_interval_ms: self.set_replay_index(index_delta=1, disable_autoplay=False) leftover = time_delta - self.step_interval_ms self.__last_step = ping() - leftover
Inherited members