Complete Roguelike Tutorial, using python3+pysdl2, part 0 code

From RogueBasin
Jump to navigation Jump to search

This is part of a series of tutorials; the main page can be found here.

time

util/time.py: available a this tutorial's github

manager

./manager.py

"""Basic scene manager."""

import ctypes
import os

os.environ["PYSDL2_DLL_PATH"] = "C:\\lib\\SDL2-2.0.5-win32-x86"

import sdl2
import sdl2.ext

from constants import (SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE, LIMIT_FPS,
                       WINDOW_COLOR)
from util.time import Clock


__all__ = ("Manager", "KeyboardStateController", "SceneBase", "Resources")


Resources = sdl2.ext.Resources(
    os.path.join(os.path.dirname(__file__), "resources"))


class Manager(object):
    """Manage scenes and the main game loop.

    At each loop the events are passed down to the active scene and it's
    update method is called.
    """

    def __init__(
        self, width=None, height=None, cols=None, rows=None, tile_size=None,
        limit_fps=None, window_color=None
    ):
        """Initialization.

        Args:
            width (int): the width of the screen in pixels. Defaults to
                constants.SCREEN_WIDTH
            height (int): the height of the screen in pixels. Defaults to
                constants.SCREEN_HEIGHT
            tile_size (int): size of a (square) tile's side in pixels.
                Defaults to constants.TILE_SIZE
            limit_fps (int): maximum frames per second that should be drawn.
                Defaults to constants.LIMIT_FPS
            window_color (4-tuple): the window's background color, as a tuple
                of 4 integers representing Red, Greehn, Blue and Alpha values
                (0-255). Defaults to constants.WINDOW_COLOR

        Usage:
            m = Manager()  # start with default parameters
            m.set_scene(SceneBase)  # set a scene. This is a blank base scene
            m.execute()  # call the main loop
        """
        # Set the default arguments
        self.width = width or SCREEN_WIDTH
        self.height = height or SCREEN_HEIGHT
        self.tile_size = tile_size or TILE_SIZE
        self.limit_fps = limit_fps or LIMIT_FPS
        self.window_color = window_color or WINDOW_COLOR

        # Number of tile_size-sized drawable columns and rows on screen
        self.cols = self.width // self.tile_size
        self.rows = self.height // self.tile_size

        # Initialize with no scene
        self.scene = None

        # Initialize the video system - this implicitly initializes some
        # necessary parts within the SDL2 DLL used by the video module.
        #
        # You SHOULD call this before using any video related methods or
        # classes.
        sdl2.ext.init()

        # Create a new window (like your browser window or editor window,
        # etc.) and give it a meaningful title and size. We definitely need
        # this, if we want to present something to the user.
        self.window = sdl2.ext.Window(
            "Tiles", size=(self.width, self.height),
            flags=sdl2.SDL_WINDOW_BORDERLESS)

        # Create a renderer that supports hardware-accelerated sprites.
        self.renderer = sdl2.ext.Renderer(self.window)

        # Create a sprite factory that allows us to create visible 2D elements
        # easily.
        self.factory = sdl2.ext.SpriteFactory(
            sdl2.ext.TEXTURE, renderer=self.renderer)

        # Creates a simple rendering system for the Window. The
        # SpriteRenderSystem can draw Sprite objects on the window.
        self.spriterenderer = self.factory.create_sprite_render_system(
            self.window)

        # By default, every Window is hidden, not shown on the screen right
        # after creation. Thus we need to tell it to be shown now.
        self.window.show()

        # Enforce window raising just to be sure.
        sdl2.SDL_RaiseWindow(self.window.window)

        # Initialize the keyboard state controller.
        # PySDL2/SDL2 shouldn't need this but the basic procedure for getting
        # key mods and locks is not working for me atm.
        # So I've implemented my own controller.
        self.kb_state = KeyboardStateController()

        # Initialize a mouse starting position. From here on the manager will
        # be able to work on distances from previous positions.
        self._get_mouse_state()

        # Initialize a clock utility to help us control the framerate
        self.clock = Clock()

        # Make the Manager alive. This is used on the main loop.
        self.alive = True

    def _get_mouse_state(self):
        """Get the mouse state.

        This is only required during initialization. Later on the mouse
        position will be passed through events.
        """
        # This is an example of what PySDL2, below the hood, does for us.
        # Here we create a ctypes int (i.e. a C type int)
        x = ctypes.c_int(0)
        y = ctypes.c_int(0)
        # And pass it by reference to the SDL C function (i.e. pointers)
        sdl2.mouse.SDL_GetMouseState(ctypes.byref(x), ctypes.byref(y))
        # The variables were modified by SDL, but are still of C type
        # So we need to get their values as python integers
        self._mouse_x = x.value
        self._mouse_y = y.value
        # Now we hope we're never going to deal with this kind of stuff again
        return self._mouse_x, self._mouse_y

    def run(self):
        """Main loop handling events and updates."""
        while self.alive:
            self.clock.tick(self.limit_fps)
            self.on_event()
            self.on_update()
        return sdl2.ext.quit()

    def on_event(self):
        """Handle the events and pass them to the active scene."""
        scene = self.scene

        if scene is None:
            return
        for event in sdl2.ext.get_events():

            # Exit events
            if event.type == sdl2.SDL_QUIT:
                self.alive = False
                return

            # Redraw in case the focus was lost and now regained
            if event.type == sdl2.SDL_WINDOWEVENT_FOCUS_GAINED:
                self.on_update()
                continue

            # on_mouse_motion, on_mouse_drag
            if event.type == sdl2.SDL_MOUSEMOTION:
                x = event.motion.x
                y = event.motion.y
                buttons = event.motion.state
                self._mouse_x = x
                self._mouse_y = y
                dx = x - self._mouse_x
                dy = y - self._mouse_y
                if buttons & sdl2.SDL_BUTTON_LMASK:
                    scene.on_mouse_drag(event, x, y, dx, dy, "LEFT")
                elif buttons & sdl2.SDL_BUTTON_MMASK:
                    scene.on_mouse_drag(event, x, y, dx, dy, "MIDDLE")
                elif buttons & sdl2.SDL_BUTTON_RMASK:
                    scene.on_mouse_drag(event, x, y, dx, dy, "RIGHT")
                else:
                    scene.on_mouse_motion(event, x, y, dx, dy)
                continue
            # on_mouse_press
            elif event.type == sdl2.SDL_MOUSEBUTTONDOWN:
                x = event.button.x
                y = event.button.y

                button_n = event.button.button
                if button_n == sdl2.SDL_BUTTON_LEFT:
                    button = "LEFT"
                elif button_n == sdl2.SDL_BUTTON_RIGHT:
                    button = "RIGHT"
                elif button_n == sdl2.SDL_BUTTON_MIDDLE:
                    button = "MIDDLE"

                double = bool(event.button.clicks - 1)

                scene.on_mouse_press(event, x, y, button, double)
                continue
            # on_mouse_scroll (wheel)
            elif event.type == sdl2.SDL_MOUSEWHEEL:
                offset_x = event.wheel.x
                offset_y = event.wheel.y
                scene.on_mouse_scroll(event, offset_x, offset_y)
                continue

            # for keyboard input, set the key symbol and keyboard modifiers
            mod = self.kb_state.process(event)
            sym = event.key.keysym.sym

            # on_key_release
            if event.type == sdl2.SDL_KEYUP:
                scene.on_key_release(event, sym, mod)
            # on_key_press
            elif event.type == sdl2.SDL_KEYDOWN:
                scene.on_key_press(event, sym, mod)

    def on_update(self):
        """Update the active scene."""
        scene = self.scene
        if self.alive:
            # clear the window with its color
            self.renderer.clear(self.window_color)
            if scene:
                # call the active scene's on_update
                scene.on_update()
            # present what we have to the screen
            self.present()

    def present(self):
        """Flip the GPU buffer."""
        sdl2.render.SDL_RenderPresent(self.spriterenderer.sdlrenderer)

    def set_scene(self, scene=None, **kwargs):
        """Set the scene.

        Args:
            scene (SceneBase): the scene to be initialized
            kwargs: the arguments that should be passed to the scene

        """
        self.scene = scene(manager=self, **kwargs)


