Module botroyale.logic.battle

Home of the Battle class.

The Battle class is used to manage the states and bots of a full battle. Bot developers will use this module when playing battles programatically (without the GUI).

Setup and Play

The Battle collects bots from a given BotSelection object, initializes them and calls their BaseBot.setup() method before any turns begin. When it is requested to play states, the Battle will manage calling the bot's BaseBot.poll_action() method on their turn and applying the action. See: Battle.play_all().

State History

The latest state in a Battle can be found using Battle.state. All previous states instances reside in the Battle.history list.

Example Usage

new_battle = br.Battle(
    initial_state=br.get_map_state("classic")
    bots=br.BotSelection(["mybot", "mybot2"])
)
new_battle.play_all()
print(f"Winner: {new_battle.winner}")  # May be None in case of draw
Expand source code Browse git
"""Home of the `botroyale.logic.battle.Battle` class.

The `Battle` class is used to manage the states and bots of a full battle. Bot
developers will use this module when playing battles programatically (without
the GUI).

## Setup and Play
The Battle collects bots from a given `botroyale.api.bots.BotSelection` object,
initializes them and calls their `botroyale.api.bots.BaseBot.setup` method
before any turns begin. When it is requested to play states, the Battle will
manage calling the bot's `botroyale.api.bots.BaseBot.poll_action` method on
their turn and applying the action. See: `Battle.play_all`.

## State History
The latest state in a Battle can be found using `Battle.state`. All previous
states instances reside in the `Battle.history` list.

## Example Usage
```python
new_battle = br.Battle(
    initial_state=br.get_map_state("classic")
    bots=br.BotSelection(["mybot", "mybot2"])
)
new_battle.play_all()
print(f"Winner: {new_battle.winner}")  # May be None in case of draw
```
"""
from typing import Optional
import sys
import traceback
import numpy as np
from botroyale.api.logging import Logger, logger as glogger
from botroyale.api.bots import BaseBot, BotSelection
from botroyale.api.actions import Action
from botroyale.logic.maps import get_map_state
from botroyale.logic.state import State
from botroyale.util.time import pingpong


LINEBR = "=" * 75


