Module botroyale.gui.app

The GUI application itself.

Has two screens: main menu, and battle. Each is associated with an API object (GameAPI, and BattleAPI respectively) provided by the game logic that instructs the GUI. Each screen fills the entire window and manages its own XInputManager and menu bar. The App is mostly responsible for running the internal mainloop, and switching between the screens (allowing them to de/activate).

See: MainMenuScreen and BattleScreen.

Expand source code Browse git
"""The GUI application itself.

Has two screens: main menu, and battle. Each is associated with an API object
(GameAPI, and BattleAPI respectively) provided by the game logic that instructs
the GUI. Each screen fills the entire window and manages its own
`botroyale.gui.kex.widgets.input_manager.XInputManager` and menu bar. The `App`
is mostly responsible for running the internal mainloop, and switching between
the screens (allowing them to de/activate).

See: `botroyale.gui.menu.MainMenuScreen` and `botroyale.gui.battle.BattleScreen`.
"""
from typing import Optional
from collections import deque
from functools import partial
from botroyale.util import PACKAGE_DIR, settings
from botroyale.util.file import popen_path, get_usr_dir
from botroyale.util.time import RateCounter
from botroyale.api.gui import GameAPI, BattleAPI, Control, Overlay
from botroyale.gui import (
    kex as kx,
    register_controls,
    logger,
    hotkey_logger,
)
from botroyale.gui.menu import MainMenuScreen
from botroyale.gui.battle import BattleScreen


ICON = str(PACKAGE_DIR / "icon.ico")
FPS = settings.get("gui.fps")
WINDOW_SIZE = settings.get("gui.window_size")
WINDOW_POS = settings.get("gui.window_pos")
START_MAXIMIZED = settings.get("gui.window_maximize")
TRANSITION_SPEED = settings.get("gui.transition_speed")


class App(kx.App):
    """See module documentation for details."""

    def __init__(self, game_api: GameAPI, **kwargs):
        """Initialize with a *game_api*."""
        logger("Starting app...")
        assert isinstance(game_api, GameAPI)
        # Kivy app configuration
        super().__init__(**kwargs)
        self.title = "Bot Royale"
        self.icon = ICON
        kx.Window.set_size(*WINDOW_SIZE)
        if any(c >= 0 for c in WINDOW_POS):
            kx.Window.set_position(*WINDOW_POS)
        if START_MAXIMIZED:
            # Schedule maximizing so that the resize happens first, otherwise
            # the resize has no effect.
            kx.schedule_once(kx.Window.maximize)
        # Setup
        self.queued_overlays = deque()
        self._overlay_cooldown = 0
        self.game_api = game_api
        self.fps_counter = RateCounter(sample_size=FPS, starting_elapsed=1000 / FPS)
        # Make widgets
        self.im = kx.InputManager(
            name="App",
            logger=hotkey_logger,
            log_callback=True,
            log_press=True,
            log_release=True,
        )
        register_controls(self.im, self.get_controls())
        self.sm = self.add(kx.ScreenManager(transition_speed=TRANSITION_SPEED))
        self.menu = MainMenuScreen(
            app_controls=self.get_controls(),
            api=self.game_api,
            start_new_battle=self._start_new_battle,
        )
        self.battle = BattleScreen(
            app_controls=self.get_controls(),
            return_to_menu=partial(self.switch_screen, "menu"),
        )
        self.sm.add_screen("menu", self.menu)
        self.sm.add_screen("battle", self.battle)
        self.screen_frames = {
            "menu": self.menu,
            "battle": self.battle,
        }
        self._activate_current_screen()
        # Start mainloop
        logger("GUI initialized, starting mainloop.")
        self.hook(self.update, FPS)

    def _start_new_battle(self, api: BattleAPI):
        self.battle.start_new_battle(api)
        self.switch_screen("battle", force=True)

    def update(self, dt):
        """Called every frame."""
        if self.queued_overlays:
            if self.overlay is not None:
                return
            overlay = self.queued_overlays.popleft()
            assert isinstance(overlay, Overlay)
            logger(f"Executing {overlay=}")
            self.with_overlay(overlay.func, text=overlay.text, after=overlay.after)
            return
        self.fps_counter.tick()
        if self.sm.current == "menu":
            self.menu.update()
        elif self.sm.current == "battle":
            self.battle.update()

    def switch_screen(
        self,
        screen: str,
        force: bool = False,
        _attempt: int = 0,
    ):
        """Switch and activate a screen, deactivating others."""
        interval = 0.05
        timeout = 2
        if (_attempt * interval) > timeout:
            logger(f"Attempted to switch to screen {screen} but timed out.")
            return
        switch_success = self.sm.switch_name(screen)
        if not switch_success:
            if force:
                logger(
                    f"Cannot switch screen {self.sm.mid_transition=}, "
                    f"trying again in 50ms (attempt: {_attempt})..."
                )
                kx.schedule_once(
                    lambda *args: self.switch_screen(
                        screen=screen,
                        force=force,
                        _attempt=_attempt + 1,
                    ),
                    interval,
                )
            return
        kx.schedule_once(self._activate_current_screen)
        logger(f"Switched to screen: {screen}")

    def _activate_current_screen(self, *args):
        current_screen = self.sm.current
        for screen_name, frame in self.screen_frames.items():
            if screen_name == current_screen:
                frame.activate()
            else:
                frame.deactivate()
        self._hotkey_debug()

    def get_controls(self):
        """Global app controls."""
        return [
            Control(
                "App",
                "Main Menu",
                partial(self.switch_screen, "menu"),
                ["f1", "escape"],
            ),
            Control("App", "Battle", partial(self.switch_screen, "battle"), "f2"),
            Control("App", "User folder", _show_usrdir, "^+ f"),
            Control("App", "Debug", self._debug, "!+ d"),
            Control("App", "Restart", kx.restart_script, "^+ w"),
            Control("App", "Quit", quit, "^+ q"),
        ]

    def _debug(self, *args):
        """GUI Debug."""
        logger("GUI DEBUG")
        print(f"{self.current_focus=}")
        self._hotkey_debug()

    def _hotkey_debug(self, *args):
        """Debug logs."""
        hotkey_logger(
            "\n".join(
                [
                    "=" * 50,
                    f"Current screen: {self.sm.current}",
                    "",
                    self.im._debug_str,
                    "",
                    self.menu.im._debug_str,
                    "",
                    self.battle.im._debug_str,
                    "=" * 50,
                ]
            )
        )

    def overlay_calls(self, overlays: Optional[list[Overlay]]):
        """Queue calling functions while displaying an overlay."""
        if overlays is None:
            return
        self.queued_overlays.extend(overlays)