class KeyboardStateController:
    """A class that keeps track of keyboard modifiers and locks."""

    def __init__(self):
        """Initialization."""
        self._shift = False
        self._ctrl = False
        self._alt = False
        self.caps = False
        self.num = False
        self.scroll = False

    def contains(self, *args):
        """..."""
        d = {arg: True for arg in args}
        return self.combine(**d)

    @property
    def alt(self):
        """..."""
        return self.combine(ctrl=True)

    @property
    def ctrl(self):
        """..."""
        return self.combine(ctrl=True)

    @property
    def shift(self):
        """..."""
        return self.combine(shift=True)

    def combine(self, alt=False, ctrl=False, shift=False):
        """..."""
        return all(
            (self._alt == alt,
             self._ctrl == ctrl,
             self._shift == shift)
        )

    def process(self, event):
        """Process the current event and update the keyboard state."""
        down = True if event.type == sdl2.SDL_KEYDOWN else False
        self._process_mods(event.key.keysym.sym, down)
        if not down:
            self._process_locks(event.key.keysym.sym)
        return self

    def _process_locks(self, key):
        """Process the locks."""
        for lock, sym in (
            ("caps", sdl2.SDLK_CAPSLOCK),
            ("num", sdl2.SDLK_NUMLOCKCLEAR),
            ("scroll", sdl2.SDLK_SCROLLLOCK)
        ):
            if key == sym:
                _prev_lock = getattr(self, lock)
                setattr(self, lock, not _prev_lock)

    def _process_mods(self, key, down):
        """Process the modifiers."""
        for mod, syms in (
            ("_ctrl", (sdl2.SDLK_LCTRL, sdl2.SDLK_RCTRL)),
            ("_shift", (sdl2.SDLK_LSHIFT, sdl2.SDLK_RSHIFT)),
            ("_alt", (sdl2.SDLK_LALT, sdl2.SDLK_RALT))
        ):
            if key in syms:
                setattr(self, mod, down)

    def __getstate__(self):
        """Prevent pickling."""
        return None

    def __repr__(self):
        """Representation of keyboard states."""
        return (
            "alt: %r, ctrl: %r, shift: %r, caps: %r, num: %r, scroll %r" %
            (self.alt, self.ctrl, self.shift, self.caps, self.num,
             self.scroll))