class Battle:
    """See module documentation for details."""

    def __init__(
        self,
        initial_state: Optional[State] = None,
        bots: Optional[BotSelection] = None,
        description: str = "No description set",
        enable_logging: bool = True,
        enable_bot_logging: Optional[bool] = None,
        only_bot_turn_states: bool = True,
        threshold_bot_block_seconds: float = 20.0,
    ):
        """Initialize the class.

        Args:
            initial_state: The first state of the battle. If initial_state is
                not provided, it will be generated using the map generator based
                on configured settings.

            bots: A `botroyale.api.bots.BotSelection` object that provides bots.

            description: A description of the battle.

            enable_logging: Passing False will disable battle logs. Not
                including bots.

            enable_bot_logging: Passing False will disable the logger while
                bots are called. Defaults to the same value as *enable_logging*.

            only_bot_turn_states: Determines whether to skip `State.end_of_round`
                states and other states that are not expecting an action from a
                unit. This is useful for bot developers who may not care about
                states out of turn and only care to see when a bot needs to take
                action. This essentially determines whether actions are applied
                to states with `State.apply_action` or
                `State.apply_action_manual`.

            threshold_bot_block_seconds: Threshold of calculation time for bots
                to trigger a warning in the log.
        """
        self.enable_logging: bool = enable_logging
        """Enable logging of the battle itself."""
        if enable_bot_logging is None:
            enable_bot_logging = enable_logging
        self.enable_bot_logging: bool = enable_bot_logging
        """Enable logging for the bots."""
        self.description: str = description
        """A description of the battle."""
        if initial_state is None:
            with Logger.set_logging_temp(enable_logging):
                initial_state = get_map_state()
        self.__current_state: State = initial_state
        self.history: list[State] = [initial_state]
        """A list of states."""
        self.__only_bot_turn_states: bool = only_bot_turn_states
        self.__threshold_bot_block_ms: float = threshold_bot_block_seconds * 1000
        # Bots
        bot_count = initial_state.num_of_units
        self.bot_timer: TurnTimer = TurnTimer(bot_count)
        """A `TurnTimer` object for keeping track of bot calculation times."""
        if bots is None:
            bots = BotSelection()
        with Logger.set_logging_temp(enable_bot_logging):
            bot_classes = bots.get_bots(bot_count)
        assert len(bot_classes) == bot_count
        self.bots: tuple[BaseBot, ...] = tuple(
            bcls(i) for i, bcls in enumerate(bot_classes)
        )
        """Tuple of bot instances."""
        # Allow bots to prepare
        with Logger.set_logging_temp(enable_bot_logging):
            for uid, bot in enumerate(self.bots):
                assert isinstance(bot, BaseBot)
                bot.setup(initial_state)
        # Skip to the first bot's turn if set to do so
        assert self.state.round_count == 0
        if only_bot_turn_states:
            self.play_state()
            assert self.state.round_count == 1

    # History
    @property
    def state(self) -> State:
        """The current state."""
        return self.__current_state

    @property
    def previous_state(self) -> Optional[State]:
        """The state before the current one (or None if currently at first state)."""
        if self.history_size <= 1:
            return None
        return self.history[self.history_size - 2]

    @property
    def history_size(self):
        """Size of state history."""
        return len(self.history)

    # Play
    def play_state(self):
        """Plays the next state, and adds it to history."""
        state = self.state
        self.log_state(state)
        if state.end_of_round:
            new_state = state.increment_round()
            self.logger(f"Death radius: {new_state.death_radius}")
        else:
            unit_id = state.current_unit
            action = self._get_bot_action(unit_id, state)
            if action is not None:
                # Bot returned an action, apply.
                self.logger(f"Applying {action} to state")
                if self.__only_bot_turn_states:
                    new_state = state.apply_action(action)
                else:
                    new_state = state.apply_action_manual(action)
                    if not new_state.is_last_action_legal:
                        self.logger(f"ILLEGAL: {action}")
            else:
                # Bot failed to return an action, kill.
                self.logger(f"Killing {self.bots[unit_id]}...")
                if self.__only_bot_turn_states:
                    new_state = state.apply_kill_unit()
                else:
                    new_state = state.apply_kill_unit_manual()
        self.__current_state = new_state
        self.history.append(new_state)
        if new_state.round_count >= self.bot_timer.round_count:
            self.bot_timer.add_round()

    def play_states(self, count: int):
        """Plays a number of states."""
        while not self.state.game_over and count:
            self.play_state()
            count -= 1

    def play_all(self, disable_logging: bool = False, print_progress: bool = False):
        """Plays the battle to completion.

        Args:
            disable_logging: Disable logging globally while playing.
            print_progress: Disable logging globally while playing and print a
                progress bar to console.
        """

        def print_progress_bar():
            rc = self.state.round_count
            done = "█" * rc
            remaining = "░" * (self.state.death_radius - 1)
            pbar = f"{done}{remaining}  ({rc} / {rc+self.state.death_radius-1} rounds)"
            print(f"\r{pbar}", end="")

        if print_progress:
            disable_logging = True
            print_progress_bar()
        last_rc = self.state.round_count
        with Logger.set_logging_temp(not disable_logging):
            while not self.state.game_over:
                last_rc = self.state.round_count
                self.play_state()
                if print_progress and self.state.round_count > last_rc:
                    print_progress_bar()
        if print_progress:
            print("")

    def _get_bot_action(self, unit_id: int, state: State) -> Optional[Action]:
        """Call the bot's `botroyale.api.bots.BaseBot.poll_action` to get their action.

        Args:
            unit_id: uid of the unit
            state: current state of the battle

        Returns:
            `botroyale.api.actions.Action` if *poll_action* is successful.
            None if there was an exception.
        """
        state = state.copy()
        bot = self.bots[unit_id]
        pingpong_desc = f"{bot} poll_action (step {state.step_count})"

        def add_bot_time(elapsed):
            self.bot_timer.add_time(unit_id, elapsed)

        with pingpong(pingpong_desc, logger=self.logger, return_elapsed=add_bot_time):
            try:
                with Logger.set_logging_temp(self.enable_bot_logging):
                    action = bot.poll_action(state)
                assert isinstance(action, Action)
            except Exception as e:
                formatted_exc = "".join(traceback.format_exception(*sys.exc_info()))
                self.logger(f"CRASH {bot}: {e}\n\n{formatted_exc}")
                return None
        self.logger(LINEBR)
        self.logger(f"Received action: {action}")
        ttime = self.bot_timer.get_time(unit_id)
        if ttime > self.__threshold_bot_block_ms:
            self.logger(
                f"BLOCK TIME WARNING : {bot} has taken {ttime/1000:.2f} "
                "seconds this turn"
            )
        return action

    # Logging
    def logger(self, text: str):
        """Logger for the battle."""
        if self.enable_logging:
            glogger(text)

    def log_state(self, state: State):
        """Log a quick summary of the current state."""
        self.logger(
            "\n".join(
                [
                    LINEBR,
                    " ".join(
                        [
                            f"Step: {str(state.step_count):^4}",
                            f"Turn: {str(state.turn_count):^4}",
                            f"Round: {str(state.round_count):^3}",
                            "|",
                            f"{self.get_state_str(state)}",
                        ]
                    ),
                    LINEBR,
                ]
            )
        )

    def get_state_str(self, state: State) -> str:
        """A string representation of the type of state (bot turn / end of round)."""
        if state.end_of_round:
            return "end of round"
        assert state.current_unit is not None
        return f"{self.bots[state.current_unit].gui_label}'s turn"

    def get_unit_str(self, unit_id: int) -> str:
        """An internal repr for a unit."""
        return self.bots[unit_id].gui_label

    # Miscallaneous
    @property
    def winner(self) -> Optional[int]:
        """Alias for `botroyale.logic.state.State.winner` of the current state."""
        return self.state.winner

    @property
    def losers(self) -> Optional[list[int]]:
        """Returns a list of unit ids that did not win, or None if game isn't over."""
        if self.state.game_over:
            return [b.id for b in self.bots if b.id != self.winner]
        return None


