diff --git a/triple-dungeon/.github/workflows/ci.yml b/triple-dungeon/.github/workflows/ci.yml new file mode 100644 index 0000000..dea89c5 --- /dev/null +++ b/triple-dungeon/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Continuous Integration (PEP8 Lint + pytest) + +on: push + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install pipenv & sync + run: | + # cd triple-dungeon + python -m pip install --upgrade pip + pip install pipenv + pipenv lock + pipenv sync + - name: Linting + run: | + pip install pycodestyle + # PyCharm sets line length at 120 chars + pycodestyle . --count --statistics --max-line-length=120 + - name: Test with pytest + run: | + pip install pytest + pytest tests.py \ No newline at end of file diff --git a/triple-dungeon/.gitignore b/triple-dungeon/.gitignore index d19812c..a80d927 100644 --- a/triple-dungeon/.gitignore +++ b/triple-dungeon/.gitignore @@ -1 +1,2 @@ -.idea/** \ No newline at end of file +.idea/** +.pytest_cache \ No newline at end of file diff --git a/triple-dungeon/Pipfile b/triple-dungeon/Pipfile index 2c24668..f754e3b 100644 --- a/triple-dungeon/Pipfile +++ b/triple-dungeon/Pipfile @@ -9,6 +9,7 @@ verify_ssl = true arcade = "*" networkx = "*" pathfinding = "*" +pytest = "*" [requires] python_version = "3.7" diff --git a/triple-dungeon/Pipfile.lock b/triple-dungeon/Pipfile.lock index 2c2ea7a..e525a08 100644 --- a/triple-dungeon/Pipfile.lock +++ b/triple-dungeon/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "072e8938371eb0a39830868ce97961536e15f31d82fcdf3019e870522684c661" + "sha256": "a4938aef96c9adc916384dca2f6b171095dc8fcbd90da0e757dfc73f1da98907" }, "pipfile-spec": 6, "requires": { @@ -23,6 +23,14 @@ "index": "pypi", "version": "==2.3.15" }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.3.0" + }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -30,6 +38,14 @@ ], "version": "==19.3.0" }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.3" + }, "decorator": { "hashes": [ "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", @@ -37,6 +53,21 @@ ], "version": "==4.4.2" }, + "importlib-metadata": { + "hashes": [ + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + ], + "markers": "python_version < '3.8'", + "version": "==1.6.0" + }, + "more-itertools": { + "hashes": [ + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" + ], + "version": "==8.2.0" + }, "networkx": { "hashes": [ "sha256:cdfbf698749a5014bf2ed9db4a07a5295df1d3a53bf80bf3cbd61edf9df05fa1", @@ -71,6 +102,13 @@ ], "version": "==1.18.3" }, + "packaging": { + "hashes": [ + "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", + "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + ], + "version": "==20.3" + }, "pathfinding": { "hashes": [ "sha256:3e3809abada1fdb292ced85cd7f63700c4f28a20f10eae68168a760be52ca746", @@ -106,6 +144,20 @@ ], "version": "==7.1.1" }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, "pyglet": { "hashes": [ "sha256:bd96b9c374a7192e4787f989e8f911719476c71476466a02312839e8cb6a0d4e", @@ -113,11 +165,47 @@ ], "version": "==1.5.3" }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", + "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" + ], + "index": "pypi", + "version": "==5.4.1" + }, "pytiled-parser": { "hashes": [ "sha256:7664f5ed291fa51bad131c0f844f80c81ee604c768eb05ebe29530ddff4a0000" ], "version": "==0.9.3" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "wcwidth": { + "hashes": [ + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + ], + "version": "==0.1.9" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "version": "==3.1.0" } }, "develop": {} diff --git a/triple-dungeon/main.py b/triple-dungeon/main.py index 02cff08..4d3b3d4 100644 --- a/triple-dungeon/main.py +++ b/triple-dungeon/main.py @@ -3,17 +3,17 @@ main.py The main class used to load the game. Holds the main game window, as well as manages basic functions for organizing the game. """ + import collections +import math import random import time import arcade -import math from config import Config from map import Dungeon -from mobs import Player, Enemy -from config import Config +from mobs import Player from projectiles import Temp @@ -45,23 +45,16 @@ class Game(arcade.Window): # Call the parent class and set up the window super().__init__(Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT, Config.SCREEN_TITLE) - # These are 'lists' that keep track of our sprites. Each sprite should - # go into a list. + # Sprite Lists self.enemy_list = None self.bullet_list = None self.player = None - + # Game Objects self.dungeon = None - - # list to keep track of keypresses - self.prev_keypress = [] - - # Our physics engine - self.physics_engine = None - + self.prev_keypress = [] # A list that assists with tracking keypress events + self.physics_engine = None # Our physics engine # Used to keep track of our scrolling - self.view_bottom = 0 - self.view_left = 0 + self.view_bottom = self.view_left = 0 arcade.set_background_color(arcade.color.BLACK) @@ -74,7 +67,7 @@ class Game(arcade.Window): self.bullet_list = arcade.SpriteList() # Create the dungeon - self.dungeon = Dungeon(0, 8) + self.dungeon = Dungeon(0, 3) # Set up the player, specifically placing it at these coordinates. self.player = Player() @@ -110,12 +103,16 @@ class Game(arcade.Window): self.enemy_list.draw() self.bullet_list.draw() - self.player.draw_hit_box() - x, y = self.player.center_x, self.player.center_y - arcade.draw_text(str((x, y)), x - 40, y + 50, arcade.color.WHITE, 15, font_name='Arial') - arcade.draw_text(f"FPS: {self.fps.get_fps():3.0f}", self.view_left + 50, self.view_bottom + 30, - arcade.color.WHITE, 16, font_name='Arial') - self.fps.tick() + if Config.DEBUG: + x, y = self.player.position + tile = Config.TILE_WIDTH * Config.TILE_SCALING + arcade.draw_rectangle_outline(round(x / tile) * tile, round(y / tile) * tile, tile, tile, + arcade.color.RED) + self.player.draw_hit_box() + arcade.draw_text(str((x, y)), x - 40, y + 50, arcade.color.WHITE, 15, font_name='Arial') + arcade.draw_text(f"FPS: {self.fps.get_fps():3.0f}", self.view_left + 50, self.view_bottom + 30, + arcade.color.WHITE, 16, font_name='Arial') + self.fps.tick() except Exception: import traceback traceback.print_exc() @@ -169,8 +166,8 @@ class Game(arcade.Window): bullet.center_y = start_y # Get from the mouse the destination location for the bullet - dest_x = x+self.view_left - dest_y = y+self.view_bottom + dest_x = x + self.view_left + dest_y = y + self.view_bottom # Do math to calculate how to get the bullet to the destination. # Calculation the angle in radians between the start points @@ -245,10 +242,10 @@ class Game(arcade.Window): # If the bullet flies off-screen, remove it. TEMP change to range calc if ( - bullet.bottom < self.view_bottom or - bullet.top > self.view_bottom+Config.SCREEN_HEIGHT or - bullet.right > self.view_left+Config.SCREEN_WIDTH or - bullet.left < self.view_left + bullet.bottom < self.view_bottom or + bullet.top > self.view_bottom + Config.SCREEN_HEIGHT or + bullet.right > self.view_left + Config.SCREEN_WIDTH or + bullet.left < self.view_left ): bullet.remove_from_sprite_lists() diff --git a/triple-dungeon/map.py b/triple-dungeon/map.py index 10f09b0..8394d7b 100644 --- a/triple-dungeon/map.py +++ b/triple-dungeon/map.py @@ -7,12 +7,10 @@ Pathfinding will also depend on objects here, and is thus integral to it's funct from __future__ import annotations import json - import arcade import numpy as np from itertools import chain - from config import Config diff --git a/triple-dungeon/resources/images/monsters/skeleton.png b/triple-dungeon/resources/images/monsters/skeleton.png deleted file mode 100644 index ad492d3..0000000 Binary files a/triple-dungeon/resources/images/monsters/skeleton.png and /dev/null differ diff --git a/triple-dungeon/sprites.py b/triple-dungeon/sprites.py index 2b5f5f9..f1c216c 100644 --- a/triple-dungeon/sprites.py +++ b/triple-dungeon/sprites.py @@ -2,13 +2,13 @@ sprites.py A file dedicated to managing sprites and animations for characters. """ -from itertools import cycle import arcade import os import re -from typing import Pattern +from typing import Pattern, Iterable +from itertools import cycle class AnimationSet(object): @@ -16,7 +16,7 @@ class AnimationSet(object): A class that helps assist with grabbing new animations from a set. """ - def __init__(self, directory: str): + def __init__(self, directory: str) -> None: """ Initializes the AnimationSet class by loading files and @@ -26,7 +26,7 @@ class AnimationSet(object): self.directory = directory self.animations = os.listdir(directory) - def getAnimations(self, pattern: Pattern) -> iter: + def getAnimations(self, pattern: Pattern) -> Iterable[arcade.Texture]: """ Loads all animations from the AnimationSet's directory that match the pattern. The pattern must have 1 group that specifies the animation's index. @@ -56,7 +56,7 @@ class PlayerAnimations(AnimationSet): index: [0,) - The index of the animation. Index should be enumerated in ascending order. """ - def __init__(self, directory: str): + def __init__(self, directory: str) -> None: """ Initializes the PlayerAnimations class. """ diff --git a/triple-dungeon/tests.py b/triple-dungeon/tests.py new file mode 100644 index 0000000..e0edd15 --- /dev/null +++ b/triple-dungeon/tests.py @@ -0,0 +1,134 @@ +""" +tests.py +A file dedicated to testing our game and ensuring it can run. +Integrate this into your IDE's workflow to ensure the game runs from top to bottom. +The tests used here should test all of our game's features as best they can. +""" + +import pytest + +from typing import Pattern, List + + +class TestGame: + """ + Tests that the Arcade framework runs the game correctly. + Only tests that it launches and runs for a little bit, not that it is functioning properly. + """ + + def test_game_runs(self) -> None: + """ + Simply test that the Game runs. + """ + + # imports + from main import Game + + # instantiate and setup + game = Game() + game.setup() + game.minimize() # Minimizes window, should reduce annoyance a little bit. + # test for 100 frames + game.test(20) + + +class TestSprites: + """ + Tests the Sprite classes as well as the available sprites. + """ + + @pytest.fixture + def sprites(self) -> List[str]: + """ + :return: List of absolute paths to Sprite images + """ + + import os + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + IMAGE_DIR = os.path.join(BASE_DIR, 'resources', 'images') + + _sprites = [] + for primary in os.listdir(IMAGE_DIR): + for secondary in os.listdir(os.path.join(IMAGE_DIR, primary)): + secondary = os.path.join(IMAGE_DIR, primary, secondary) + if os.path.isfile(secondary): + _sprites.append(secondary) + else: + _sprites.extend( + os.path.join(secondary, file) for file in + os.listdir(os.path.join(IMAGE_DIR, primary, secondary))) + return _sprites + + @pytest.fixture + def patterns(self) -> List[Pattern]: + """ + :return: A list of Pattern objects to test. + """ + import re + _patterns = [ + r'\w+_(?:\w+_)?\d+\.(?:jp(?:eg|e|g)|png)', + r'\w+\d+\.(?:jp(?:eg|e|g)|png)', + r'\w+_tile\.(?:jp(?:eg|e|g)|png)' + ] + return list(map(re.compile, _patterns)) + + def test_sprite_schema(self, sprites: List[str], patterns: List[Pattern]) -> None: + """ + Tests that all sprites follow the naming conventions. + """ + import os + + for sprite in sprites: + head, tail = os.path.split(sprite) + if any(pattern.match(tail) is not None for pattern in patterns): + continue + pytest.fail(f"Sprite '{tail}' in '{head}' did not match the schema.") + + def test_sprite_loads(self, sprites) -> None: + """ + Tests that all sprites can be loaded by the arcade framework. + """ + import arcade + + for sprite in sprites: + _sprite = arcade.Sprite(sprite) + + +class TestLevels: + """ + Tests the Level class. + """ + + @pytest.fixture + def levels(self) -> List[str]: + """ + :return: List of paths to Level files + """ + import os + + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + LEVEL_DIR = os.path.join(BASE_DIR, 'resources', 'levels') + levels = [os.path.join(LEVEL_DIR, file) for file in os.listdir(LEVEL_DIR)] + + return levels + + def test_levels_are_loadable(self, levels) -> None: + """ + Tests whether or not a level can be loaded. + """ + from map import Level + + for level in levels: + Level.load_file(2, 3, level) + + +class TestDungeon: + """ + Tests the Dungeon class. + """ + + +class TestMisc: + """ + Tests things that don't fit anywhere else. + """