From 1b1ddeba97884d045577d08df1f4c438db1a1e6a Mon Sep 17 00:00:00 2001 From: Koen <98043234+koen1711@users.noreply.github.com> Date: Sat, 25 May 2024 14:24:45 +0200 Subject: [PATCH] chore: add pull_request.yaml workflow (#8) * chore: add pull_request.yaml workflow * chore: run black * chore: fix all pylint issues --- .github/workflows/pull_request.yaml | 33 +++ play/__init__.py | 2 + play/all_sprites.py | 4 +- play/async_helpers.py | 29 ++- play/clamp.py | 7 +- play/color.py | 310 ++++++++++++------------ play/exceptions.py | 7 +- play/io/__init__.py | 69 ++++-- play/keypress.py | 357 +++++++++++++++++++--------- play/objects/__init__.py | 39 +-- play/objects/box.py | 76 ++++-- play/objects/circle.py | 79 ++++-- play/objects/line.py | 83 +++++-- play/objects/sprite.py | 136 +++++++---- play/objects/text.py | 67 ++++-- play/physics/__init__.py | 100 +++++--- play/play.py | 293 +++++++++-------------- pyproject.toml | 5 + 18 files changed, 1052 insertions(+), 644 deletions(-) create mode 100644 .github/workflows/pull_request.yaml create mode 100644 pyproject.toml diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml new file mode 100644 index 0000000..e940f9f --- /dev/null +++ b/.github/workflows/pull_request.yaml @@ -0,0 +1,33 @@ +name: Run tests + +on: + pull_request: + branches: + - main + +jobs: + run_tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint==3.0.2 black==23.11.0 + + - name: Checking formatting with black + run: | + black --check ./play + + - name: Analysing the code with pylint + run: | + pylint $(git ls-files 'play/*.py') \ No newline at end of file diff --git a/play/__init__.py b/play/__init__.py index f9a4b1b..2c66cfc 100644 --- a/play/__init__.py +++ b/play/__init__.py @@ -1,2 +1,4 @@ +"""The library to make pygame easier to use.""" + from .play import * from .objects import * diff --git a/play/all_sprites.py b/play/all_sprites.py index 4d933cd..eed9207 100644 --- a/play/all_sprites.py +++ b/play/all_sprites.py @@ -1,4 +1,4 @@ +"""This module contains the list of all sprites and walls in the game.""" + all_sprites = [] _walls = [] - - diff --git a/play/async_helpers.py b/play/async_helpers.py index dca2030..3097f0c 100644 --- a/play/async_helpers.py +++ b/play/async_helpers.py @@ -1,27 +1,34 @@ +"""This module contains code to help with async functions.""" + import asyncio as _asyncio import warnings as _warnings from .exceptions import Oops + def _raise_on_await_warning(func): """ If someone doesn't put 'await' before functions that require 'await' like play.timer() or play.animate(), raise an exception. """ - async def f(*args, **kwargs): - with _warnings.catch_warnings(record=True) as w: + async def raise_on_warning(*args, **kwargs): + with _warnings.catch_warnings(record=True) as warnings: await func(*args, **kwargs) - for warning in w: - str_message = warning.message.args[0] # e.g. "coroutine 'timer' was never awaited" - if 'was never awaited' in str_message: + for warning in warnings: + str_message = warning.message.args[ + 0 + ] # e.g. "coroutine 'timer' was never awaited" + if "was never awaited" in str_message: unawaited_function_name = str_message.split("'")[1] + raise Oops( - f"""Looks like you forgot to put "await" before play.{unawaited_function_name} on line {warning.lineno} of file {warning.filename}. -To fix this, just add the word 'await' before play.{unawaited_function_name} on line {warning.lineno} of file {warning.filename} in the function {func.__name__}.""") - else: - print(warning.message) + f"""Looks like you forgot to put "await" before play.{unawaited_function_name} on line {warning.lineno} of file {warning.filename}.""" + # pylint: disable=line-too-long +"""To fix this, just add the word 'await' before play.{unawaited_function_name} on line {warning.lineno} of file {warning.filename} in the function {func.__name__}.""" # pylint: disable=line-too-long + # pylint: enable=line-too-long + ) + print(warning.message) - return f + return raise_on_warning def _make_async(func): @@ -37,4 +44,4 @@ def _make_async(func): async def async_func(*args, **kwargs): return func(*args, **kwargs) - return async_func \ No newline at end of file + return async_func diff --git a/play/clamp.py b/play/clamp.py index ad98f0d..112a7c4 100644 --- a/play/clamp.py +++ b/play/clamp.py @@ -1,6 +1,11 @@ +""" Module for clamping values. + +""" + + def _clamp(num, min_, max_): if num < min_: return min_ - elif num > max_: + if num > max_: return max_ return num diff --git a/play/color.py b/play/color.py index 8ee5787..80af0e6 100644 --- a/play/color.py +++ b/play/color.py @@ -1,157 +1,161 @@ +"""This module contains a lot of color-related code, including the color_name_to_rgb function.""" + from .exceptions import Oops + # most color names from https://upload.wikimedia.org/wikipedia/commons/2/2b/SVG_Recognized_color_keyword_names.svg # except that list doesn't have obvious colors people might want to use like "light brown", so we add those manually color_names = { -'aliceblue': (240, 248, 255), -'antiquewhite': (250, 235, 215), -'aqua': ( 0, 255, 255), -'aquamarine': (127, 255, 212), -'azure': (240, 255, 255), -'beige': (245, 245, 220), -'bisque': (255, 228, 196), -'black': ( 0, 0, 0), -'blanchedalmond': (255, 235, 205), -'blue': ( 0, 0, 255), -'blueviolet': (138, 43, 226), -'brown': (165, 42, 42), -'burlywood': (222, 184, 135), -'cadetblue': ( 95, 158, 160), -'chartreuse': (127, 255, 0), -'chocolate': (210, 105, 30), -'coral': (255, 127, 80), -'cornflowerblue': (100, 149, 237), -'cornsilk': (255, 248, 220), -'crimson': (220, 20, 60), -'cyan': ( 0, 255, 255), -'darkblue': ( 0, 0, 139), -'darkcyan': ( 0, 139, 139), -'darkgoldenrod': (184, 134, 11), -'darkgray': (169, 169, 169), -'darkgreen': ( 0, 100, 0), -'darkgrey': (169, 169, 169), -'darkkhaki': (189, 183, 107), -'darkmagenta': (139, 0, 139), -'darkolivegreen': ( 85, 107, 47), -'darkorange': (255, 140, 0), -'darkorchid': (153, 50, 204), -'darkred': (139, 0, 0), -'darksalmon': (233, 150, 122), -'darkseagreen': (143, 188, 143), -'darkslateblue': ( 72, 61, 139), -'darkslategray': ( 47, 79, 79), -'darkslategrey': ( 47, 79, 79), -'darkturquoise': ( 0, 206, 209), -'darkviolet': (148, 0, 211), -'deeppink': (255, 20, 147), -'deepskyblue': ( 0, 191, 255), -'dimgray': (105, 105, 105), -'dimgrey': (105, 105, 105), -'dodgerblue': ( 30, 144, 255), -'firebrick': (178, 34, 34), -'floralwhite': (255, 250, 240), -'forestgreen': ( 34, 139, 34), -'fuchsia': (255, 0, 255), -'gainsboro': (220, 220, 220), -'ghostwhite': (248, 248, 255), -'gold': (255, 215, 0), -'goldenrod': (218, 165, 32), -'gray': (128, 128, 128), -'grey': (128, 128, 128), -'green': ( 0, 128, 0), -'greenyellow': (173, 255, 47), -'honeydew': (240, 255, 240), -'hotpink': (255, 105, 180), -'indianred': (205, 92, 92), -'indigo': ( 75, 0, 130), -'ivory': (255, 255, 240), -'khaki': (240, 230, 140), -'lavender': (230, 230, 250), -'lavenderblush': (255, 240, 245), -'lawngreen': (124, 252, 0), -'lemonchiffon': (255, 250, 205), -'lightblue': (173, 216, 230), -'lightcoral': (240, 128, 128), -'lightcyan': (224, 255, 255), -'lightgoldenrodyellow': (250, 250, 210), -'lightgray': (211, 211, 211), -'lightgreen': (144, 238, 144), -'lightgrey': (211, 211, 211), -'lightpink': (255, 182, 193), -'lightsalmon': (255, 160, 122), -'lightseagreen': ( 32, 178, 170), -'lightskyblue': (135, 206, 250), -'lightslategray': (119, 136, 153), -'lightslategrey': (119, 136, 153), -'lightsteelblue': (176, 196, 222), -'lightyellow': (255, 255, 224), -'lime': ( 0, 255, 0), -'limegreen': ( 50, 205, 50), -'linen': (250, 240, 230), -'magenta': (255, 0, 255), -'maroon': (128, 0, 0), -'mediumaquamarine': (102, 205, 170), -'mediumblue': ( 0, 0, 205), -'mediumorchid': (186, 85, 211), -'mediumpurple': (147, 112, 219), -'mediumseagreen': ( 60, 179, 113), -'mediumslateblue': (123, 104, 238), -'mediumspringgreen': ( 0, 250, 154), -'mediumturquoise': ( 72, 209, 204), -'mediumvioletred': (199, 21, 133), -'midnightblue': ( 25, 25, 112), -'mintcream': (245, 255, 250), -'mistyrose': (255, 228, 225), -'moccasin': (255, 228, 181), -'navajowhite': (255, 222, 173), -'navy': ( 0, 0, 128), -'oldlace': (253, 245, 230), -'olive': (128, 128, 0), -'olivedrab': (107, 142, 35), -'orange': (255, 165, 0), -'orangered': (255, 69, 0), -'orchid': (218, 112, 214), -'palegoldenrod': (238, 232, 170), -'palegreen': (152, 251, 152), -'paleturquoise': (175, 238, 238), -'palevioletred': (219, 112, 147), -'papayawhip': (255, 239, 213), -'peachpuff': (255, 218, 185), -'peru': (205, 133, 63), -'pink': (255, 192, 203), -'plum': (221, 160, 221), -'powderblue': (176, 224, 230), -'purple': (128, 0, 128), -'red': (255, 0, 0), -'rosybrown': (188, 143, 143), -'royalblue': ( 65, 105, 225), -'saddlebrown': (139, 69, 19), -'salmon': (250, 128, 114), -'sandybrown': (244, 164, 96), -'seagreen': ( 46, 139, 87), -'seashell': ( 46, 139, 87), -'sienna': (160, 82, 45), -'silver': (192, 192, 192), -'skyblue': (135, 206, 235), -'slateblue': (106, 90, 205), -'slategray': (112, 128, 144), -'slategrey': (112, 128, 144), -'snow': (255, 250, 250), -'springgreen': ( 0, 255, 127), -'steelblue': ( 70, 130, 180), -'tan': (210, 180, 140), -'teal': ( 0, 128, 128), -'thistle': (216, 191, 216), -'tomato': (255, 99, 71), -'turquoise': ( 64, 224, 208), -'violet': (238, 130, 238), -'wheat': (245, 222, 179), -'white': (255, 255, 255), -'whitesmoke': (245, 245, 245), -'yellow': (255, 255, 0), -'yellowgreen': (154, 205, 50), -'transparent': ( 0, 0, 0, 0), + "aliceblue": (240, 248, 255), + "antiquewhite": (250, 235, 215), + "aqua": (0, 255, 255), + "aquamarine": (127, 255, 212), + "azure": (240, 255, 255), + "beige": (245, 245, 220), + "bisque": (255, 228, 196), + "black": (0, 0, 0), + "blanchedalmond": (255, 235, 205), + "blue": (0, 0, 255), + "blueviolet": (138, 43, 226), + "brown": (165, 42, 42), + "burlywood": (222, 184, 135), + "cadetblue": (95, 158, 160), + "chartreuse": (127, 255, 0), + "chocolate": (210, 105, 30), + "coral": (255, 127, 80), + "cornflowerblue": (100, 149, 237), + "cornsilk": (255, 248, 220), + "crimson": (220, 20, 60), + "cyan": (0, 255, 255), + "darkblue": (0, 0, 139), + "darkcyan": (0, 139, 139), + "darkgoldenrod": (184, 134, 11), + "darkgray": (169, 169, 169), + "darkgreen": (0, 100, 0), + "darkgrey": (169, 169, 169), + "darkkhaki": (189, 183, 107), + "darkmagenta": (139, 0, 139), + "darkolivegreen": (85, 107, 47), + "darkorange": (255, 140, 0), + "darkorchid": (153, 50, 204), + "darkred": (139, 0, 0), + "darksalmon": (233, 150, 122), + "darkseagreen": (143, 188, 143), + "darkslateblue": (72, 61, 139), + "darkslategray": (47, 79, 79), + "darkslategrey": (47, 79, 79), + "darkturquoise": (0, 206, 209), + "darkviolet": (148, 0, 211), + "deeppink": (255, 20, 147), + "deepskyblue": (0, 191, 255), + "dimgray": (105, 105, 105), + "dimgrey": (105, 105, 105), + "dodgerblue": (30, 144, 255), + "firebrick": (178, 34, 34), + "floralwhite": (255, 250, 240), + "forestgreen": (34, 139, 34), + "fuchsia": (255, 0, 255), + "gainsboro": (220, 220, 220), + "ghostwhite": (248, 248, 255), + "gold": (255, 215, 0), + "goldenrod": (218, 165, 32), + "gray": (128, 128, 128), + "grey": (128, 128, 128), + "green": (0, 128, 0), + "greenyellow": (173, 255, 47), + "honeydew": (240, 255, 240), + "hotpink": (255, 105, 180), + "indianred": (205, 92, 92), + "indigo": (75, 0, 130), + "ivory": (255, 255, 240), + "khaki": (240, 230, 140), + "lavender": (230, 230, 250), + "lavenderblush": (255, 240, 245), + "lawngreen": (124, 252, 0), + "lemonchiffon": (255, 250, 205), + "lightblue": (173, 216, 230), + "lightcoral": (240, 128, 128), + "lightcyan": (224, 255, 255), + "lightgoldenrodyellow": (250, 250, 210), + "lightgray": (211, 211, 211), + "lightgreen": (144, 238, 144), + "lightgrey": (211, 211, 211), + "lightpink": (255, 182, 193), + "lightsalmon": (255, 160, 122), + "lightseagreen": (32, 178, 170), + "lightskyblue": (135, 206, 250), + "lightslategray": (119, 136, 153), + "lightslategrey": (119, 136, 153), + "lightsteelblue": (176, 196, 222), + "lightyellow": (255, 255, 224), + "lime": (0, 255, 0), + "limegreen": (50, 205, 50), + "linen": (250, 240, 230), + "magenta": (255, 0, 255), + "maroon": (128, 0, 0), + "mediumaquamarine": (102, 205, 170), + "mediumblue": (0, 0, 205), + "mediumorchid": (186, 85, 211), + "mediumpurple": (147, 112, 219), + "mediumseagreen": (60, 179, 113), + "mediumslateblue": (123, 104, 238), + "mediumspringgreen": (0, 250, 154), + "mediumturquoise": (72, 209, 204), + "mediumvioletred": (199, 21, 133), + "midnightblue": (25, 25, 112), + "mintcream": (245, 255, 250), + "mistyrose": (255, 228, 225), + "moccasin": (255, 228, 181), + "navajowhite": (255, 222, 173), + "navy": (0, 0, 128), + "oldlace": (253, 245, 230), + "olive": (128, 128, 0), + "olivedrab": (107, 142, 35), + "orange": (255, 165, 0), + "orangered": (255, 69, 0), + "orchid": (218, 112, 214), + "palegoldenrod": (238, 232, 170), + "palegreen": (152, 251, 152), + "paleturquoise": (175, 238, 238), + "palevioletred": (219, 112, 147), + "papayawhip": (255, 239, 213), + "peachpuff": (255, 218, 185), + "peru": (205, 133, 63), + "pink": (255, 192, 203), + "plum": (221, 160, 221), + "powderblue": (176, 224, 230), + "purple": (128, 0, 128), + "red": (255, 0, 0), + "rosybrown": (188, 143, 143), + "royalblue": (65, 105, 225), + "saddlebrown": (139, 69, 19), + "salmon": (250, 128, 114), + "sandybrown": (244, 164, 96), + "seagreen": (46, 139, 87), + "seashell": (46, 139, 87), + "sienna": (160, 82, 45), + "silver": (192, 192, 192), + "skyblue": (135, 206, 235), + "slateblue": (106, 90, 205), + "slategray": (112, 128, 144), + "slategrey": (112, 128, 144), + "snow": (255, 250, 250), + "springgreen": (0, 255, 127), + "steelblue": (70, 130, 180), + "tan": (210, 180, 140), + "teal": (0, 128, 128), + "thistle": (216, 191, 216), + "tomato": (255, 99, 71), + "turquoise": (64, 224, 208), + "violet": (238, 130, 238), + "wheat": (245, 222, 179), + "white": (255, 255, 255), + "whitesmoke": (245, 245, 245), + "yellow": (255, 255, 0), + "yellowgreen": (154, 205, 50), + "transparent": (0, 0, 0, 0), } + def color_name_to_rgb(name): """ Turn an English color name into an RGB value. @@ -162,12 +166,14 @@ def color_name_to_rgb(name): are all valid and will produce the rgb value for lightblue. """ - if type(name) == tuple: + if isinstance(name, tuple): return name try: - return color_names[name.lower().strip().replace('-', '').replace(' ', '')] + return color_names[name.lower().strip().replace("-", "").replace(" ", "")] except KeyError as exception: - raise Oops(f"""You gave a color name we didn't understand: '{name}' + raise Oops( + f"""You gave a color name we didn't understand: '{name}' If this our mistake, please let us know. Otherwise, try using the RGB number form of the color e.g. '(0, 255, 255)'. -You can find the RGB form of a color on websites like this: https://www.rapidtables.com/web/color/RGB_Color.html\n""") from exception +You can find the RGB form of a color on websites like this: https://www.rapidtables.com/web/color/RGB_Color.html\n""" + ) from exception diff --git a/play/exceptions.py b/play/exceptions.py index 76407ee..92e1751 100644 --- a/play/exceptions.py +++ b/play/exceptions.py @@ -1,11 +1,12 @@ -import warnings +"""Custom exceptions for the play library.""" class Oops(Exception): def __init__(self, message): # for readability, always prepend exception messages in the library with two blank lines - message = '\n\n\tOops!\n\n\t'+message.replace('\n', '\n\t')+'\n' - super(Oops, self).__init__(message) + message = "\n\n\tOops!\n\n\t" + message.replace("\n", "\n\t") + "\n" + super(Oops, self).__init__(message) # pylint: disable=super-with-arguments + class Hmm(UserWarning): pass diff --git a/play/io/__init__.py b/play/io/__init__.py index b4c3085..dde5aff 100644 --- a/play/io/__init__.py +++ b/play/io/__init__.py @@ -1,19 +1,24 @@ +"""This file contains all the IO-related code for the Python Play library.""" + +from sys import platform + import pygame -from ..all_sprites import _walls -from ..physics import physics_space import pymunk as _pymunk -from pygame._sdl2.video import Window +from pygame._sdl2.video import Window # pylint: disable=no-name-in-module from pygame.locals import * -from sys import platform -_pygame_display = None +from ..all_sprites import _walls +from ..physics import physics_space + +PYGAME_DISPLAY = None + -class Screen(object): +class Screen(): def __init__(self, width=800, height=600): - global _pygame_display + global PYGAME_DISPLAY self._width = width self._height = height - _pygame_display = pygame.display.set_mode((width, height), pygame.DOUBLEBUF) + PYGAME_DISPLAY = pygame.display.set_mode((width, height), pygame.DOUBLEBUF) # pylint: disable=no-member pygame.display.set_caption("Python Play") self._fullscreen = False @@ -23,7 +28,7 @@ def width(self): @width.setter def width(self, _width): - global _pygame_display + global PYGAME_DISPLAY self._width = _width remove_walls() @@ -32,7 +37,7 @@ def width(self, _width): if self._fullscreen: self.enable_fullscreen() else: - _pygame_display = pygame.display.set_mode((self._width, self._height)) + PYGAME_DISPLAY = pygame.display.set_mode((self._width, self._height)) @property def height(self): @@ -40,7 +45,7 @@ def height(self): @height.setter def height(self, _height): - global _pygame_display + global PYGAME_DISPLAY self._height = _height remove_walls() @@ -49,7 +54,7 @@ def height(self, _height): if self._fullscreen: self.enable_fullscreen() else: - _pygame_display = pygame.display.set_mode((self._width, self._height)) + PYGAME_DISPLAY = pygame.display.set_mode((self._width, self._height)) @property def top(self): @@ -72,27 +77,33 @@ def size(self): return self.width, self.height def enable_fullscreen(self): - global _pygame_display + global PYGAME_DISPLAY if self._fullscreen: return self._fullscreen = True window = Window.from_display_module() - full_screen_size = (pygame.display.Info().current_w, pygame.display.Info().current_h) - if platform != 'linux': + full_screen_size = ( + pygame.display.Info().current_w, + pygame.display.Info().current_h, + ) + if platform != "linux": pygame.display.toggle_fullscreen() # works for entering and exiting fullscreen, except in linux - window.position = (full_screen_size[i] / 2 - window.size[i] / 2 for i in - range(2)) # reset X and Y position of the window to original instead of top left + window.position = ( + full_screen_size[i] / 2 - window.size[i] / 2 for i in range(2) + ) # reset X and Y position of the window to original instead of top left else: - _pygame_display = pygame.display.set_mode((self.width, self.height), SCALED + NOFRAME + FULLSCREEN, 32) # all flags are necessary + PYGAME_DISPLAY = pygame.display.set_mode( + (self.width, self.height), SCALED + NOFRAME + FULLSCREEN, 32 # pylint: disable=undefined-variable + ) # all flags are necessary def disable_fullscreen(self): - global _pygame_display + global PYGAME_DISPLAY if not self._fullscreen: return self._fullscreen = False pygame.display.quit() pygame.display.init() - _pygame_display = pygame.display.set_mode((self.width, self.height)) + PYGAME_DISPLAY = pygame.display.set_mode((self.width, self.height)) screen = Screen() @@ -101,16 +112,24 @@ def disable_fullscreen(self): def _create_wall(a, b): segment = _pymunk.Segment(physics_space.static_body, a, b, 0.0) segment.elasticity = 1.0 - segment.friction = .1 + segment.friction = 0.1 physics_space.add(segment) return segment def create_walls(): - _walls.append(_create_wall([screen.left, screen.top], [screen.right, screen.top])) # top - _walls.append(_create_wall([screen.left, screen.bottom], [screen.right, screen.bottom])) # bottom - _walls.append(_create_wall([screen.left, screen.bottom], [screen.left, screen.top])) # left - _walls.append(_create_wall([screen.right, screen.bottom], [screen.right, screen.top])) # right + _walls.append( + _create_wall([screen.left, screen.top], [screen.right, screen.top]) + ) # top + _walls.append( + _create_wall([screen.left, screen.bottom], [screen.right, screen.bottom]) + ) # bottom + _walls.append( + _create_wall([screen.left, screen.bottom], [screen.left, screen.top]) + ) # left + _walls.append( + _create_wall([screen.right, screen.bottom], [screen.right, screen.top]) + ) # right def remove_walls(): diff --git a/play/keypress.py b/play/keypress.py index 049a1a7..81a2754 100644 --- a/play/keypress.py +++ b/play/keypress.py @@ -1,76 +1,85 @@ +"""This module contains functions and decorators for handling keypresses.""" + +import asyncio as _asyncio + import pygame + +from .exceptions import Oops +from .async_helpers import _make_async + +# pylint: disable=no-member keypress_map = { - pygame.K_BACKSPACE: 'backspace', - pygame.K_TAB: 'tab', - pygame.K_CLEAR: 'clear', - pygame.K_RETURN: 'enter', - pygame.K_PAUSE: 'pause', - pygame.K_ESCAPE: 'escape', - pygame.K_SPACE: 'space', - pygame.K_EXCLAIM: '!', + pygame.K_BACKSPACE: "backspace", + pygame.K_TAB: "tab", + pygame.K_CLEAR: "clear", + pygame.K_RETURN: "enter", + pygame.K_PAUSE: "pause", + pygame.K_ESCAPE: "escape", + pygame.K_SPACE: "space", + pygame.K_EXCLAIM: "!", pygame.K_QUOTEDBL: '"', - pygame.K_HASH: '#', - pygame.K_DOLLAR: '$', - pygame.K_AMPERSAND: '&', + pygame.K_HASH: "#", + pygame.K_DOLLAR: "$", + pygame.K_AMPERSAND: "&", pygame.K_QUOTE: "'", - pygame.K_LEFTPAREN: '(', - pygame.K_RIGHTPAREN: ')', - pygame.K_ASTERISK: '*', - pygame.K_PLUS: '+', - pygame.K_COMMA: ',', - pygame.K_MINUS: '-', - pygame.K_PERIOD: '.', - pygame.K_SLASH: '/', - pygame.K_0: '0', - pygame.K_1: '1', - pygame.K_2: '2', - pygame.K_3: '3', - pygame.K_4: '4', - pygame.K_5: '5', - pygame.K_6: '6', - pygame.K_7: '7', - pygame.K_8: '8', - pygame.K_9: '9', - pygame.K_COLON: ':', - pygame.K_SEMICOLON: ';', - pygame.K_LESS: '<', - pygame.K_EQUALS: '=', - pygame.K_GREATER: '>', - pygame.K_QUESTION: '?', - pygame.K_AT: '@', - pygame.K_LEFTBRACKET: '[', - pygame.K_BACKSLASH: '\\', - pygame.K_RIGHTBRACKET: ']', - pygame.K_CARET: '^', - pygame.K_UNDERSCORE: '_', - pygame.K_BACKQUOTE: '`', - pygame.K_a: 'a', - pygame.K_b: 'b', - pygame.K_c: 'c', - pygame.K_d: 'd', - pygame.K_e: 'e', - pygame.K_f: 'f', - pygame.K_g: 'g', - pygame.K_h: 'h', - pygame.K_i: 'i', - pygame.K_j: 'j', - pygame.K_k: 'k', - pygame.K_l: 'l', - pygame.K_m: 'm', - pygame.K_n: 'n', - pygame.K_o: 'o', - pygame.K_p: 'p', - pygame.K_q: 'q', - pygame.K_r: 'r', - pygame.K_s: 's', - pygame.K_t: 't', - pygame.K_u: 'u', - pygame.K_v: 'v', - pygame.K_w: 'w', - pygame.K_x: 'x', - pygame.K_y: 'y', - pygame.K_z: 'z', - pygame.K_DELETE: 'delete', + pygame.K_LEFTPAREN: "(", + pygame.K_RIGHTPAREN: ")", + pygame.K_ASTERISK: "*", + pygame.K_PLUS: "+", + pygame.K_COMMA: ",", + pygame.K_MINUS: "-", + pygame.K_PERIOD: ".", + pygame.K_SLASH: "/", + pygame.K_0: "0", + pygame.K_1: "1", + pygame.K_2: "2", + pygame.K_3: "3", + pygame.K_4: "4", + pygame.K_5: "5", + pygame.K_6: "6", + pygame.K_7: "7", + pygame.K_8: "8", + pygame.K_9: "9", + pygame.K_COLON: ":", + pygame.K_SEMICOLON: ";", + pygame.K_LESS: "<", + pygame.K_EQUALS: "=", + pygame.K_GREATER: ">", + pygame.K_QUESTION: "?", + pygame.K_AT: "@", + pygame.K_LEFTBRACKET: "[", + pygame.K_BACKSLASH: "\\", + pygame.K_RIGHTBRACKET: "]", + pygame.K_CARET: "^", + pygame.K_UNDERSCORE: "_", + pygame.K_BACKQUOTE: "`", + pygame.K_a: "a", + pygame.K_b: "b", + pygame.K_c: "c", + pygame.K_d: "d", + pygame.K_e: "e", + pygame.K_f: "f", + pygame.K_g: "g", + pygame.K_h: "h", + pygame.K_i: "i", + pygame.K_j: "j", + pygame.K_k: "k", + pygame.K_l: "l", + pygame.K_m: "m", + pygame.K_n: "n", + pygame.K_o: "o", + pygame.K_p: "p", + pygame.K_q: "q", + pygame.K_r: "r", + pygame.K_s: "s", + pygame.K_t: "t", + pygame.K_u: "u", + pygame.K_v: "v", + pygame.K_w: "w", + pygame.K_x: "x", + pygame.K_y: "y", + pygame.K_z: "z", + pygame.K_DELETE: "delete", # pygame.K_KP0: '', # pygame.K_KP1: '', # pygame.K_KP2: '', @@ -88,43 +97,43 @@ # pygame.K_KP_PLUS: '', # pygame.K_KP_ENTER: '', # pygame.K_KP_EQUALS: '', - pygame.K_UP: 'up', - pygame.K_DOWN: 'down', - pygame.K_RIGHT: 'right', - pygame.K_LEFT: 'left', - pygame.K_INSERT: 'insert', - pygame.K_HOME: 'home', - pygame.K_END: 'end', - pygame.K_PAGEUP: 'pageup', - pygame.K_PAGEDOWN: 'pagedown', - pygame.K_F1: 'F1', - pygame.K_F2: 'F2', - pygame.K_F3: 'F3', - pygame.K_F4: 'F4', - pygame.K_F5: 'F5', - pygame.K_F6: 'F6', - pygame.K_F7: 'F7', - pygame.K_F8: 'F8', - pygame.K_F9: 'F9', - pygame.K_F10: 'F10', - pygame.K_F11: 'F11', - pygame.K_F12: 'F12', - pygame.K_F13: 'F13', - pygame.K_F14: 'F14', - pygame.K_F15: 'F15', - pygame.K_NUMLOCK: 'numlock', - pygame.K_CAPSLOCK: 'capslock', - pygame.K_SCROLLOCK: 'scrollock', - pygame.K_RSHIFT: 'shift', - pygame.K_LSHIFT: 'shift', - pygame.K_RCTRL: 'control', - pygame.K_LCTRL: 'control', - pygame.K_RALT: 'alt', - pygame.K_LALT: 'alt', - pygame.K_RMETA: 'meta', - pygame.K_LMETA: 'meta', - pygame.K_LSUPER: 'super', - pygame.K_RSUPER: 'super', + pygame.K_UP: "up", + pygame.K_DOWN: "down", + pygame.K_RIGHT: "right", + pygame.K_LEFT: "left", + pygame.K_INSERT: "insert", + pygame.K_HOME: "home", + pygame.K_END: "end", + pygame.K_PAGEUP: "pageup", + pygame.K_PAGEDOWN: "pagedown", + pygame.K_F1: "F1", + pygame.K_F2: "F2", + pygame.K_F3: "F3", + pygame.K_F4: "F4", + pygame.K_F5: "F5", + pygame.K_F6: "F6", + pygame.K_F7: "F7", + pygame.K_F8: "F8", + pygame.K_F9: "F9", + pygame.K_F10: "F10", + pygame.K_F11: "F11", + pygame.K_F12: "F12", + pygame.K_F13: "F13", + pygame.K_F14: "F14", + pygame.K_F15: "F15", + pygame.K_NUMLOCK: "numlock", + pygame.K_CAPSLOCK: "capslock", + pygame.K_SCROLLOCK: "scrollock", + pygame.K_RSHIFT: "shift", + pygame.K_LSHIFT: "shift", + pygame.K_RCTRL: "control", + pygame.K_LCTRL: "control", + pygame.K_RALT: "alt", + pygame.K_LALT: "alt", + pygame.K_RMETA: "meta", + pygame.K_LMETA: "meta", + pygame.K_LSUPER: "super", + pygame.K_RSUPER: "super", # pygame.K_MODE: '', # pygame.K_HELP: '', # pygame.K_PRINT: '', @@ -132,9 +141,142 @@ # pygame.K_BREAK: '', # pygame.K_MENU: '', # pygame.K_POWER: '', - pygame.K_EURO: '€', + pygame.K_EURO: "€", } +pygame.key.set_repeat(200, 16) +_pressed_keys = {} +_keypress_callbacks = [] +_keyrelease_callbacks = [] + + +# @decorator +def when_any_key_pressed(func): + if not callable(func): + raise Oops( + """@play.when_any_key_pressed doesn't use a list of keys. Try just this instead: + +@play.when_any_key_pressed +async def do(key): + print("This key was pressed!", key) +""" + ) + async_callback = _make_async(func) + + async def wrapper(*args, **kwargs): + wrapper.is_running = True + await async_callback(*args, **kwargs) + wrapper.is_running = False + + wrapper.keys = None + wrapper.is_running = False + _keypress_callbacks.append(wrapper) + return wrapper + + +# @decorator +def when_key_pressed(*keys): + def decorator(func): + async_callback = _make_async(func) + + async def wrapper(*args, **kwargs): + wrapper.is_running = True + await async_callback(*args, **kwargs) + wrapper.is_running = False + + wrapper.keys = keys + wrapper.is_running = False + _keypress_callbacks.append(wrapper) + return wrapper + + return decorator + + +# @decorator +def when_any_key_released(func): + if not callable(func): + raise Oops( + """@play.when_any_key_released doesn't use a list of keys. Try just this instead: + +@play.when_any_key_released +async def do(key): + print("This key was released!", key) +""" + ) + async_callback = _make_async(func) + + async def wrapper(*args, **kwargs): + wrapper.is_running = True + await async_callback(*args, **kwargs) + wrapper.is_running = False + + wrapper.keys = None + wrapper.is_running = False + _keyrelease_callbacks.append(wrapper) + return wrapper + + +# @decorator +def when_key_released(*keys): + def decorator(func): + async_callback = _make_async(func) + + async def wrapper(*args, **kwargs): + wrapper.is_running = True + await async_callback(*args, **kwargs) + wrapper.is_running = False + + wrapper.keys = keys + wrapper.is_running = False + _keyrelease_callbacks.append(wrapper) + return wrapper + + return decorator + + +def key_is_pressed(*keys): + """ + Returns True if any of the given keys are pressed. + + Example: + + @play.repeat_forever + async def do(): + if play.key_is_pressed('up', 'w'): + print('up or w pressed') + """ + # Called this function key_is_pressed instead of is_key_pressed so it will + # sound more english-like with if-statements: + # + # if play.key_is_pressed('w', 'up'): ... + + for key in keys: + if key in _pressed_keys.values(): + return True + return False + + +_loop = _asyncio.get_event_loop() +_loop.set_debug(False) + +_keys_pressed_this_frame = [] +_keys_released_this_frame = [] +_keys_to_skip = (pygame.K_MODE,) +pygame.event.set_allowed( + [ + pygame.QUIT, + pygame.KEYDOWN, + pygame.KEYUP, + pygame.MOUSEBUTTONDOWN, + pygame.MOUSEBUTTONUP, + pygame.MOUSEMOTION, + ] +) + + +# pylint: enable=no-member + + def pygame_key_to_name(pygame_key_event): english_name = keypress_map[pygame_key_event.key] if not pygame_key_event.mod and len(english_name) > 1: @@ -143,4 +285,3 @@ def pygame_key_to_name(pygame_key_event): return pygame_key_event.unicode # pygame_key_event.unicode is how we get e.g. # instead of 3 on US keyboards when shift+3 is pressed. # It also gives us capital letters and things like that. - diff --git a/play/objects/__init__.py b/play/objects/__init__.py index 3cb9207..800e588 100644 --- a/play/objects/__init__.py +++ b/play/objects/__init__.py @@ -1,3 +1,11 @@ +"""This module contains all the objects that can be used in the game window. +Each object is a subclass of Sprite, which is a subclass of pygame.sprite.Sprite. +The objects are: Box, Circle, Line, Text, and Group. +Each object has a corresponding new_* function that can be used to create the object. +For example, play.new_box() creates a new Box object. +""" + + from statistics import mean as _mean from .box import Box, new_box from .circle import Circle, new_circle @@ -23,9 +31,9 @@ class group(play.Group): group.move(10) # calls move(10) on all the group's sprites """ - def f(*args, **kwargs): + def get_attr(*args, **kwargs): # pylint: disable=inconsistent-return-statements results = [] - for sprite in cls: + for sprite in cls: # pylint: disable=not-an-iterable result = getattr(sprite, attr) if callable(result): result(*args, **kwargs) @@ -34,26 +42,26 @@ def f(*args, **kwargs): if results: return results - return f + return get_attr @property def x(cls): - return _mean(sprite.x for sprite in cls) + return _mean(sprite.x for sprite in cls) # pylint: disable=not-an-iterable @x.setter def x(cls, new_x): x_offset = new_x - cls.x - for sprite in cls: + for sprite in cls: # pylint: disable=not-an-iterable sprite.x += x_offset @property def y(cls): - return _mean(sprite.y for sprite in cls) + return _mean(sprite.y for sprite in cls) # pylint: disable=not-an-iterable @y.setter def y(cls, new_y): y_offset = new_y - cls.y - for sprite in cls: + for sprite in cls: # pylint: disable=not-an-iterable sprite.y += y_offset @@ -91,7 +99,7 @@ def sprites(cls): if isinstance(item, Sprite): yield item - def sprites(self): + def sprites(self): # pylint: disable=function-redefined for sprite in self.sprites_: yield sprite print(self.__class__.sprites) @@ -99,7 +107,7 @@ def sprites(self): yield sprite def __iter__(self): - for sprite in self.sprites: + for sprite in self.sprites: # pylint: disable=not-an-iterable yield sprite def go_to(self, x_or_sprite, y): @@ -108,7 +116,6 @@ def go_to(self, x_or_sprite, y): y = x_or_sprite.y except AttributeError: x = x_or_sprite - y = y max_x = max(sprite.x for sprite in self) min_x = min(sprite.x for sprite in self) @@ -141,11 +148,7 @@ def new_group(*sprites): return Group(*sprites) -def new_image(image=None, x=0, y=0, size=100, angle=0, transparency=100): - return Sprite(image=image, x=x, y=y, size=size, angle=angle, transparency=transparency) - - - - - - +def new_image(image=None, x=0, y=0, size=100, angle=0, transparency=100): # pylint: disable=too-many-arguments + return Sprite( + image=image, x=x, y=y, size=size, angle=angle, transparency=transparency + ) diff --git a/play/objects/box.py b/play/objects/box.py index 9922865..03bc4ad 100644 --- a/play/objects/box.py +++ b/play/objects/box.py @@ -1,3 +1,5 @@ +"""This module contains the Box class, which represents a box in the game.""" + import pygame from .sprite import Sprite from ..all_sprites import all_sprites @@ -5,8 +7,19 @@ class Box(Sprite): - def __init__(self, color='black', x=0, y=0, width=100, height=200, border_color='light blue', border_width=0, - transparency=100, size=100, angle=0): + def __init__( # pylint: disable=too-many-arguments + self, + color="black", + x=0, + y=0, + width=100, + height=200, + border_color="light blue", + border_width=0, + transparency=100, + size=100, + angle=0, + ): super().__init__(x, y, size, angle, transparency) self._x = x self._y = y @@ -30,15 +43,24 @@ def __init__(self, color='black', x=0, y=0, width=100, height=200, border_color= all_sprites.append(self) def _compute_primary_surface(self): - self._primary_pygame_surface = pygame.Surface((self._width, self._height), pygame.SRCALPHA) + self._primary_pygame_surface = pygame.Surface( + (self._width, self._height), pygame.SRCALPHA # pylint: disable=no-member + ) if self._border_width and self._border_color: # draw border rectangle self._primary_pygame_surface.fill(_color_name_to_rgb(self._border_color)) # draw fill rectangle over border rectangle at the proper position - pygame.draw.rect(self._primary_pygame_surface, _color_name_to_rgb(self._color), ( - self._border_width, self._border_width, self._width - 2 * self._border_width, - self._height - 2 * self.border_width)) + pygame.draw.rect( + self._primary_pygame_surface, + _color_name_to_rgb(self._color), + ( + self._border_width, + self._border_width, + self._width - 2 * self._border_width, + self._height - 2 * self.border_width, + ), + ) else: self._primary_pygame_surface.fill(_color_name_to_rgb(self._color)) @@ -97,11 +119,37 @@ def border_width(self, _border_width): self._should_recompute_primary_surface = True def clone(self): - return self.__class__(color=self.color, width=self.width, height=self.height, border_color=self.border_color, - border_width=self.border_width, **self._common_properties()) - - -def new_box(color='black', x=0, y=0, width=100, height=200, border_color='light blue', border_width=0, angle=0, - transparency=100, size=100): - return Box(color=color, x=x, y=y, width=width, height=height, border_color=border_color, border_width=border_width, - angle=angle, transparency=transparency, size=size) \ No newline at end of file + return self.__class__( + color=self.color, + width=self.width, + height=self.height, + border_color=self.border_color, + border_width=self.border_width, + **self._common_properties() + ) + + +def new_box( # pylint: disable=too-many-arguments + color="black", + x=0, + y=0, + width=100, + height=200, + border_color="light blue", + border_width=0, + angle=0, + transparency=100, + size=100, +): + return Box( + color=color, + x=x, + y=y, + width=width, + height=height, + border_color=border_color, + border_width=border_width, + angle=angle, + transparency=transparency, + size=size, + ) diff --git a/play/objects/circle.py b/play/objects/circle.py index e1d75c5..2e9b271 100644 --- a/play/objects/circle.py +++ b/play/objects/circle.py @@ -1,3 +1,5 @@ +"""This module defines the Circle class, which represents a circle in the game.""" + import pygame from .sprite import Sprite from ..all_sprites import all_sprites @@ -5,8 +7,18 @@ class Circle(Sprite): - def __init__(self, color='black', x=0, y=0, radius=100, border_color='light blue', border_width=0, transparency=100, - size=100, angle=0): + def __init__( # pylint: disable=too-many-arguments, super-init-not-called + self, + color="black", + x=0, + y=0, + radius=100, + border_color="light blue", + border_width=0, + transparency=100, + size=100, + angle=0, + ): self._x = x self._y = y self._color = color @@ -28,25 +40,44 @@ def __init__(self, color='black', x=0, y=0, radius=100, border_color='light blue all_sprites.append(self) def clone(self): - return self.__class__(color=self.color, radius=self.radius, border_color=self.border_color, - border_width=self.border_width, **self._common_properties()) + return self.__class__( + color=self.color, + radius=self.radius, + border_color=self.border_color, + border_width=self.border_width, + **self._common_properties() + ) def _compute_primary_surface(self): total_diameter = (self.radius + self._border_width) * 2 - self._primary_pygame_surface = pygame.Surface((total_diameter, total_diameter), pygame.SRCALPHA) + self._primary_pygame_surface = pygame.Surface( + (total_diameter, total_diameter), pygame.SRCALPHA # pylint: disable=no-member + ) center = self._radius + self._border_width if self._border_width and self._border_color: # draw border circle - pygame.draw.circle(self._primary_pygame_surface, _color_name_to_rgb(self._border_color), (center, center), - self._radius) + pygame.draw.circle( + self._primary_pygame_surface, + _color_name_to_rgb(self._border_color), + (center, center), + self._radius, + ) # draw fill circle over border circle - pygame.draw.circle(self._primary_pygame_surface, _color_name_to_rgb(self._color), (center, center), - self._radius - self._border_width) + pygame.draw.circle( + self._primary_pygame_surface, + _color_name_to_rgb(self._color), + (center, center), + self._radius - self._border_width, + ) else: - pygame.draw.circle(self._primary_pygame_surface, _color_name_to_rgb(self._color), (center, center), - self._radius) + pygame.draw.circle( + self._primary_pygame_surface, + _color_name_to_rgb(self._color), + (center, center), + self._radius, + ) self._should_recompute_primary_surface = False self._compute_secondary_surface(force=True) @@ -94,7 +125,25 @@ def border_width(self, _border_width): self._should_recompute_primary_surface = True -def new_circle(color='black', x=0, y=0, radius=100, border_color='light blue', border_width=0, transparency=100, - size=100, angle=0): - return Circle(color=color, x=x, y=y, radius=radius, border_color=border_color, border_width=border_width, - transparency=transparency, size=size, angle=angle) +def new_circle( # pylint: disable=too-many-arguments + color="black", + x=0, + y=0, + radius=100, + border_color="light blue", + border_width=0, + transparency=100, + size=100, + angle=0, +): + return Circle( + color=color, + x=x, + y=y, + radius=radius, + border_color=border_color, + border_width=border_width, + transparency=transparency, + size=size, + angle=angle, + ) diff --git a/play/objects/line.py b/play/objects/line.py index 15b8a70..ae7123e 100644 --- a/play/objects/line.py +++ b/play/objects/line.py @@ -1,12 +1,26 @@ +"""This module contains the Line class, which is a subclass of Sprite. It is used to create lines in the game window.""" + +import math as _math + import pygame from .sprite import Sprite from ..all_sprites import all_sprites -import math as _math class Line(Sprite): - def __init__(self, color='black', x=0, y=0, length=None, angle=None, thickness=1, x1=None, y1=None, - transparency=100, size=100): + def __init__( # pylint: disable=too-many-arguments + self, + color="black", + x=0, + y=0, + length=None, + angle=None, + thickness=1, + x1=None, + y1=None, + transparency=100, + size=100, + ): super().__init__(x, y, size, angle, transparency) self._x = x self._y = y @@ -14,11 +28,11 @@ def __init__(self, color='black', x=0, y=0, length=None, angle=None, thickness=1 self._thickness = thickness # can set either (length, angle) or (x1,y1), otherwise a default is used - if length != None and angle != None: + if length is not None and angle is not None: self._length = length self._angle = angle self._x1, self._y1 = self._calc_endpoint() - elif x1 != None and y1 != None: + elif x1 is not None and y1 is not None: self._x1 = x1 self._y1 = y1 self._length, self._angle = self._calc_length_angle() @@ -41,8 +55,12 @@ def __init__(self, color='black', x=0, y=0, length=None, angle=None, thickness=1 all_sprites.append(self) def clone(self): - return self.__class__(color=self.color, length=self.length, thickness=self.thickness, - **self._common_properties()) + return self.__class__( + color=self.color, + length=self.length, + thickness=self.thickness, + **self._common_properties() + ) def _compute_primary_surface(self): # Make a surface that just contains the line and no white-space around the line. @@ -50,27 +68,23 @@ def _compute_primary_surface(self): width = self.length height = self.thickness + 1 - self._primary_pygame_surface = pygame.Surface((width, height), pygame.SRCALPHA) + self._primary_pygame_surface = pygame.Surface((width, height), pygame.SRCALPHA) # pylint: disable=no-member # self._primary_pygame_surface.set_colorkey((255,255,255, 255)) # set background to transparent - # # @hack - # if self.thickness == 1: - # pygame.draw.aaline(self._primary_pygame_surface, _color_name_to_rgb(self.color), (0,1), (width,1), True) - # else: - # pygame.draw.line(self._primary_pygame_surface, _color_name_to_rgb(self.color), (0,_math.floor(height/2)), (width,_math.floor(height/2)), self.thickness) - # line is actually drawn in _game_loop because coordinates work different self._should_recompute_primary_surface = False self._compute_secondary_surface(force=True) def _compute_secondary_surface(self, force=False): - self._secondary_pygame_surface = self._primary_pygame_surface.copy() + self._secondary_pygame_surface = self._primary_pygame_surface.copy() # pylint: disable=attribute-defined-outside-init if force or self._transparency != 100: - self._secondary_pygame_surface.set_alpha(round((self._transparency / 100.) * 255)) + self._secondary_pygame_surface.set_alpha( + round((self._transparency / 100.0) * 255) + ) - self._should_recompute_secondary_surface = False + self._should_recompute_secondary_surface = False # pylint: disable=attribute-defined-outside-init ##### color ##### @property @@ -94,7 +108,10 @@ def thickness(self, _thickness): def _calc_endpoint(self): radians = _math.radians(self._angle) - return self._length * _math.cos(radians) + self.x, self._length * _math.sin(radians) + self.y + return ( + self._length * _math.cos(radians) + self.x, + self._length * _math.sin(radians) + self.y, + ) ##### length ##### @property @@ -124,7 +141,7 @@ def _calc_length_angle(self): dy = self.y1 - self.y # TODO: this doesn't work at all - return _math.sqrt(dx ** 2 + dy ** 2), _math.degrees(_math.atan2(dy, dx)) + return _math.sqrt(dx**2 + dy**2), _math.degrees(_math.atan2(dy, dx)) ##### x1 ##### @property @@ -149,7 +166,27 @@ def y1(self, _y1): self._should_recompute_primary_surface = True -def new_line(color='black', x=0, y=0, length=None, angle=None, thickness=1, x1=None, y1=None, transparency=100, - size=100): - return Line(color=color, x=x, y=y, length=length, angle=angle, thickness=thickness, x1=x1, y1=y1, - transparency=transparency, size=size) \ No newline at end of file +def new_line( # pylint: disable=too-many-arguments + color="black", + x=0, + y=0, + length=None, + angle=None, + thickness=1, + x1=None, + y1=None, + transparency=100, + size=100, +): + return Line( + color=color, + x=x, + y=y, + length=length, + angle=angle, + thickness=thickness, + x1=x1, + y1=y1, + transparency=transparency, + size=size, + ) diff --git a/play/objects/sprite.py b/play/objects/sprite.py index ef79dcf..0c3b19b 100644 --- a/play/objects/sprite.py +++ b/play/objects/sprite.py @@ -1,11 +1,14 @@ -from ..all_sprites import all_sprites -import pygame -from ..exceptions import Oops, Hmm -import os as _os +"""This module contains the base sprite class for all objects in the game.""" + import math as _math +import warnings as _warnings +import os as _os import pymunk as _pymunk +import pygame + +from ..all_sprites import all_sprites +from ..exceptions import Oops, Hmm from ..physics import physics_space, _Physics -import warnings as _warnings from ..clamp import _clamp from ..io import screen from ..async_helpers import _make_async @@ -15,18 +18,24 @@ def _sprite_touching_sprite(a, b): # todo: custom code for circle, line, rotated rectangley sprites # use physics engine if both sprites have physics on # if a.physics and b.physics: - if a.left >= b.right or a.right <= b.left or a.top <= b.bottom or a.bottom >= b.top: return False + if a.left >= b.right or a.right <= b.left or a.top <= b.bottom or a.bottom >= b.top: + return False return True -def _point_touching_sprite(point, sprite): +def point_touching_sprite(point, sprite): # todo: custom code for circle, line, rotated rectangley sprites - return sprite.left <= point.x <= sprite.right and sprite.bottom <= point.y <= sprite.top + return ( + sprite.left <= point.x <= sprite.right + and sprite.bottom <= point.y <= sprite.top + ) -class Sprite(object): - def __init__(self, image=None, x=0, y=0, size=100, angle=0, transparency=100): - self._image = image or _os.path.join(_os.path.split(__file__)[0], 'blank_image.png') +class Sprite(): # pylint: disable=attribute-defined-outside-init, too-many-public-methods + def __init__(self, image=None, x=0, y=0, size=100, angle=0, transparency=100): # pylint: disable=too-many-arguments + self._image = image or _os.path.join( + _os.path.split(__file__)[0], "blank_image.png" + ) self._x = x self._y = y self._angle = angle @@ -46,10 +55,14 @@ def __init__(self, image=None, x=0, y=0, size=100, angle=0, transparency=100): def _compute_primary_surface(self): try: self._primary_pygame_surface = pygame.image.load(_os.path.join(self._image)) - except pygame.error as exc: - raise Oops(f"""We couldn't find the image file you provided named "{self._image}". -If the file is in a folder, make sure you add the folder name, too.""") from exc - self._primary_pygame_surface.set_colorkey((255, 255, 255, 255)) # set background to transparent + except pygame.error as exc: # pylint: disable=no-member + raise Oops( + f"""We couldn't find the image file you provided named "{self._image}". +If the file is in a folder, make sure you add the folder name, too.""" + ) from exc + self._primary_pygame_surface.set_colorkey( + (255, 255, 255, 255) + ) # set background to transparent self._should_recompute_primary_surface = False @@ -65,24 +78,32 @@ def _compute_secondary_surface(self, force=False): try: # for text and images with transparent pixels array = pygame.surfarray.pixels_alpha(self._secondary_pygame_surface) - array[:, :] = (array[:, :] * (self._transparency / 100.)).astype( - array.dtype) # modify surface pixels in-place + array[:, :] = (array[:, :] * (self._transparency / 100.0)).astype( + array.dtype + ) # modify surface pixels in-place del array # I think pixels are written when array leaves memory, so delete it explicitly here - except Exception as e: + except Exception: # pylint: disable=broad-except # this works for images without alpha pixels in them - self._secondary_pygame_surface.set_alpha(round((self._transparency / 100.) * 255)) + self._secondary_pygame_surface.set_alpha( + round((self._transparency / 100.0) * 255) + ) # scale if (self.size != 100) or force: - ratio = self.size / 100. + ratio = self.size / 100.0 self._secondary_pygame_surface = pygame.transform.scale( self._secondary_pygame_surface, - (round(self._secondary_pygame_surface.get_width() * ratio), # width - round(self._secondary_pygame_surface.get_height() * ratio))) # height + ( + round(self._secondary_pygame_surface.get_width() * ratio), # width + round(self._secondary_pygame_surface.get_height() * ratio), + ), + ) # height # rotate if (self.angle != 0) or force: - self._secondary_pygame_surface = pygame.transform.rotate(self._secondary_pygame_surface, self._angle) + self._secondary_pygame_surface = pygame.transform.rotate( + self._secondary_pygame_surface, self._angle + ) self._should_recompute_secondary_surface = False @@ -110,7 +131,10 @@ def x(self, _x): self.physics._pymunk_body.position = self._x, self._y if prev_x != _x: # setting velocity makes the simulation more realistic usually - self.physics._pymunk_body.velocity = _x - prev_x, self.physics._pymunk_body.velocity.y + self.physics._pymunk_body.velocity = ( + _x - prev_x, + self.physics._pymunk_body.velocity.y, + ) if self.physics._pymunk_body.body_type == _pymunk.Body.STATIC: physics_space.reindex_static() @@ -126,7 +150,10 @@ def y(self, _y): self.physics._pymunk_body.position = self._x, self._y if prev_y != _y: # setting velocity makes the simulation more realistic usually - self.physics._pymunk_body.velocity = self.physics._pymunk_body.velocity.x, _y - prev_y + self.physics._pymunk_body.velocity = ( + self.physics._pymunk_body.velocity.x, + _y - prev_y, + ) if self.physics._pymunk_body.body_type == _pymunk.Body.STATIC: physics_space.reindex_static() @@ -137,12 +164,17 @@ def transparency(self): @transparency.setter def transparency(self, alpha): if not isinstance(alpha, float) and not isinstance(alpha, int): - raise Oops(f"""Looks like you're trying to set {self}'s transparency to '{alpha}', which isn't a number. + raise Oops( + f"""Looks like you're trying to set {self}'s transparency to '{alpha}', which isn't a number. Try looking in your code for where you're setting transparency for {self} and change it a number. -""") +""" + ) if alpha > 100 or alpha < 0: - _warnings.warn(f"""The transparency setting for {self} is being set to {alpha} and it should be between 0 and 100. -You might want to look in your code where you're setting transparency and make sure it's between 0 and 100. """, Hmm) + _warnings.warn( + f"""The transparency setting for {self} is being set to {alpha} and it should be between 0 and 100. +You might want to look in your code where you're setting transparency and make sure it's between 0 and 100. """, + Hmm, + ) self._transparency = _clamp(alpha, 0, 100) self._should_recompute_secondary_surface = True @@ -207,17 +239,16 @@ def is_shown(self, show): self._is_hidden = not show def is_touching(self, sprite_or_point): - rect = self._secondary_pygame_surface.get_rect() + self._secondary_pygame_surface.get_rect() if isinstance(sprite_or_point, Sprite): return _sprite_touching_sprite(sprite_or_point, self) - else: - return _point_touching_sprite(sprite_or_point, self) + return point_touching_sprite(sprite_or_point, self) def point_towards(self, x, y=None): try: x, y = x.x, x.y except AttributeError: - x, y = x, y + pass self.angle = _math.degrees(_math.atan2(y - self.y, x - self.x)) def go_to(self, x=None, y=None): @@ -231,7 +262,7 @@ def go_to(self, x=None, y=None): async def do(): text.go_to(play.mouse) """ - assert (not x is None) + assert not x is None try: # users can call e.g. sprite.go_to(play.mouse), so x will be an object with x and y @@ -242,7 +273,7 @@ async def do(): self.y = y def distance_to(self, x, y=None): - assert (not x is None) + assert not x is None try: # x can either be a number or a sprite. If it's a sprite: @@ -255,7 +286,7 @@ def distance_to(self, x, y=None): dx = self.x - x1 dy = self.y - y1 - return _math.sqrt(dx ** 2 + dy ** 2) + return _math.sqrt(dx**2 + dy**2) def remove(self): if self.physics: @@ -303,10 +334,18 @@ def bottom(self, y): self.y = y + self.height / 2 def _pygame_x(self): - return self.x + (screen.width / 2.) - (self._secondary_pygame_surface.get_width() / 2.) + return ( + self.x + + (screen.width / 2.0) + - (self._secondary_pygame_surface.get_width() / 2.0) + ) def _pygame_y(self): - return (screen.height / 2.) - self.y - (self._secondary_pygame_surface.get_height() / 2.) + return ( + (screen.height / 2.0) + - self.y + - (self._secondary_pygame_surface.get_height() / 2.0) + ) # @decorator def when_clicked(self, callback, call_with_sprite=False): @@ -326,7 +365,13 @@ async def wrapper(): def _common_properties(self): # used with inheritance to clone - return {'x': self.x, 'y': self.y, 'size': self.size, 'transparency': self.transparency, 'angle': self.angle} + return { + "x": self.x, + "y": self.y, + "size": self.size, + "transparency": self.transparency, + "angle": self.angle, + } def clone(self): # TODO: make work with physics @@ -345,8 +390,17 @@ def clone(self): # elif self.physics and name in : # return setattr(self.physics, name, value) - def start_physics(self, can_move=True, stable=False, x_speed=0, y_speed=0, obeys_gravity=True, bounciness=1.0, - mass=10, friction=0.1): + def start_physics( # pylint: disable=too-many-arguments + self, + can_move=True, + stable=False, + x_speed=0, + y_speed=0, + obeys_gravity=True, + bounciness=1.0, + mass=10, + friction=0.1, + ): if not self.physics: self.physics = _Physics( self, diff --git a/play/objects/text.py b/play/objects/text.py index 385dd61..e503e7d 100644 --- a/play/objects/text.py +++ b/play/objects/text.py @@ -1,14 +1,25 @@ +"""This module contains the Text class, which is used to create text objects in the game.""" +import warnings as _warnings import pygame from .sprite import Sprite from ..all_sprites import all_sprites from ..exceptions import Hmm -import warnings as _warnings from ..color import color_name_to_rgb as _color_name_to_rgb class Text(Sprite): - def __init__(self, words='hi :)', x=0, y=0, font=None, font_size=50, color='black', angle=0, transparency=100, - size=100): + def __init__( # pylint: disable=too-many-arguments + self, + words="hi :)", + x=0, + y=0, + font=None, + font_size=50, + color="black", + angle=0, + transparency=100, + size=100, + ): self._font = font self._font_size = font_size self._words = words @@ -18,8 +29,6 @@ def __init__(self, words='hi :)', x=0, y=0, font=None, font_size=50, color='blac self._x = x self._y = y - - self._size = size self._angle = angle self.transparency = transparency @@ -35,19 +44,28 @@ def __init__(self, words='hi :)', x=0, y=0, font=None, font_size=50, color='blac all_sprites.append(self) def clone(self): - return self.__class__(words=self.words, font=self.font, font_size=self.font_size, color=self.color, - **self._common_properties()) + return self.__class__( + words=self.words, + font=self.font, + font_size=self.font_size, + color=self.color, + **self._common_properties(), + ) def _compute_primary_surface(self): try: self._pygame_font = pygame.font.Font(self._font, self._font_size) - except: - _warnings.warn(f"""We couldn't find the font file '{self._font}'. We'll use the default font instead for now. -To fix this, either set the font to None, or make sure you have a font file (usually called something like Arial.ttf) in your project folder.\n""", - Hmm) + except: # pylint: disable=bare-except + _warnings.warn( + f"""We couldn't find the font file '{self._font}'. We'll use the default font instead for now.""" + # pylint: disable=line-too-long +"""To fix this, either set the font to None, or make sure you have a font file (usually called something like Arial.ttf) in your project folder.\n""", # pylint: disable=line-too-long + Hmm, + ) self._pygame_font = pygame.font.Font(None, self._font_size) - self._primary_pygame_surface = self._pygame_font.render(self._words, True, _color_name_to_rgb(self._color)) + self._primary_pygame_surface = self._pygame_font.render( + self._words, True, _color_name_to_rgb(self._color) + ) self._should_recompute_primary_surface = False self._compute_secondary_surface(force=True) @@ -89,6 +107,25 @@ def color(self, color_): self._should_recompute_primary_surface = True -def new_text(words='hi :)', x=0, y=0, font=None, font_size=50, color='black', angle=0, transparency=100, size=100): - return Text(words=words, x=x, y=y, font=font, font_size=font_size, color=color, angle=angle, - transparency=transparency, size=size) \ No newline at end of file +def new_text( # pylint: disable=too-many-arguments + words="hi :)", + x=0, + y=0, + font=None, + font_size=50, + color="black", + angle=0, + transparency=100, + size=100, +): + return Text( + words=words, + x=x, + y=y, + font=font, + font_size=font_size, + color=color, + angle=angle, + transparency=transparency, + size=size, + ) diff --git a/play/physics/__init__.py b/play/physics/__init__.py index efc7f58..26ad085 100644 --- a/play/physics/__init__.py +++ b/play/physics/__init__.py @@ -1,13 +1,26 @@ -import pymunk as _pymunk +"""This handles the physics of the game.""" + import math as _math +import pymunk as _pymunk from ..clamp import _clamp _SPEED_MULTIPLIER = 10 -class _Physics(object): - - def __init__(self, sprite, can_move, stable, x_speed, y_speed, obeys_gravity, bounciness, mass, friction): +class _Physics(): + + def __init__( # pylint: disable=too-many-arguments + self, + sprite, + can_move, + stable, + x_speed, + y_speed, + obeys_gravity, + bounciness, + mass, + friction, + ): """ Examples of objects with the different parameters: @@ -40,23 +53,31 @@ def __init__(self, sprite, can_move, stable, x_speed, y_speed, obeys_gravity, bo self._make_pymunk() - def _make_pymunk(self): + def _make_pymunk(self): # pylint: disable=too-many-branches mass = self.mass if self.can_move else 0 # non-moving line shapes are platforms and it's easier to take care of them less-generically if not self.can_move and self.sprite.__class__ == "Line": self._pymunk_body = physics_space.static_body.copy() - self._pymunk_shape = _pymunk.Segment(self._pymunk_body, (self.sprite.x, self.sprite.y), - (self.sprite.x1, self.sprite.y1), self.sprite.thickness) + self._pymunk_shape = _pymunk.Segment( + self._pymunk_body, + (self.sprite.x, self.sprite.y), + (self.sprite.x1, self.sprite.y1), + self.sprite.thickness, + ) else: if self.stable: - moment = _pymunk.inf + moment = float("inf") elif self.sprite.__class__ == "Circle": moment = _pymunk.moment_for_circle(mass, 0, self.sprite.radius, (0, 0)) elif self.sprite.__class__ == "Line": - moment = _pymunk.moment_for_box(mass, (self.sprite.length, self.sprite.thickness)) + moment = _pymunk.moment_for_box( + mass, (self.sprite.length, self.sprite.thickness) + ) else: - moment = _pymunk.moment_for_box(mass, (self.sprite.width, self.sprite.height)) + moment = _pymunk.moment_for_box( + mass, (self.sprite.width, self.sprite.height) + ) if self.can_move and not self.stable: body_type = _pymunk.Body.DYNAMIC @@ -70,8 +91,10 @@ def _make_pymunk(self): self._pymunk_body = _pymunk.Body(mass, moment, body_type=body_type) if self.sprite.__class__ == "Line": - self._pymunk_body.position = self.sprite.x + (self.sprite.x1 - self.sprite.x) / 2, self.sprite.y + ( - self.sprite.y1 - self.sprite.y) / 2 + self._pymunk_body.position = ( + self.sprite.x + (self.sprite.x1 - self.sprite.x) / 2, + self.sprite.y + (self.sprite.y1 - self.sprite.y) / 2, + ) else: self._pymunk_body.position = self.sprite.x, self.sprite.y @@ -81,24 +104,39 @@ def _make_pymunk(self): self._pymunk_body.velocity = (self._x_speed, self._y_speed) if not self.obeys_gravity: - self._pymunk_body.velocity_func = lambda body, gravity, damping, dt: None + self._pymunk_body.velocity_func = ( + lambda body, gravity, damping, dt: None + ) if self.sprite.__class__ == "Circle": - self._pymunk_shape = _pymunk.Circle(self._pymunk_body, self.sprite.radius, (0, 0)) + self._pymunk_shape = _pymunk.Circle( + self._pymunk_body, self.sprite.radius, (0, 0) + ) elif self.sprite.__class__ == "Line": - self._pymunk_shape = _pymunk.Segment(self._pymunk_body, (self.sprite.x, self.sprite.y), - (self.sprite.x1, self.sprite.y1), self.sprite.thickness) + self._pymunk_shape = _pymunk.Segment( + self._pymunk_body, + (self.sprite.x, self.sprite.y), + (self.sprite.x1, self.sprite.y1), + self.sprite.thickness, + ) else: - self._pymunk_shape = _pymunk.Poly.create_box(self._pymunk_body, (self.sprite.width, self.sprite.height)) + self._pymunk_shape = _pymunk.Poly.create_box( + self._pymunk_body, (self.sprite.width, self.sprite.height) + ) - self._pymunk_shape.elasticity = _clamp(self.bounciness, 0, .99) + self._pymunk_shape.elasticity = _clamp(self.bounciness, 0, 0.99) self._pymunk_shape.friction = self._friction physics_space.add(self._pymunk_body, self._pymunk_shape) def clone(self, sprite): # TODO: finish filling out params - return self.__class__(sprite=sprite, can_move=self.can_move, x_speed=self.x_speed, - y_speed=self.y_speed, obeys_gravity=self.obeys_gravity) + return self.__class__( # pylint: disable=no-value-for-parameter + sprite=sprite, + can_move=self.can_move, + x_speed=self.x_speed, + y_speed=self.y_speed, + obeys_gravity=self.obeys_gravity, + ) def pause(self): self._remove() @@ -150,7 +188,7 @@ def bounciness(self): @bounciness.setter def bounciness(self, _bounciness): self._bounciness = _bounciness - self._pymunk_shape.elasticity = _clamp(self._bounciness, 0, .99) + self._pymunk_shape.elasticity = _clamp(self._bounciness, 0, 0.99) @property def stable(self): @@ -186,26 +224,28 @@ def obeys_gravity(self, _obeys_gravity): self._pymunk_body.velocity_func = lambda body, gravity, damping, dt: None -class _Gravity(object): +class _Gravity(): # pylint: disable=too-few-public-methods # TODO: make this default to vertical if horizontal is 0? vertical = -100 * _SPEED_MULTIPLIER horizontal = 0 -gravity = _Gravity() +GRAVITY = _Gravity() physics_space = _pymunk.Space() physics_space.sleep_time_threshold = 0.5 -physics_space.idle_speed_threshold = 0 # pymunk estimates good threshold based on gravity -physics_space.gravity = gravity.horizontal, gravity.vertical +physics_space.idle_speed_threshold = ( + 0 # pymunk estimates good threshold based on gravity +) +physics_space.gravity = GRAVITY.horizontal, GRAVITY.vertical def set_gravity(vertical=-100, horizontal=None): - global gravity - gravity.vertical = vertical * _SPEED_MULTIPLIER - if horizontal != None: - gravity.horizontal = horizontal * _SPEED_MULTIPLIER + global GRAVITY # pylint: disable=global-variable-not-assigned + GRAVITY.vertical = vertical * _SPEED_MULTIPLIER + if horizontal is not None: + GRAVITY.horizontal = horizontal * _SPEED_MULTIPLIER - physics_space.gravity = gravity.horizontal, gravity.vertical + physics_space.gravity = GRAVITY.horizontal, GRAVITY.vertical _NUM_SIMULATION_STEPS = 3 diff --git a/play/play.py b/play/play.py index 052f4f1..3d4b060 100644 --- a/play/play.py +++ b/play/play.py @@ -1,30 +1,36 @@ +"""The main source file of the play library.""" + import logging as _logging -import warnings as _warnings -from .async_helpers import _make_async -import pygame +import math as _math +import random as _random +import asyncio as _asyncio -pygame.init() +import pygame import pygame.gfxdraw -import asyncio as _asyncio -import random as _random +from .async_helpers import _make_async + -from .keypress import \ - pygame_key_to_name as _pygame_key_to_name # don't pollute user-facing namespace with library internals from .color import color_name_to_rgb as _color_name_to_rgb -from .exceptions import Oops, Hmm -import math as _math -from .io import screen, _pygame_display + +from .io import screen, PYGAME_DISPLAY from .physics import simulate_physics from .all_sprites import all_sprites from .objects import Line -from .objects.sprite import _point_touching_sprite +from .objects.sprite import point_touching_sprite -# _pygame_display = pygame.display.set_mode((screen_width, screen_height), pygame.DOUBLEBUF | pygame.OPENGL) +pygame.init() # pylint: disable=no-member + +# pylint: disable=wrong-import-position +from .keypress import ( + pygame_key_to_name as _pygame_key_to_name, _loop, _keys_pressed_this_frame, _keys_released_this_frame, + _keys_to_skip, _pressed_keys, _keypress_callbacks, _keyrelease_callbacks, +) # don't pollute user-facing namespace with library internals +# _pygame_display = pygame.display.set_mode((screen_width, screen_height), pygame.DOUBLEBUF | pygame.OPENGL) -class _Mouse(object): +class _Mouse(): def __init__(self): self.x = 0 self.y = 0 @@ -42,7 +48,7 @@ def is_clicked(self): return self._is_clicked def is_touching(self, other): - return _point_touching_sprite(self, other) + return point_touching_sprite(self, other) # @decorator def when_clicked(self, func): @@ -65,20 +71,19 @@ async def wrapper(): return wrapper def distance_to(self, x=None, y=None): - assert (not x is None) + assert x is not None try: # x can either by a number or a sprite. If it's a sprite: x = x.x y = x.y except AttributeError: - x = x - y = y + pass dx = self.x - x dy = self.y - y - return _math.sqrt(dx ** 2 + dy ** 2) + return _math.sqrt(dx**2 + dy**2) # @decorator @@ -93,22 +98,22 @@ def when_click_released(func): mouse = _Mouse() -_debug = True +_DEBUG = True def debug(on_or_off): - global _debug - if on_or_off == 'on': - _debug = True - elif on_or_off == 'off': - _debug = False + global _DEBUG + if on_or_off == "on": + _DEBUG = True + elif on_or_off == "off": + _DEBUG = False -backdrop = (255, 255, 255) +BACKDROP = (255, 255, 255) def set_backdrop(color_or_image_name): - global backdrop + global BACKDROP # I chose to make set_backdrop a function so that we can give # good error messages at the call site if a color isn't recognized. @@ -122,23 +127,22 @@ def set_backdrop(color_or_image_name): # this line will raise a useful exception _color_name_to_rgb(color_or_image_name) - backdrop = color_or_image_name + BACKDROP = color_or_image_name def random_number(lowest=0, highest=100): # if user supplies whole numbers, return whole numbers - if type(lowest) == int and type(highest) == int: + if isinstance(lowest, int) and isinstance(highest, int): return _random.randint(lowest, highest) - else: - # if user supplied any floats, return decimals - return round(_random.uniform(lowest, highest), 2) + # if user supplied any floats, return decimals + return round(_random.uniform(lowest, highest), 2) def random_color(): - return (random_number(0, 255), random_number(0, 255), random_number(0, 255)) + return random_number(0, 255), random_number(0, 255), random_number(0, 255) -class _Position(object): +class _Position(): def __init__(self, x, y): self.x = x self.y = y @@ -146,7 +150,7 @@ def __init__(self, x, y): def __getitem__(self, indices): if indices == 0: return self.x - elif indices == 1: + if indices == 1: return self.y raise IndexError() @@ -178,7 +182,7 @@ def random_position(): """ return _Position( random_number(screen.left, screen.right), - random_number(screen.bottom, screen.top) + random_number(screen.bottom, screen.top), ) @@ -192,125 +196,10 @@ def wrapper(func): return wrapper -pygame.key.set_repeat(200, 16) -_pressed_keys = {} -_keypress_callbacks = [] -_keyrelease_callbacks = [] - - -# @decorator -def when_any_key_pressed(func): - if not callable(func): - raise Oops("""@play.when_any_key_pressed doesn't use a list of keys. Try just this instead: - -@play.when_any_key_pressed -async def do(key): - print("This key was pressed!", key) -""") - async_callback = _make_async(func) - - async def wrapper(*args, **kwargs): - wrapper.is_running = True - await async_callback(*args, **kwargs) - wrapper.is_running = False - - wrapper.keys = None - wrapper.is_running = False - _keypress_callbacks.append(wrapper) - return wrapper - - -# @decorator -def when_key_pressed(*keys): - def decorator(func): - async_callback = _make_async(func) - - async def wrapper(*args, **kwargs): - wrapper.is_running = True - await async_callback(*args, **kwargs) - wrapper.is_running = False - - wrapper.keys = keys - wrapper.is_running = False - _keypress_callbacks.append(wrapper) - return wrapper - - return decorator - - -# @decorator -def when_any_key_released(func): - if not callable(func): - raise Oops("""@play.when_any_key_released doesn't use a list of keys. Try just this instead: - -@play.when_any_key_released -async def do(key): - print("This key was released!", key) -""") - async_callback = _make_async(func) - - async def wrapper(*args, **kwargs): - wrapper.is_running = True - await async_callback(*args, **kwargs) - wrapper.is_running = False - - wrapper.keys = None - wrapper.is_running = False - _keyrelease_callbacks.append(wrapper) - return wrapper - - -# @decorator -def when_key_released(*keys): - def decorator(func): - async_callback = _make_async(func) - - async def wrapper(*args, **kwargs): - wrapper.is_running = True - await async_callback(*args, **kwargs) - wrapper.is_running = False - - wrapper.keys = keys - wrapper.is_running = False - _keyrelease_callbacks.append(wrapper) - return wrapper - - return decorator - - -def key_is_pressed(*keys): - """ - Returns True if any of the given keys are pressed. - - Example: - - @play.repeat_forever - async def do(): - if play.key_is_pressed('up', 'w'): - print('up or w pressed') - """ - # Called this function key_is_pressed instead of is_key_pressed so it will - # sound more english-like with if-statements: - # - # if play.key_is_pressed('w', 'up'): ... - - for key in keys: - if key in _pressed_keys.values(): - return True - return False - - -_loop = _asyncio.get_event_loop() -_loop.set_debug(False) - -_keys_pressed_this_frame = [] -_keys_released_this_frame = [] -_keys_to_skip = (pygame.K_MODE,) -pygame.event.set_allowed( - [pygame.QUIT, pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION]) _clock = pygame.time.Clock() +# pylint: disable=too-many-branches, too-many-statements def _game_loop(): _keys_pressed_this_frame.clear() # do this instead of `_keys_pressed_this_frame = []` to save a tiny bit of memory _keys_released_this_frame.clear() @@ -319,27 +208,33 @@ def _game_loop(): _clock.tick(60) for event in pygame.event.get(): - if event.type == pygame.QUIT or ( - event.type == pygame.KEYDOWN and event.key == pygame.K_q and ( - pygame.key.get_mods() & pygame.KMOD_META or pygame.key.get_mods() & pygame.KMOD_CTRL - )): + if event.type == pygame.QUIT or ( # pylint: disable=no-member + event.type == pygame.KEYDOWN # pylint: disable=no-member + and event.key == pygame.K_q # pylint: disable=no-member + and ( + pygame.key.get_mods() & pygame.KMOD_META # pylint: disable=no-member + or pygame.key.get_mods() & pygame.KMOD_CTRL # pylint: disable=no-member + ) + ): # quitting by clicking window's close button or pressing ctrl+q / command+q _loop.stop() return False - if event.type == pygame.MOUSEBUTTONDOWN: + if event.type == pygame.MOUSEBUTTONDOWN: # pylint: disable=no-member click_happened_this_frame = True mouse._is_clicked = True - if event.type == pygame.MOUSEBUTTONUP: + if event.type == pygame.MOUSEBUTTONUP: # pylint: disable=no-member click_release_happened_this_frame = True mouse._is_clicked = False - if event.type == pygame.MOUSEMOTION: - mouse.x, mouse.y = (event.pos[0] - screen.width / 2.), (screen.height / 2. - event.pos[1]) - if event.type == pygame.KEYDOWN: - if not (event.key in _keys_to_skip): + if event.type == pygame.MOUSEMOTION: # pylint: disable=no-member + mouse.x, mouse.y = (event.pos[0] - screen.width / 2.0), ( + screen.height / 2.0 - event.pos[1] + ) + if event.type == pygame.KEYDOWN: # pylint: disable=no-member + if event.key not in _keys_to_skip: name = _pygame_key_to_name(event) _pressed_keys[event.key] = name _keys_pressed_this_frame.append(name) - if event.type == pygame.KEYUP: + if event.type == pygame.KEYUP: # pylint: disable=no-member if not (event.key in _keys_to_skip) and event.key in _pressed_keys: _keys_released_this_frame.append(_pressed_keys[event.key]) del _pressed_keys[event.key] @@ -349,7 +244,9 @@ def _game_loop(): ############################################################ for key in _keys_pressed_this_frame: for callback in _keypress_callbacks: - if not callback.is_running and (callback.keys is None or key in callback.keys): + if not callback.is_running and ( + callback.keys is None or key in callback.keys + ): _loop.create_task(callback(key)) ############################################################ @@ -357,7 +254,9 @@ def _game_loop(): ############################################################ for key in _keys_released_this_frame: for callback in _keyrelease_callbacks: - if not callback.is_running and (callback.keys is None or key in callback.keys): + if not callback.is_running and ( + callback.keys is None or key in callback.keys + ): _loop.create_task(callback(key)) #################################### @@ -399,7 +298,7 @@ def _game_loop(): # 10. render sprites (with correct z-order) # 11. call event loop again - _pygame_display.fill(_color_name_to_rgb(backdrop)) + PYGAME_DISPLAY.fill(_color_name_to_rgb(BACKDROP)) # BACKGROUND COLOR # note: cannot use screen.fill((1, 1, 1)) because pygame's screen @@ -428,25 +327,28 @@ def _game_loop(): sprite._y1 = body.position.y + (sprite.length / 2) * _math.sin(angle) # sprite._length, sprite._angle = sprite._calc_length_angle() else: - if str(body.position.x) != 'nan': # this condition can happen when changing sprite.physics.can_move + if ( + str(body.position.x) != "nan" + ): # this condition can happen when changing sprite.physics.can_move sprite._x = body.position.x - if str(body.position.y) != 'nan': + if str(body.position.y) != "nan": sprite._y = body.position.y - sprite.angle = angle # needs to be .angle, not ._angle so surface gets recalculated + sprite.angle = ( + angle # needs to be .angle, not ._angle so surface gets recalculated + ) sprite.physics._x_speed, sprite.physics._y_speed = body.velocity ################################# # @sprite.when_clicked events ################################# - if mouse.is_clicked and not type(sprite) == Line: - if _point_touching_sprite(mouse, sprite): + if mouse.is_clicked and not isinstance(sprite, Line): + if point_touching_sprite(mouse, sprite) and click_happened_this_frame: # only run sprite clicks on the frame the mouse was clicked - if click_happened_this_frame: - sprite._is_clicked = True - for callback in sprite._when_clicked_callbacks: - if not callback.is_running: - _loop.create_task(callback()) + sprite._is_clicked = True + for callback in sprite._when_clicked_callbacks: + if not callback.is_running: + _loop.create_task(callback()) # do sprite image transforms (re-rendering images/fonts, scaling, rotating, etc) @@ -458,7 +360,7 @@ def _game_loop(): elif sprite._should_recompute_secondary_surface: _loop.call_soon(sprite._compute_secondary_surface) - if type(sprite) == Line: + if isinstance(sprite, Line): # @hack: Line-drawing code should probably be in the line._compute_primary_surface function # but the coordinates work different for lines than other sprites. @@ -466,22 +368,41 @@ def _game_loop(): # y = screen.height/2 - sprite.y - sprite.thickness # _pygame_display.blit(sprite._secondary_pygame_surface, (x,y) ) - x = screen.width / 2 + sprite.x - y = screen.height / 2 - sprite.y - x1 = screen.width / 2 + sprite.x1 - y1 = screen.height / 2 - sprite.y1 + x = screen.width / 2 + sprite.x # pylint: disable=invalid-name + y = screen.height / 2 - sprite.y # pylint: disable=invalid-name + x_1 = screen.width / 2 + sprite.x1 + y_1 = screen.height / 2 - sprite.y1 if sprite.thickness == 1: - pygame.draw.aaline(_pygame_display, _color_name_to_rgb(sprite.color), (x, y), (x1, y1), True) + pygame.draw.aaline( + PYGAME_DISPLAY, + _color_name_to_rgb(sprite.color), + (x, y), + (x_1, y_1), + True, + ) else: - pygame.draw.line(_pygame_display, _color_name_to_rgb(sprite.color), (x, y), (x1, y1), sprite.thickness) + pygame.draw.line( + PYGAME_DISPLAY, + _color_name_to_rgb(sprite.color), + (x, y), + (x_1, y_1), + sprite.thickness, + ) else: - _pygame_display.blit(sprite._secondary_pygame_surface, (sprite._pygame_x(), sprite._pygame_y())) + + PYGAME_DISPLAY.blit( + sprite._secondary_pygame_surface, + (sprite._pygame_x(), sprite._pygame_y()), + ) pygame.display.flip() _loop.call_soon(_game_loop) return True +# pylint: enable=too-many-branches, too-many-statements + + async def timer(seconds=1.0): """ Wait a number of seconds. Used with the await keyword like this: @@ -567,7 +488,7 @@ async def wrapper(*args, **kwargs): def repeat(number_of_times): """ - Repeat a set of commands a certain number of times. + Repeat a set of commands a certain number of times. Equivalent to `range(1, number_of_times+1)`. @@ -595,4 +516,4 @@ def start_program(): _loop.run_forever() finally: _logging.getLogger("asyncio").setLevel(_logging.CRITICAL) - pygame.quit() + pygame.quit() # pylint: disable=no-member diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bde1945 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.pylint.MASTER] +max-line-length = 120 +disable = ["missing-function-docstring", "missing-class-docstring", "duplicate-code", "protected-access", "global-statement","too-many-instance-attributes", "fixme"] +good-names = ["x","y","i","dx","dy","x1","y1","a","b"] +