class TurnTimer:
    """Records calculation times for multiple bots over multiple turns."""

    def __init__(self, num_of_units: int):
        """Initialize the class."""
        self.num_of_units: int = num_of_units
        self.round_timers: np.ndarray = np.zeros((1, num_of_units), dtype=np.float64)

    @property
    def all_times(self) -> np.ndarray:
        """Return the full table of times (numpy array). Not a copy.

        The ndarray shape will be (num_of_rounds, num_of_units), such that every
        element in the array represents the time that unit has in that round.
        """
        return self.round_timers

    def rounds_played(self, unit_id: int) -> int:
        """Return the number of rounds that have times recorded for a unit."""
        return int(np.sum(self.round_timers[:, unit_id] > 0))

    def total(self, unit_id: int) -> float:
        """Return the total time recorded for a unit."""
        return float(np.sum(self.round_timers[:, unit_id]))

    def mean(self, unit_id: int) -> float:
        """Return the mean of times recorded for a unit.

        Assumes rounds with 0.0 time do not count.
        """
        rounds_played = self.rounds_played(unit_id)
        if rounds_played > 0:
            return float(self.total(unit_id) / rounds_played)
        return 0.0

    def max(self, unit_id: int) -> float:
        """Return the max of times recorded for a unit."""
        return float(np.max(self.round_timers[:, unit_id]))

    @property
    def round_count(self) -> int:
        """Number of rounds in the table."""
        return len(self.round_timers)

    def add_round(self):
        """Add a new round to the table."""
        new_row = np.zeros(self.num_of_units, dtype=np.float64)
        self.round_timers = np.vstack((self.round_timers, new_row))

    def add_time(self, unit_id: int, time: float, round: Optional[int] = None):
        """Add time for a unit at a given round.

        Will use the last round if none is provided.
        """
        if round is None:
            round = self.round_count - 1
        self.round_timers[round, unit_id] += time

    def get_time(self, unit_id: int, round: Optional[int] = None) -> float:
        """Get the time recorded for a unit at a given round (default: last round)."""
        if round is None:
            round = self.round_count - 1
        return float(self.round_timers[round, unit_id])