def _show_usrdir(*args):
    """Open the user's directory."""
    usrdir = get_usr_dir("subfolder").parent
    popen_path(usrdir)

Classes

class App (game_api: GameAPI, **kwargs)

See module documentation for details.

Initialize with a game_api.

Expand source code Browse git
class App(kx.App):
    """See module documentation for details."""

    def __init__(self, game_api: GameAPI, **kwargs):
        """Initialize with a *game_api*."""
        logger("Starting app...")
        assert isinstance(game_api, GameAPI)
        # Kivy app configuration
        super().__init__(**kwargs)
        self.title = "Bot Royale"
        self.icon = ICON
        kx.Window.set_size(*WINDOW_SIZE)
        if any(c >= 0 for c in WINDOW_POS):
            kx.Window.set_position(*WINDOW_POS)
        if START_MAXIMIZED:
            # Schedule maximizing so that the resize happens first, otherwise
            # the resize has no effect.
            kx.schedule_once(kx.Window.maximize)
        # Setup
        self.queued_overlays = deque()
        self._overlay_cooldown = 0
        self.game_api = game_api
        self.fps_counter = RateCounter(sample_size=FPS, starting_elapsed=1000 / FPS)
        # Make widgets
        self.im = kx.InputManager(
            name="App",
            logger=hotkey_logger,
            log_callback=True,
            log_press=True,
            log_release=True,
        )
        register_controls(self.im, self.get_controls())
        self.sm = self.add(kx.ScreenManager(transition_speed=TRANSITION_SPEED))
        self.menu = MainMenuScreen(
            app_controls=self.get_controls(),
            api=self.game_api,
            start_new_battle=self._start_new_battle,
        )
        self.battle = BattleScreen(
            app_controls=self.get_controls(),
            return_to_menu=partial(self.switch_screen, "menu"),
        )
        self.sm.add_screen("menu", self.menu)
        self.sm.add_screen("battle", self.battle)
        self.screen_frames = {
            "menu": self.menu,
            "battle": self.battle,
        }
        self._activate_current_screen()
        # Start mainloop
        logger("GUI initialized, starting mainloop.")
        self.hook(self.update, FPS)

    def _start_new_battle(self, api: BattleAPI):
        self.battle.start_new_battle(api)
        self.switch_screen("battle", force=True)

    def update(self, dt):
        """Called every frame."""
        if self.queued_overlays:
            if self.overlay is not None:
                return
            overlay = self.queued_overlays.popleft()
            assert isinstance(overlay, Overlay)
            logger(f"Executing {overlay=}")
            self.with_overlay(overlay.func, text=overlay.text, after=overlay.after)
            return
        self.fps_counter.tick()
        if self.sm.current == "menu":
            self.menu.update()
        elif self.sm.current == "battle":
            self.battle.update()

    def switch_screen(
        self,
        screen: str,
        force: bool = False,
        _attempt: int = 0,
    ):
        """Switch and activate a screen, deactivating others."""
        interval = 0.05
        timeout = 2
        if (_attempt * interval) > timeout:
            logger(f"Attempted to switch to screen {screen} but timed out.")
            return
        switch_success = self.sm.switch_name(screen)
        if not switch_success:
            if force:
                logger(
                    f"Cannot switch screen {self.sm.mid_transition=}, "
                    f"trying again in 50ms (attempt: {_attempt})..."
                )
                kx.schedule_once(
                    lambda *args: self.switch_screen(
                        screen=screen,
                        force=force,
                        _attempt=_attempt + 1,
                    ),
                    interval,
                )
            return
        kx.schedule_once(self._activate_current_screen)
        logger(f"Switched to screen: {screen}")

    def _activate_current_screen(self, *args):
        current_screen = self.sm.current
        for screen_name, frame in self.screen_frames.items():
            if screen_name == current_screen:
                frame.activate()
            else:
                frame.deactivate()
        self._hotkey_debug()

    def get_controls(self):
        """Global app controls."""
        return [
            Control(
                "App",
                "Main Menu",
                partial(self.switch_screen, "menu"),
                ["f1", "escape"],
            ),
            Control("App", "Battle", partial(self.switch_screen, "battle"), "f2"),
            Control("App", "User folder", _show_usrdir, "^+ f"),
            Control("App", "Debug", self._debug, "!+ d"),
            Control("App", "Restart", kx.restart_script, "^+ w"),
            Control("App", "Quit", quit, "^+ q"),
        ]

    def _debug(self, *args):
        """GUI Debug."""
        logger("GUI DEBUG")
        print(f"{self.current_focus=}")
        self._hotkey_debug()

    def _hotkey_debug(self, *args):
        """Debug logs."""
        hotkey_logger(
            "\n".join(
                [
                    "=" * 50,
                    f"Current screen: {self.sm.current}",
                    "",
                    self.im._debug_str,
                    "",
                    self.menu.im._debug_str,
                    "",
                    self.battle.im._debug_str,
                    "=" * 50,
                ]
            )
        )

    def overlay_calls(self, overlays: Optional[list[Overlay]]):
        """Queue calling functions while displaying an overlay."""
        if overlays is None:
            return
        self.queued_overlays.extend(overlays)

