Skip to content

Commit

Permalink
Majorly refactor the code base
Browse files Browse the repository at this point in the history
This refactoring is intended to give the package hierarchy a more flat
structure. Some stylistic problems such as extra newlines at the end of
files were also fixed. More refactoring and cleanup will likely have to
be done in the coming commits, but this is the first and largest pass in
this effort.
  • Loading branch information
binyomen committed Jul 8, 2017
1 parent 402c185 commit 083142e
Show file tree
Hide file tree
Showing 18 changed files with 447 additions and 436 deletions.
8 changes: 8 additions & 0 deletions pyrogi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ def get_caption():
def get_mouse_position():
return _backend.mouse_position

# Expose all the root package's public members.
import pyrogi.drawing
import pyrogi.ui
import pyrogi.util

from .core import Backend, Screen
from .events import LEFT_BUTTON, MIDDLE_BUTTON, RIGHT_BUTTON, SCROLL_WHEEL_UP, SCROLL_WHEEL_DOWN
from .events import Event, KeyDownEvent, KeyUpEvent, MouseMovedEvent, MouseButtonDownEvent, MouseButtonUpEvent, MouseWheelScrolledEvent
173 changes: 173 additions & 0 deletions pyrogi/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""A package defining all the backend (i.e. non-graphical) work of the engine.
Thus, most of the implementation of the engine is underneath the :mod:`backend`
package.
The most important class exposed by the backend package is :class:`Backend`.
This class is central to the functioning of the entire pyrogi backend.
"""
import pyrogi
from pyrogi.ui import UIElementContainer
from pyrogi.util import Vec2

class Backend(object):
"""The :class:`Backend` class is intended to be extended (for example, by
:class:`PyGameBackend`). :class:`Backend` forwards events such as key
events and ticks on to the current :class:`Screen`. Its implementations are
expected to implement the :meth:`run` method, however, which should start
up the main loop for the backend.
The :code:`__init__` method of a :class:`Backend` is as follows:
:param window_dimensions: The dimensions, in terms of of tiles, for the
game window to be.
:type window_dimensions: Vec2
:param tile_dimensions: The dimensions, in terms of pixels, for each tile
to be.
:type tile_dimensions: Vec2
:param caption: The title text of the game window.
:type caption: str
"""

def __init__(self, window_dimensions, tile_dimensions, caption):
pyrogi._backend = self
self.window_dimensions = window_dimensions
self.tile_dimensions = tile_dimensions
self.caption = caption
self.mouse_position = Vec2(0, 0)
self.screens = []

def run(self):
"""Run the main loop for pyrogi.
This must be implemented by subclasses of :class:`Backend`.
"""
raise NotImplementedError()

def on_tick(self, millis):
"""Called by the :class:`Backend` implementation when a tick should
occur in the game loop. This should never be overriden, but can be
extended if the :class:`Backend` implementation wishes to do something
every tick. The tick is passed on to the backend's current screen.
:param millis: The number of milliseconds since the last tick.
:type millis: int
"""
self.get_current_screen().on_tick(millis)
def on_draw(self, g):
"""Called by the :class:`Backend` implementation when a draw should
occur in the game loop. This should never be overriden, but can be
extended if the :class:`Backend` implementation wishes to do something
every draw. The draw is passed on to the backend's current screen.
:param g: The :class:`Graphics` object to be used to perform the
drawing.
:type g: Graphics
"""
self.get_current_screen().on_draw(g)

def get_current_screen(self):
""":returns: The screen currently set in the engine.
:rtype: Screen
"""
return self.screens[-1]
def set_screen(self, screen):
"""Sets the current screen in the engine. It is added to a stack of
other screens, which maintains history and allows the engine to return
to previous screens.
:param screen: The screen to be added to the stack.
:type screen: Screen
"""
self.screens.append(screen)
def go_back_n_screens(self, n):
"""Return to the nth screen in the stack, popping off screens as you
go. Because screens are removed from the stack to reach the nth most
recent screen, they cannot be returned to without constructing new
ones.
:param n: The number of screens to go back.
:type n: int
"""
for i in range(n):
self.screens.pop()

def handle_key_down(self, event):
"""Called by the :class:`Backend` implementation when a key down event
has occured. This should never be overriden, but can be extended if the
:class:`Backend` implementation wishes to do something every time the
event is triggered. The event is passed on to the backend's current
screen.
:param event: The event triggered.
:type event: KeyDownEvent
"""
self.get_current_screen().handle_key_down(event)
def handle_key_up(self, event):
"""Called by the :class:`Backend` implementation when a key up event
has occured. This should never be overriden, but can be extended if the
:class:`Backend` implementation wishes to do something every time the
event is triggered. The event is passed on to the backend's current
screen.
:param event: The event triggered.
:type event: KeyUpEvent
"""
self.get_current_screen().handle_key_up(event)
def handle_mouse_moved(self, event):
"""Called by the :class:`Backend` implementation when a mouse moved
event has occured. This should never be overriden, but can be extended
if the :class:`Backend` implementation wishes to do something every
time the event is triggered. The event is passed on to the backend's
current screen.
:param event: The event triggered.
:type event: MouseMovedEvent
"""
self.mouse_position = event.position
self.get_current_screen().handle_mouse_moved(event)
def handle_mouse_button_down(self, event):
"""Called by the :class:`Backend` implementation when a mouse button
down event has occured. This should never be overriden, but can be
extended if the :class:`Backend` implementation wishes to do something
every time the event is triggered. The event is passed on to the
backend's current screen.
:param event: The event triggered.
:type event: MouseButtonDownEvent
"""
self.get_current_screen().handle_mouse_button_down(event)
def handle_mouse_button_up(self, event):
"""Called by the :class:`Backend` implementation when a mouse button up
event has occured. This should never be overriden, but can be extended
if the :class:`Backend` implementation wishes to do something every
time the event is triggered. The event is passed on to the backend's
current screen.
:param event: The event triggered.
:type event: MouseButtonUpEvent
"""
self.get_current_screen().handle_mouse_button_up(event)
def handle_mouse_wheel_scrolled(self, event):
"""Called by the :class:`Backend` implementation when a mouse wheel
scrolled has occured. This should never be overriden, but can be
extended if the :class:`Backend` implementation wishes to do something
every time the event is triggered. The event is passed on to the
backend's current screen.
:param event: The event triggered.
:type event: MouseWheelScrolledEvent
"""
self.get_current_screen().handle_mouse_wheel_scrolled(event)


class Screen(UIElementContainer):
"""The :class:`Screen` class is the top level :class:`UIElementContainer`.
It represents a single screen in a game. An example may be the menu screen,
the main game screen, or the shop screen if you are placed in a view
separate from the game world to shop. Because they are the top level
container, a screen's position is always (0, 0), and its dimensions is
always equal to that of the game window. A stack of screens is maintained
in the :class:`Backend` object.
"""
def __init__(self):
super(Screen, self).__init__(Vec2(0, 0))
2 changes: 2 additions & 0 deletions pyrogi/drawing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .drawing import Color, Drawable, Graphics, Paint, Tile
from .paints import GradientPaint, LinearGradientPaint, SolidPaint
113 changes: 34 additions & 79 deletions pyrogi/graphics/__init__.py → pyrogi/drawing/drawing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import math
from operator import attrgetter
from pyrogi.util import Vec2

def parse_text_into_characters(text):
def _parse_text_into_characters(text):
characters = []
is_escaping = False
group = None
Expand Down Expand Up @@ -71,82 +70,6 @@ def draw(self, g, position):
g.draw_tile(position, self.character, self.fg_color, self.bg_color)


class Color(object):
def __init__(self, r, g, b, a=255):
for val in [r, g, b, a]:
if isinstance(val, float):
raise ValueError("A Color object cannot contain the float value '" + str(val) + "'.")
if val < 0 or val > 255:
raise ValueError('The parameters to a Color object must be in the range [0, 255].')
self.r = r
self.g = g
self.b = b
self.a = a

def to_RGB_tuple(self):
return (self.r, self.g, self.b)
def to_RGBA_tuple(self):
return (self.r, self.g, self.b, self.a)

def __hash__(self):
return hash(self.to_RGBA_tuple())
def __eq__(self, other):
return self.r == other.r and self.g == other.g and self.b == other.b and self.a == other.a


class Paint(object):
def get_tile_color(self, absolute_position, relative_position):
"""absolute_position is absolute in the window, relative_position is relative_position to the Drawable being colored
The Paint may only use one of them depending on whether it draws absolutely or relatively
"""
raise NotImplementedError()
def tick(self, millis):
pass
class SolidPaint(Paint):
def __init__(self, color):
self.color = color
def get_tile_color(self, absolute_position, relative_position):
return self.color
class GradientPaint(Paint):
pass
class LinearGradientPaint(GradientPaint):
def __init__(self, color1, position1, color2, position2, is_cyclical=True, is_relative=True):
self.color1 = color1
self.position1 = position1
self.color2 = color2
self.position2 = position2
self.is_cyclical = is_cyclical
self.is_relative = is_relative
self.direction_vector = (self.position2 - self.position1).normalized()
def get_tile_color(self, relative_position, absolute_position):
position = relative_position if self.is_relative else absolute_position
projection = position.dot(self.direction_vector) # project the position onto the linear direction vector
p1_projection = self.position1.dot(self.direction_vector)
p2_projection = self.position2.dot(self.direction_vector)
percent = self._get_percent_along_projection(projection, p1_projection, p2_projection)
return Color(
int(round(self.color1.r + percent * (self.color2.r - self.color1.r))),
int(round(self.color1.g + percent * (self.color2.g - self.color1.g))),
int(round(self.color1.b + percent * (self.color2.b - self.color1.b))),
int(round(self.color1.a + percent * (self.color2.a - self.color1.a))),
)
def _get_percent_along_projection(self, projection, p1_projection, p2_projection):
diff = p2_projection - p1_projection
raw_percent = (projection-p1_projection) / diff
if self.is_cyclical:
must_inverse = int(raw_percent) % 2 == 1
fractional_part = math.modf(raw_percent)[0]
percent_before_inverse = abs(fractional_part)
return 1-percent_before_inverse if must_inverse else percent_before_inverse
else:
if raw_percent < 0:
return 0
elif raw_percent > 1:
return 1
else:
return raw_percent


class Drawable(object):
def __init__(self, position, fg_paint=None, bg_paint=None):
self.position = position
Expand Down Expand Up @@ -189,10 +112,42 @@ def contains_position(self, position):
return False

def write_text(self, text):
characters = parse_text_into_characters(text)
characters = _parse_text_into_characters(text)
for tile, offset in self.tiles:
if len(characters) == 0:
break
tile.character = characters[0]
characters.pop(0)


class Color(object):
def __init__(self, r, g, b, a=255):
for val in [r, g, b, a]:
if isinstance(val, float):
raise ValueError("A Color object cannot contain the float value '" + str(val) + "'.")
if val < 0 or val > 255:
raise ValueError('The parameters to a Color object must be in the range [0, 255].')
self.r = r
self.g = g
self.b = b
self.a = a

def to_RGB_tuple(self):
return (self.r, self.g, self.b)
def to_RGBA_tuple(self):
return (self.r, self.g, self.b, self.a)

def __hash__(self):
return hash(self.to_RGBA_tuple())
def __eq__(self, other):
return self.r == other.r and self.g == other.g and self.b == other.b and self.a == other.a


class Paint(object):
def get_tile_color(self, absolute_position, relative_position):
"""absolute_position is absolute in the window, relative_position is relative_position to the Drawable being colored
The Paint may only use one of them depending on whether it draws absolutely or relatively
"""
raise NotImplementedError()
def tick(self, millis):
pass
51 changes: 51 additions & 0 deletions pyrogi/drawing/paints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import math

from pyrogi.drawing import Color, Paint

class SolidPaint(Paint):
def __init__(self, color):
self.color = color
def get_tile_color(self, absolute_position, relative_position):
return self.color


class GradientPaint(Paint):
pass


class LinearGradientPaint(GradientPaint):
def __init__(self, color1, position1, color2, position2, is_cyclical=True, is_relative=True):
self.color1 = color1
self.position1 = position1
self.color2 = color2
self.position2 = position2
self.is_cyclical = is_cyclical
self.is_relative = is_relative
self.direction_vector = (self.position2 - self.position1).normalized()
def get_tile_color(self, relative_position, absolute_position):
position = relative_position if self.is_relative else absolute_position
projection = position.dot(self.direction_vector) # project the position onto the linear direction vector
p1_projection = self.position1.dot(self.direction_vector)
p2_projection = self.position2.dot(self.direction_vector)
percent = self._get_percent_along_projection(projection, p1_projection, p2_projection)
return Color(
int(round(self.color1.r + percent * (self.color2.r - self.color1.r))),
int(round(self.color1.g + percent * (self.color2.g - self.color1.g))),
int(round(self.color1.b + percent * (self.color2.b - self.color1.b))),
int(round(self.color1.a + percent * (self.color2.a - self.color1.a))),
)
def _get_percent_along_projection(self, projection, p1_projection, p2_projection):
diff = p2_projection - p1_projection
raw_percent = (projection-p1_projection) / diff
if self.is_cyclical:
must_inverse = int(raw_percent) % 2 == 1
fractional_part = math.modf(raw_percent)[0]
percent_before_inverse = abs(fractional_part)
return 1-percent_before_inverse if must_inverse else percent_before_inverse
else:
if raw_percent < 0:
return 0
elif raw_percent > 1:
return 1
else:
return raw_percent
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import os.path
import json
import os.path
import pygame
from pyrogi import FONT_PATH, FONT_CONFIG_EXTENSION
from pyrogi.graphics import Graphics, Color

from pyrogi import FONT_CONFIG_EXTENSION, FONT_PATH
from pyrogi.drawing import Graphics
from pyrogi.util import Vec2

GRAYSCALE_FONT_TYPE = 'grayscale'
Expand Down

0 comments on commit 083142e

Please sign in to comment.