Classes

class Battle (initial_state: Optional[State] = None, bots: Optional[BotSelection] = None, description: str = 'No description set', enable_logging: bool = True, enable_bot_logging: Optional[bool] = None, only_bot_turn_states: bool = True, threshold_bot_block_seconds: float = 20.0)

See module documentation for details.

Initialize the class.

Args

initial_state
The first state of the battle. If initial_state is not provided, it will be generated using the map generator based on configured settings.
bots
A BotSelection object that provides bots.
description
A description of the battle.
enable_logging
Passing False will disable battle logs. Not including bots.
enable_bot_logging
Passing False will disable the logger while bots are called. Defaults to the same value as enable_logging.
only_bot_turn_states
Determines whether to skip State.end_of_round states and other states that are not expecting an action from a unit. This is useful for bot developers who may not care about states out of turn and only care to see when a bot needs to take action. This essentially determines whether actions are applied to states with State.apply_action or State.apply_action_manual.
threshold_bot_block_seconds
Threshold of calculation time for bots to trigger a warning in the log.
Expand source code Browse git
class Battle:
    """See module documentation for details."""

    def __init__(
        self,
        initial_state: Optional[State] = None,
        bots: Optional[BotSelection] = None,
        description: str = "No description set",
        enable_logging: bool = True,
        enable_bot_logging: Optional[bool] = None,
        only_bot_turn_states: bool = True,
        threshold_bot_block_seconds: float = 20.0,
    ):
        """Initialize the class.

        Args:
            initial_state: The first state of the battle. If initial_state is
                not provided, it will be generated using the map generator based
                on configured settings.

            bots: A `botroyale.api.bots.BotSelection` object that provides bots.

            description: A description of the battle.

            enable_logging: Passing False will disable battle logs. Not
                including bots.

            enable_bot_logging: Passing False will disable the logger while
                bots are called. Defaults to the same value as *enable_logging*.

            only_bot_turn_states: Determines whether to skip `State.end_of_round`
                states and other states that are not expecting an action from a
                unit. This is useful for bot developers who may not care about
                states out of turn and only care to see when a bot needs to take
                action. This essentially determines whether actions are applied
                to states with `State.apply_action` or
                `State.apply_action_manual`.

            threshold_bot_block_seconds: Threshold of calculation time for bots
                to trigger a warning in the log.
        """
        self.enable_logging: bool = enable_logging
        """Enable logging of the battle itself."""
        if enable_bot_logging is None:
            enable_bot_logging = enable_logging
        self.enable_bot_logging: bool = enable_bot_logging
        """Enable logging for the bots."""
        self.description: str = description
        """A description of the battle."""
        if initial_state is None:
            with Logger.set_logging_temp(enable_logging):
                initial_state = get_map_state()
        self.__current_state: State = initial_state
        self.history: list[State] = [initial_state]
        """A list of states."""
        self.__only_bot_turn_states: bool = only_bot_turn_states
        self.__threshold_bot_block_ms: float = threshold_bot_block_seconds * 1000
        # Bots
        bot_count = initial_state.num_of_units
        self.bot_timer: TurnTimer = TurnTimer(bot_count)
        """A `TurnTimer` object for keeping track of bot calculation times."""
        if bots is None:
            bots = BotSelection()
        with Logger.set_logging_temp(enable_bot_logging):
            bot_classes = bots.get_bots(bot_count)
        assert len(bot_classes) == bot_count
        self.bots: tuple[BaseBot, ...] = tuple(
            bcls(i) for i, bcls in enumerate(bot_classes)
        )
        """Tuple of bot instances."""
        # Allow bots to prepare
        with Logger.set_logging_temp(enable_bot_logging):
            for uid, bot in enumerate(self.bots):
                assert isinstance(bot, BaseBot)
                bot.setup(initial_state)
        # Skip to the first bot's turn if set to do so
        assert self.state.round_count == 0
        if only_bot_turn_states:
            self.play_state()
            assert self.state.round_count == 1

    # History
    @property
    def state(self) -> State:
        """The current state."""
        return self.__current_state

    @property
    def previous_state(self) -> Optional[State]:
        """The state before the current one (or None if currently at first state)."""
        if self.history_size <= 1:
            return None
        return self.history[self.history_size - 2]

    @property
    def history_size(self):
        """Size of state history."""
        return len(self.history)

    # Play
    def play_state(self):
        """Plays the next state, and adds it to history."""
        state = self.state
        self.log_state(state)
        if state.end_of_round:
            new_state = state.increment_round()
            self.logger(f"Death radius: {new_state.death_radius}")
        else:
            unit_id = state.current_unit
            action = self._get_bot_action(unit_id, state)
            if action is not None:
                # Bot returned an action, apply.
                self.logger(f"Applying {action} to state")
                if self.__only_bot_turn_states:
                    new_state = state.apply_action(action)
                else:
                    new_state = state.apply_action_manual(action)
                    if not new_state.is_last_action_legal:
                        self.logger(f"ILLEGAL: {action}")
            else:
                # Bot failed to return an action, kill.
                self.logger(f"Killing {self.bots[unit_id]}...")
                if self.__only_bot_turn_states:
                    new_state = state.apply_kill_unit()
                else:
                    new_state = state.apply_kill_unit_manual()
        self.__current_state = new_state
        self.history.append(new_state)
        if new_state.round_count >= self.bot_timer.round_count:
            self.bot_timer.add_round()

    def play_states(self, count: int):
        """Plays a number of states."""
        while not self.state.game_over and count:
            self.play_state()
            count -= 1

    def play_all(self, disable_logging: bool = False, print_progress: bool = False):
        """Plays the battle to completion.

        Args:
            disable_logging: Disable logging globally while playing.
            print_progress: Disable logging globally while playing and print a
                progress bar to console.
        """

        def print_progress_bar():
            rc = self.state.round_count
            done = "█" * rc
            remaining = "░" * (self.state.death_radius - 1)
            pbar = f"{done}{remaining}  ({rc} / {rc+self.state.death_radius-1} rounds)"
            print(f"\r{pbar}", end="")

        if print_progress:
            disable_logging = True
            print_progress_bar()
        last_rc = self.state.round_count
        with Logger.set_logging_temp(not disable_logging):
            while not self.state.game_over:
                last_rc = self.state.round_count
                self.play_state()
                if print_progress and self.state.round_count > last_rc:
                    print_progress_bar()
        if print_progress:
            print("")

    def _get_bot_action(self, unit_id: int, state: State) -> Optional[Action]:
        """Call the bot's `botroyale.api.bots.BaseBot.poll_action` to get their action.

        Args:
            unit_id: uid of the unit
            state: current state of the battle

        Returns:
            `botroyale.api.actions.Action` if *poll_action* is successful.
            None if there was an exception.
        """
        state = state.copy()
        bot = self.bots[unit_id]
        pingpong_desc = f"{bot} poll_action (step {state.step_count})"

        def add_bot_time(elapsed):
            self.bot_timer.add_time(unit_id, elapsed)

        with pingpong(pingpong_desc, logger=self.logger, return_elapsed=add_bot_time):
            try:
                with Logger.set_logging_temp(self.enable_bot_logging):
                    action = bot.poll_action(state)
                assert isinstance(action, Action)
            except Exception as e:
                formatted_exc = "".join(traceback.format_exception(*sys.exc_info()))
                self.logger(f"CRASH {bot}: {e}\n\n{formatted_exc}")
                return None
        self.logger(LINEBR)
        self.logger(f"Received action: {action}")
        ttime = self.bot_timer.get_time(unit_id)
        if ttime > self.__threshold_bot_block_ms:
            self.logger(
                f"BLOCK TIME WARNING : {bot} has taken {ttime/1000:.2f} "
                "seconds this turn"
            )
        return action

    # Logging
    def logger(self, text: str):
        """Logger for the battle."""
        if self.enable_logging:
            glogger(text)

    def log_state(self, state: State):
        """Log a quick summary of the current state."""
        self.logger(
            "\n".join(
                [
                    LINEBR,
                    " ".join(
                        [
                            f"Step: {str(state.step_count):^4}",
                            f"Turn: {str(state.turn_count):^4}",
                            f"Round: {str(state.round_count):^3}",
                            "|",
                            f"{self.get_state_str(state)}",
                        ]
                    ),
                    LINEBR,
                ]
            )
        )

    def get_state_str(self, state: State) -> str:
        """A string representation of the type of state (bot turn / end of round)."""
        if state.end_of_round:
            return "end of round"
        assert state.current_unit is not None
        return f"{self.bots[state.current_unit].gui_label}'s turn"

    def get_unit_str(self, unit_id: int) -> str:
        """An internal repr for a unit."""
        return self.bots[unit_id].gui_label

    # Miscallaneous
    @property
    def winner(self) -> Optional[int]:
        """Alias for `botroyale.logic.state.State.winner` of the current state."""
        return self.state.winner

    @property
    def losers(self) -> Optional[list[int]]:
        """Returns a list of unit ids that did not win, or None if game isn't over."""
        if self.state.game_over:
            return [b.id for b in self.bots if b.id != self.winner]
        return None