Ancestors

  • XApp
  • XWidget
  • kivy.app.App
  • kivy._event.EventDispatcher
  • kivy._event.ObjectWithUid

Methods

def get_controls(self)

Global app controls.

Expand source code Browse git
def get_controls(self):
    """Global app controls."""
    return [
        Control(
            "App",
            "Main Menu",
            partial(self.switch_screen, "menu"),
            ["f1", "escape"],
        ),
        Control("App", "Battle", partial(self.switch_screen, "battle"), "f2"),
        Control("App", "User folder", _show_usrdir, "^+ f"),
        Control("App", "Debug", self._debug, "!+ d"),
        Control("App", "Restart", kx.restart_script, "^+ w"),
        Control("App", "Quit", quit, "^+ q"),
    ]
def overlay_calls(self, overlays: Optional[list[Overlay]])

Queue calling functions while displaying an overlay.

Expand source code Browse git
def overlay_calls(self, overlays: Optional[list[Overlay]]):
    """Queue calling functions while displaying an overlay."""
    if overlays is None:
        return
    self.queued_overlays.extend(overlays)
def switch_screen(self, screen: str, force: bool = False)

Switch and activate a screen, deactivating others.

Expand source code Browse git
def switch_screen(
    self,
    screen: str,
    force: bool = False,
    _attempt: int = 0,
):
    """Switch and activate a screen, deactivating others."""
    interval = 0.05
    timeout = 2
    if (_attempt * interval) > timeout:
        logger(f"Attempted to switch to screen {screen} but timed out.")
        return
    switch_success = self.sm.switch_name(screen)
    if not switch_success:
        if force:
            logger(
                f"Cannot switch screen {self.sm.mid_transition=}, "
                f"trying again in 50ms (attempt: {_attempt})..."
            )
            kx.schedule_once(
                lambda *args: self.switch_screen(
                    screen=screen,
                    force=force,
                    _attempt=_attempt + 1,
                ),
                interval,
            )
        return
    kx.schedule_once(self._activate_current_screen)
    logger(f"Switched to screen: {screen}")
def update(self, dt)

Called every frame.

Expand source code Browse git
def update(self, dt):
    """Called every frame."""
    if self.queued_overlays:
        if self.overlay is not None:
            return
        overlay = self.queued_overlays.popleft()
        assert isinstance(overlay, Overlay)
        logger(f"Executing {overlay=}")
        self.with_overlay(overlay.func, text=overlay.text, after=overlay.after)
        return
    self.fps_counter.tick()
    if self.sm.current == "menu":
        self.menu.update()
    elif self.sm.current == "battle":
        self.battle.update()

Inherited members