Merge pull request #33 from n0remac/tests

Test Suite with pytest
This commit is contained in:
2020-04-21 03:53:50 -05:00
committed by GitHub
9 changed files with 287 additions and 37 deletions

31
triple-dungeon/.github/workflows/ci.yml vendored Normal file
View File

@@ -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

View File

@@ -1 +1,2 @@
.idea/**
.idea/**
.pytest_cache

View File

@@ -9,6 +9,7 @@ verify_ssl = true
arcade = "*"
networkx = "*"
pathfinding = "*"
pytest = "*"
[requires]
python_version = "3.7"

View File

@@ -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": {}

View File

@@ -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()

View File

@@ -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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

View File

@@ -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.
"""

134
triple-dungeon/tests.py Normal file
View File

@@ -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.
"""