Subclasses

Instance variables

var bot_timer

A TurnTimer object for keeping track of bot calculation times.

var bots

Tuple of bot instances.

var description

A description of the battle.

var enable_bot_logging

Enable logging for the bots.

var enable_logging

Enable logging of the battle itself.

var history

A list of states.

var history_size

Size of state history.

Expand source code Browse git
@property
def history_size(self):
    """Size of state history."""
    return len(self.history)
var losers : Optional[list[int]]

Returns a list of unit ids that did not win, or None if game isn't over.

Expand source code Browse git
@property
def losers(self) -> Optional[list[int]]:
    """Returns a list of unit ids that did not win, or None if game isn't over."""
    if self.state.game_over:
        return [b.id for b in self.bots if b.id != self.winner]
    return None
var previous_state : Optional[State]

The state before the current one (or None if currently at first state).

Expand source code Browse git
@property
def previous_state(self) -> Optional[State]:
    """The state before the current one (or None if currently at first state)."""
    if self.history_size <= 1:
        return None
    return self.history[self.history_size - 2]
var stateState

The current state.

Expand source code Browse git
@property
def state(self) -> State:
    """The current state."""
    return self.__current_state
var winner : Optional[int]

Alias for State.winner of the current state.

Expand source code Browse git
@property
def winner(self) -> Optional[int]:
    """Alias for `botroyale.logic.state.State.winner` of the current state."""
    return self.state.winner