class SceneBase(object):
    """Basic scene of the game.

    New Scenes should be subclasses of SceneBase.
    """

    def __new__(cls, manager, **kwargs):
        """Create a new instance of a scene.

        A reference to the manager is stored before returning the instance.
        This is made preventively because many properties are related to the
        manager.

        Args:
            manager (Manager): the running instance of the Manager
        """
        scene = super().__new__(cls)
        scene.manager = manager
        return scene

    def __init__(self, **kwargs):
        """Initialization."""
        pass

    # properties
    @property
    def height(self):
        """Main window height.

        Returns:
            Manager.height
        """
        return self.manager.height

    @property
    def width(self):
        """Main window width.

        Returns:
            Manager.height
        """
        return self.manager.width

    @property
    def factory(self):
        """Reference to sdl2.ext.SpriteFactory instance.

        Returns:
            Manager.factory
        """
        return self.manager.factory

    @property
    def kb_state(self):
        """Reference to KeyboardStateController instance.

        Returns:
            Manager.kb_state
        """
        return self.manager.kb_state

    @property
    def renderer(self):
        """Reference to sdl2.ext.Renderer instance.

        Returns:
            Manager.renderer

        """
        return self.manager.renderer

    @property
    def sdlrenderer(self):
        """Reference to sdl2.SDL_Renderer instance.

        Returns:
            Manager.renderer.sdlrenderer
        """
        return self.manager.renderer.sdlrenderer

    @property
    def spriterenderer(self):
        """Reference to sdl2.ext.TextureSpriteRenderSystem instance.

        Returns:
            Manager.spriterenderer
        """
        return self.manager.spriterenderer

    # other methods
    def quit(self):
        """Stop the manager main loop."""
        self.manager.alive = False

    # event methods
    def on_key_press(self, event, sym, mod):
        """Called on keyboard input, when a key is **held down**.

        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                Unless specifically needed, sym and mod should be used
                instead.
            sym (int): Integer representing code of the key pressed. For
                printable keys ``chr(key)`` should return the corresponding
                character.
            mod (KeyboardStateController): the keyboard state for modifiers
                and locks. See :class:KeyboardStateController
        """
        pass

    def on_key_release(self, event, sym, mod):
        """Called on keyboard input, when a key is **released**.

        By default if the Escape key is pressed the manager quits.
        If that behaviour is desired you can call ``super().on_key_release(
        event, sym, mod)`` on a child class.

        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            sym (int): Integer representing code of the key pressed. For
                printable keys ``chr(key)`` should return the corresponding
                character.
            mod (KeyboardStateController): the keyboard state for modifiers
                and locks. See :class:KeyboardStateController
        """
        if sym == sdl2.SDLK_ESCAPE:
            self.quit()

    def on_mouse_drag(self, event, x, y, dx, dy, button):
        """Called when mouse buttons are pressed and the mouse is dragged.

        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            x (int): horizontal coordinate, relative to window.
            y (int): vertical coordinate, relative to window.
            dx (int): relative motion in the horizontal direction
            dy (int): relative motion in the vertical direction
            button (str, "RIGHT"|"MIDDLE"|"LEFT"): string representing the
                button pressed.
        """
        pass

    def on_mouse_motion(self, event, x, y, dx, dy):
        """Called when the mouse is moved.

        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            x (int): horizontal coordinate, relative to window.
            y (int): vertical coordinate, relative to window.
            dx (int): relative motion in the horizontal direction
            dy (int): relative motion in the vertical direction
        """
        pass

    def on_mouse_press(self, event, x, y, button, double):
        """Called when mouse buttons are pressed.

        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            x (int): horizontal coordinate, relative to window.
            y (int): vertical coordinate, relative to window.
            button (str, "RIGHT"|"MIDDLE"|"LEFT"): string representing the
                button pressed.
            double (bool, True|False): boolean indicating if the click was a
                double click.
        """
        pass

    def on_mouse_scroll(self, event, offset_x, offset_y):
        """Called when the mouse wheel is scrolled.

        Args:
            event (sdl2.events.SDL_Event): The base event, as passed by SDL2.
                The other arguments should be used for a higher level
                interaction, unless specifically needed.
            offset_x (int): the amount scrolled horizontally, positive to the
                right and negative to the left.
            offset_y (int): the amount scrolled vertically, positive away
                from the user and negative toward the user.
        """
        pass

    def on_update(self):
        """Graphical logic."""
        pass


if __name__ == '__main__':
    # example, with a borderless yet ugly green window
    m = Manager(window_color=(0, 255, 0, 255))
    m.set_scene(scene=SceneBase)
    m.run()