Methods

def get_state_str(self, state: State) ‑> str

A string representation of the type of state (bot turn / end of round).

Expand source code Browse git
def get_state_str(self, state: State) -> str:
    """A string representation of the type of state (bot turn / end of round)."""
    if state.end_of_round:
        return "end of round"
    assert state.current_unit is not None
    return f"{self.bots[state.current_unit].gui_label}'s turn"
def get_unit_str(self, unit_id: int) ‑> str

An internal repr for a unit.

Expand source code Browse git
def get_unit_str(self, unit_id: int) -> str:
    """An internal repr for a unit."""
    return self.bots[unit_id].gui_label
def log_state(self, state: State)

Log a quick summary of the current state.

Expand source code Browse git
def log_state(self, state: State):
    """Log a quick summary of the current state."""
    self.logger(
        "\n".join(
            [
                LINEBR,
                " ".join(
                    [
                        f"Step: {str(state.step_count):^4}",
                        f"Turn: {str(state.turn_count):^4}",
                        f"Round: {str(state.round_count):^3}",
                        "|",
                        f"{self.get_state_str(state)}",
                    ]
                ),
                LINEBR,
            ]
        )
    )
def logger(self, text: str)

Logger for the battle.

Expand source code Browse git
def logger(self, text: str):
    """Logger for the battle."""
    if self.enable_logging:
        glogger(text)
def play_all(self, disable_logging: bool = False, print_progress: bool = False)

Plays the battle to completion.

Args

disable_logging
Disable logging globally while playing.
print_progress
Disable logging globally while playing and print a progress bar to console.
Expand source code Browse git
def play_all(self, disable_logging: bool = False, print_progress: bool = False):
    """Plays the battle to completion.

    Args:
        disable_logging: Disable logging globally while playing.
        print_progress: Disable logging globally while playing and print a
            progress bar to console.
    """

    def print_progress_bar():
        rc = self.state.round_count
        done = "█" * rc
        remaining = "░" * (self.state.death_radius - 1)
        pbar = f"{done}{remaining}  ({rc} / {rc+self.state.death_radius-1} rounds)"
        print(f"\r{pbar}", end="")

    if print_progress:
        disable_logging = True
        print_progress_bar()
    last_rc = self.state.round_count
    with Logger.set_logging_temp(not disable_logging):
        while not self.state.game_over:
            last_rc = self.state.round_count
            self.play_state()
            if print_progress and self.state.round_count > last_rc:
                print_progress_bar()
    if print_progress:
        print("")
def play_state(self)

Plays the next state, and adds it to history.

Expand source code Browse git
def play_state(self):
    """Plays the next state, and adds it to history."""
    state = self.state
    self.log_state(state)
    if state.end_of_round:
        new_state = state.increment_round()
        self.logger(f"Death radius: {new_state.death_radius}")
    else:
        unit_id = state.current_unit
        action = self._get_bot_action(unit_id, state)
        if action is not None:
            # Bot returned an action, apply.
            self.logger(f"Applying {action} to state")
            if self.__only_bot_turn_states:
                new_state = state.apply_action(action)
            else:
                new_state = state.apply_action_manual(action)
                if not new_state.is_last_action_legal:
                    self.logger(f"ILLEGAL: {action}")
        else:
            # Bot failed to return an action, kill.
            self.logger(f"Killing {self.bots[unit_id]}...")
            if self.__only_bot_turn_states:
                new_state = state.apply_kill_unit()
            else:
                new_state = state.apply_kill_unit_manual()
    self.__current_state = new_state
    self.history.append(new_state)
    if new_state.round_count >= self.bot_timer.round_count:
        self.bot_timer.add_round()
def play_states(self, count: int)

Plays a number of states.

Expand source code Browse git
def play_states(self, count: int):
    """Plays a number of states."""
    while not self.state.game_over and count:
        self.play_state()
        count -= 1
class TurnTimer (num_of_units: int)

Records calculation times for multiple bots over multiple turns.

Initialize the class.

Expand source code Browse git
class TurnTimer:
    """Records calculation times for multiple bots over multiple turns."""

    def __init__(self, num_of_units: int):
        """Initialize the class."""
        self.num_of_units: int = num_of_units
        self.round_timers: np.ndarray = np.zeros((1, num_of_units), dtype=np.float64)

    @property
    def all_times(self) -> np.ndarray:
        """Return the full table of times (numpy array). Not a copy.

        The ndarray shape will be (num_of_rounds, num_of_units), such that every
        element in the array represents the time that unit has in that round.
        """
        return self.round_timers

    def rounds_played(self, unit_id: int) -> int:
        """Return the number of rounds that have times recorded for a unit."""
        return int(np.sum(self.round_timers[:, unit_id] > 0))

    def total(self, unit_id: int) -> float:
        """Return the total time recorded for a unit."""
        return float(np.sum(self.round_timers[:, unit_id]))

    def mean(self, unit_id: int) -> float:
        """Return the mean of times recorded for a unit.

        Assumes rounds with 0.0 time do not count.
        """
        rounds_played = self.rounds_played(unit_id)
        if rounds_played > 0:
            return float(self.total(unit_id) / rounds_played)
        return 0.0

    def max(self, unit_id: int) -> float:
        """Return the max of times recorded for a unit."""
        return float(np.max(self.round_timers[:, unit_id]))

    @property
    def round_count(self) -> int:
        """Number of rounds in the table."""
        return len(self.round_timers)

    def add_round(self):
        """Add a new round to the table."""
        new_row = np.zeros(self.num_of_units, dtype=np.float64)
        self.round_timers = np.vstack((self.round_timers, new_row))

    def add_time(self, unit_id: int, time: float, round: Optional[int] = None):
        """Add time for a unit at a given round.

        Will use the last round if none is provided.
        """
        if round is None:
            round = self.round_count - 1
        self.round_timers[round, unit_id] += time

    def get_time(self, unit_id: int, round: Optional[int] = None) -> float:
        """Get the time recorded for a unit at a given round (default: last round)."""
        if round is None:
            round = self.round_count - 1
        return float(self.round_timers[round, unit_id])

Instance variables

var all_times : numpy.ndarray

Return the full table of times (numpy array). Not a copy.

The ndarray shape will be (num_of_rounds, num_of_units), such that every element in the array represents the time that unit has in that round.

Expand source code Browse git
@property
def all_times(self) -> np.ndarray:
    """Return the full table of times (numpy array). Not a copy.

    The ndarray shape will be (num_of_rounds, num_of_units), such that every
    element in the array represents the time that unit has in that round.
    """
    return self.round_timers
var round_count : int

Number of rounds in the table.

Expand source code Browse git
@property
def round_count(self) -> int:
    """Number of rounds in the table."""
    return len(self.round_timers)

Methods

def add_round(self)

Add a new round to the table.

Expand source code Browse git
def add_round(self):
    """Add a new round to the table."""
    new_row = np.zeros(self.num_of_units, dtype=np.float64)
    self.round_timers = np.vstack((self.round_timers, new_row))
def add_time(self, unit_id: int, time: float, round: Optional[int] = None)

Add time for a unit at a given round.

Will use the last round if none is provided.

Expand source code Browse git
def add_time(self, unit_id: int, time: float, round: Optional[int] = None):
    """Add time for a unit at a given round.

    Will use the last round if none is provided.
    """
    if round is None:
        round = self.round_count - 1
    self.round_timers[round, unit_id] += time
def get_time(self, unit_id: int, round: Optional[int] = None) ‑> float

Get the time recorded for a unit at a given round (default: last round).

Expand source code Browse git
def get_time(self, unit_id: int, round: Optional[int] = None) -> float:
    """Get the time recorded for a unit at a given round (default: last round)."""
    if round is None:
        round = self.round_count - 1
    return float(self.round_timers[round, unit_id])
def max(self, unit_id: int) ‑> float

Return the max of times recorded for a unit.

Expand source code Browse git
def max(self, unit_id: int) -> float:
    """Return the max of times recorded for a unit."""
    return float(np.max(self.round_timers[:, unit_id]))
def mean(self, unit_id: int) ‑> float

Return the mean of times recorded for a unit.

Assumes rounds with 0.0 time do not count.

Expand source code Browse git
def mean(self, unit_id: int) -> float:
    """Return the mean of times recorded for a unit.

    Assumes rounds with 0.0 time do not count.
    """
    rounds_played = self.rounds_played(unit_id)
    if rounds_played > 0:
        return float(self.total(unit_id) / rounds_played)
    return 0.0
def rounds_played(self, unit_id: int) ‑> int

Return the number of rounds that have times recorded for a unit.

Expand source code Browse git
def rounds_played(self, unit_id: int) -> int:
    """Return the number of rounds that have times recorded for a unit."""
    return int(np.sum(self.round_timers[:, unit_id] > 0))
def total(self, unit_id: int) ‑> float

Return the total time recorded for a unit.

Expand source code Browse git
def total(self, unit_id: int) -> float:
    """Return the total time recorded for a unit."""
    return float(np.sum(self.round_timers[:, unit_id]))