Rename dungeonbattle to squirrelbattle

This commit is contained in:
Yohann D'ANELLO
2020-11-19 02:18:08 +01:00
parent 9232f67dc9
commit d0ee9ec562
30 changed files with 43 additions and 43 deletions

View File

@ -1,21 +0,0 @@
from dungeonbattle.game import Game
from dungeonbattle.display.display_manager import DisplayManager
from dungeonbattle.term_manager import TermManager
class Bootstrap:
"""
The bootstrap object is used to bootstrap the game so that it starts
properly.
(It was initially created to avoid circular imports between the Game and
Display classes)
"""
@staticmethod
def run_game():
with TermManager() as term_manager: # pragma: no cover
game = Game()
game.new_game()
display = DisplayManager(term_manager.screen, game)
game.display_actions = display.handle_display_action
game.run(term_manager.screen)

View File

@ -1,52 +0,0 @@
import curses
from typing import Any, Optional, Union
from dungeonbattle.display.texturepack import TexturePack
from dungeonbattle.tests.screen import FakePad
class Display:
x: int
y: int
width: int
height: int
pad: Any
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
self.screen = screen
self.pack = pack or TexturePack.get_pack("ascii")
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
return curses.newpad(height, width) if self.screen else FakePad()
def init_pair(self, number: int, foreground: int, background: int) -> None:
return curses.init_pair(number, foreground, background) \
if self.screen else None
def color_pair(self, number: int) -> int:
return curses.color_pair(number) if self.screen else 0
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
self.x = x
self.y = y
self.width = width
self.height = height
if hasattr(self, "pad") and resize_pad:
self.pad.resize(self.height - 1, self.width - 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
if len(args) == 4:
self.resize(*args, resize_pad)
self.display()
def display(self) -> None:
raise NotImplementedError
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42

View File

@ -1,70 +0,0 @@
import curses
from dungeonbattle.display.mapdisplay import MapDisplay
from dungeonbattle.display.statsdisplay import StatsDisplay
from dungeonbattle.display.menudisplay import SettingsMenuDisplay, \
MainMenuDisplay
from dungeonbattle.display.texturepack import TexturePack
from typing import Any
from dungeonbattle.game import Game, GameMode
from dungeonbattle.enums import DisplayActions
class DisplayManager:
def __init__(self, screen: Any, g: Game):
self.game = g
self.screen = screen
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay = MapDisplay(screen, pack)
self.statsdisplay = StatsDisplay(screen, pack)
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None:
if action == DisplayActions.REFRESH:
self.refresh()
elif action == DisplayActions.UPDATE:
self.update_game_components()
def update_game_components(self) -> None:
for d in self.displays:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
def refresh(self) -> None:
if self.game.state == GameMode.PLAY:
# The map pad has already the good size
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols,
resize_pad=False)
self.statsdisplay.refresh(self.rows * 4 // 5, 0,
self.rows // 5, self.cols)
if self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
if self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1)
self.resize_window()
def resize_window(self) -> bool:
"""
If the window got resized, ensure that the screen size got updated.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows,
self.cols): # pragma: nocover
curses.resizeterm(y, x)
return True
return False
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python
from dungeonbattle.interfaces import Map
from .display import Display
class MapDisplay(Display):
def __init__(self, *args):
super().__init__(*args)
def update_map(self, m: Map) -> None:
self.map = m
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.pad.addstr(0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for e in self.map.entities:
self.pad.addstr(e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
pminrow, pmincol = y - deltay, x - deltax
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
deltay, deltax = self.height - deltay, self.width - deltax
smaxrow = self.map.height - (y + deltay) + self.height - 1
smaxrow = min(smaxrow, self.height - 1)
smaxcol = self.pack.tile_width * self.map.width - \
(x + deltax) + self.width - 1
smaxcol = min(smaxcol, self.width - 1)
pminrow = max(0, min(self.map.height, pminrow))
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
self.pad.clear()
self.update_pad()
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)

View File

@ -1,94 +0,0 @@
from typing import List
from dungeonbattle.menus import Menu, MainMenu
from .display import Display
class MenuDisplay(Display):
position: int
def __init__(self, *args):
super().__init__(*args)
self.menubox = self.newpad(self.rows, self.cols)
def update_menu(self, menu: Menu) -> None:
self.menu = menu
self.trueheight = len(self.values)
self.truewidth = max([len(a) for a in self.values])
# Menu values are printed in pad
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i])
def update_pad(self) -> None:
for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i])
# set a marker on the selected line
self.pad.addstr(self.menu.position, 0, ">")
def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
else self.trueheight - self.height + 2 \
if self.height - 2 >= self.trueheight - self.menu.position else 0
# Menu box
self.menubox.addstr(0, 0, "" + "" * (self.width - 2) + "")
for i in range(1, self.height - 1):
self.menubox.addstr(i, 0, "" + " " * (self.width - 2) + "")
self.menubox.addstr(self.height - 1, 0,
"" + "" * (self.width - 2) + "")
self.menubox.refresh(0, 0, self.y, self.x,
self.height + self.y,
self.width + self.x)
self.update_pad()
self.pad.refresh(cornery, 0, self.y + 1, self.x + 2,
self.height - 2 + self.y,
self.width - 2 + self.x)
@property
def preferred_width(self) -> int:
return self.truewidth + 6
@property
def preferred_height(self) -> int:
return self.trueheight + 2
@property
def values(self) -> List[str]:
return [str(a) for a in self.menu.values]
class SettingsMenuDisplay(MenuDisplay):
@property
def values(self) -> List[str]:
return [a[1][1] + (" : "
+ ("?" if self.menu.waiting_for_key else a[1][0])
if a[1][0] else "") for a in self.menu.values]
class MainMenuDisplay(Display):
def __init__(self, menu: MainMenu, *args):
super().__init__(*args)
self.menu = menu
with open("resources/ascii_art.txt", "r") as file:
self.title = file.read().split("\n")
self.pad = self.newpad(max(self.rows, len(self.title) + 30),
max(len(self.title[0]) + 5, self.cols))
self.menudisplay = MenuDisplay(self.screen, self.pack)
self.menudisplay.update_menu(self.menu)
def display(self) -> None:
for i in range(len(self.title)):
self.pad.addstr(4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i])
self.pad.refresh(0, 0, self.y, self.x, self.height, self.width)
menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
self.menudisplay.refresh(
menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth)

View File

@ -1,52 +0,0 @@
import curses
from .display import Display
from dungeonbattle.entities.player import Player
class StatsDisplay(Display):
player: Player
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
def update_player(self, p: Player) -> None:
self.player = p
def update_pad(self) -> None:
string = ""
for _ in range(self.width - 1):
string = string + "-"
self.pad.addstr(0, 0, string)
string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
for _ in range(self.width - len(string2) - 1):
string2 = string2 + " "
self.pad.addstr(1, 0, string2)
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
for _ in range(self.width - len(string3) - 1):
string3 = string3 + " "
self.pad.addstr(2, 0, string3)
inventory_str = "Inventaire : " + "".join(
self.pack[item.name.upper()] for item in self.player.inventory)
self.pad.addstr(3, 0, inventory_str)
if self.player.dead:
self.pad.addstr(4, 0, "VOUS ÊTES MORT",
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
def display(self) -> None:
self.pad.clear()
self.update_pad()
self.pad.refresh(0, 0, self.y, self.x,
4 + self.y, self.width + self.x)

View File

@ -1,75 +0,0 @@
import curses
from typing import Any
class TexturePack:
_packs = dict()
name: str
tile_width: int
tile_fg_color: int
tile_bg_color: int
entity_fg_color: int
entity_bg_color: int
EMPTY: str
WALL: str
FLOOR: str
PLAYER: str
ASCII_PACK: "TexturePack"
SQUIRREL_PACK: "TexturePack"
def __init__(self, name: str, **kwargs):
self.name = name
self.__dict__.update(**kwargs)
TexturePack._packs[name] = self
def __getitem__(self, item: str) -> Any:
return self.__dict__[item]
@classmethod
def get_pack(cls, name: str) -> "TexturePack":
return cls._packs[name.lower()]
@classmethod
def get_next_pack_name(cls, name: str) -> str:
return "squirrel" if name == "ascii" else "ascii"
TexturePack.ASCII_PACK = TexturePack(
name="ascii",
tile_width=1,
tile_fg_color=curses.COLOR_WHITE,
tile_bg_color=curses.COLOR_BLACK,
entity_fg_color=curses.COLOR_WHITE,
entity_bg_color=curses.COLOR_BLACK,
EMPTY=' ',
WALL='#',
FLOOR='.',
PLAYER='@',
HEDGEHOG='*',
HEART='',
BOMB='o',
RABBIT='Y',
BEAVER='_',
TEDDY_BEAR='8',
)
TexturePack.SQUIRREL_PACK = TexturePack(
name="squirrel",
tile_width=2,
tile_fg_color=curses.COLOR_WHITE,
tile_bg_color=curses.COLOR_BLACK,
entity_fg_color=curses.COLOR_WHITE,
entity_bg_color=curses.COLOR_WHITE,
EMPTY=' ',
WALL='🧱',
FLOOR='██',
PLAYER='🐿 ',
HEDGEHOG='🦔',
HEART='💜',
BOMB='💣',
RABBIT='🐇',
BEAVER='🦫',
TEDDY_BEAR='🧸',
)

View File

@ -1,109 +0,0 @@
from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map
class Item(Entity):
"""
A class for items
"""
held: bool
held_by: Optional[Player]
def __init__(self, held: bool = False, held_by: Optional[Player] = None,
*args, **kwargs):
super().__init__(*args, **kwargs)
self.held = held
self.held_by = held_by
def drop(self, y: int, x: int) -> None:
"""
The item is dropped from the inventory onto the floor
"""
if self.held:
self.held_by.inventory.remove(self)
self.held = False
self.held_by = None
self.map.add_entity(self)
self.move(y, x)
def hold(self, player: "Player") -> None:
"""
The item is taken from the floor and put into the inventory
"""
self.held = True
self.held_by = player
self.map.remove_entity(self)
player.inventory.append(self)
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
d["held"] = self.held
return d
class Heart(Item):
"""
A heart item to return health to the player
"""
healing: int
def __init__(self, healing: int = 5, *args, **kwargs):
super().__init__(name="heart", *args, **kwargs)
self.healing = healing
def hold(self, player: "Player") -> None:
"""
When holding a heart, heal the player and don't put item in inventory.
"""
player.health = min(player.maxhealth, player.health + self.healing)
self.map.remove_entity(self)
def save_state(self) -> dict:
"""
Saves the state of the header into a dictionary
"""
d = super().save_state()
d["healing"] = self.healing
return d
class Bomb(Item):
"""
A bomb item intended to deal damage to enemies at long range
"""
damage: int = 5
exploding: bool
def __init__(self, damage: int = 5, exploding: bool = False,
*args, **kwargs):
super().__init__(name="bomb", *args, **kwargs)
self.damage = damage
self.exploding = exploding
def drop(self, x: int, y: int) -> None:
super().drop(x, y)
self.exploding = True
def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
"""
if self.exploding:
for e in m.entities.copy():
if abs(e.x - self.x) + abs(e.y - self.y) <= 1 and \
isinstance(e, FightingEntity):
e.take_damage(self, self.damage)
def save_state(self) -> dict:
"""
Saves the state of the bomb into a dictionary
"""
d = super().save_state()
d["exploding"] = self.exploding
d["damage"] = self.damage
return d

View File

@ -1,92 +0,0 @@
from random import choice
from .player import Player
from ..interfaces import FightingEntity, Map
class Monster(FightingEntity):
"""
The class for all monsters in the dungeon.
A monster must override this class, and the parameters are given
in the __init__ function.
An example of the specification of a monster that has a strength of 4
and 20 max HP:
class MyMonster(Monster):
def __init__(self, strength: int = 4, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="my_monster", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
With that way, attributes can be overwritten when the entity got created.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def act(self, m: Map) -> None:
"""
By default, a monster will move randomly where it is possible
And if a player is close to the monster, the monster run on the player.
"""
target = None
for entity in m.entities:
if self.distance_squared(entity) <= 25 and \
isinstance(entity, Player):
target = entity
break
# A Dijkstra algorithm has ran that targets the player.
# With that way, monsters can simply follow the path.
# If they can't move and they are already close to the player,
# They hit.
if target and (self.y, self.x) in target.paths:
# Move to target player
next_y, next_x = target.paths[(self.y, self.x)]
moved = self.check_move(next_y, next_x, True)
if not moved and self.distance_squared(target) <= 1:
self.hit(target)
else:
for _ in range(100):
if choice([self.move_up, self.move_down,
self.move_left, self.move_right])():
break
class Beaver(Monster):
"""
A beaver monster
"""
def __init__(self, strength: int = 2, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="beaver", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class Hedgehog(Monster):
"""
A really mean hedgehog monster
"""
def __init__(self, strength: int = 3, maxhealth: int = 10,
*args, **kwargs) -> None:
super().__init__(name="hedgehog", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class Rabbit(Monster):
"""
A rabbit monster
"""
def __init__(self, strength: int = 1, maxhealth: int = 15,
*args, **kwargs) -> None:
super().__init__(name="rabbit", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class TeddyBear(Monster):
"""
A cute teddybear monster
"""
def __init__(self, strength: int = 0, maxhealth: int = 50,
*args, **kwargs) -> None:
super().__init__(name="teddy_bear", strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -1,115 +0,0 @@
from random import randint
from typing import Dict, Tuple
from ..interfaces import FightingEntity
class Player(FightingEntity):
"""
The class of the player
"""
current_xp: int = 0
max_xp: int = 10
inventory: list
paths: Dict[Tuple[int, int], Tuple[int, int]]
def __init__(self, maxhealth: int = 20, strength: int = 5,
intelligence: int = 1, charisma: int = 1, dexterity: int = 1,
constitution: int = 1, level: int = 1, current_xp: int = 0,
max_xp: int = 10, *args, **kwargs) -> None:
super().__init__(name="player", maxhealth=maxhealth, strength=strength,
intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution,
level=level, *args, **kwargs)
self.current_xp = current_xp
self.max_xp = max_xp
self.inventory = list()
self.paths = dict()
def move(self, y: int, x: int) -> None:
"""
When the player moves, move the camera of the map.
"""
super().move(y, x)
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
"""
while self.current_xp > self.max_xp:
self.level += 1
self.current_xp -= self.max_xp
self.max_xp = self.level * 10
self.health = self.maxhealth
# TODO Remove it, that's only fun
self.map.spawn_random_entities(randint(3 * self.level,
10 * self.level))
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
"""
self.current_xp += xp
self.level_up()
# noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool:
"""
If the player tries to move but a fighting entity is there,
the player fights this entity.
It rewards some XP if it is dead.
"""
# Don't move if we are dead
if self.dead:
return False
for entity in self.map.entities:
if entity.y == y and entity.x == x:
if entity.is_fighting_entity():
self.hit(entity)
if entity.dead:
self.add_xp(randint(3, 7))
return True
elif entity.is_item():
entity.hold(self)
return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths
for monsters to go to the player.
"""
queue = [(self.y, self.x)]
visited = []
distances = {(self.y, self.x): 0}
predecessors = {}
while queue:
y, x = queue.pop(0)
visited.append((y, x))
if distances[(y, x)] >= max_distance:
continue
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[y][x].can_walk() or \
(new_y, new_x) in visited or \
(new_y, new_x) in queue:
continue
predecessors[(new_y, new_x)] = (y, x)
distances[(new_y, new_x)] = distances[(y, x)] + 1
queue.append((new_y, new_x))
self.paths = predecessors
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
d["current_xp"] = self.current_xp
d["max_xp"] = self.max_xp
return d

View File

@ -1,60 +0,0 @@
from enum import Enum, auto
from typing import Optional
from dungeonbattle.settings import Settings
# This file contains a few useful enumeration classes used elsewhere in the code
class DisplayActions(Enum):
"""
Display actions options for the callable displayaction Game uses
It just calls the same action on the display object displayaction refers to.
"""
REFRESH = auto()
UPDATE = auto()
class GameMode(Enum):
"""
Game mode options
"""
MAINMENU = auto()
PLAY = auto()
SETTINGS = auto()
INVENTORY = auto()
class KeyValues(Enum):
"""
Key values options used in the game
"""
UP = auto()
DOWN = auto()
LEFT = auto()
RIGHT = auto()
ENTER = auto()
SPACE = auto()
@staticmethod
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
"""
Translate the raw string key into an enum value that we can use.
"""
if key in (settings.KEY_DOWN_SECONDARY,
settings.KEY_DOWN_PRIMARY):
return KeyValues.DOWN
elif key in (settings.KEY_LEFT_PRIMARY,
settings.KEY_LEFT_SECONDARY):
return KeyValues.LEFT
elif key in (settings.KEY_RIGHT_PRIMARY,
settings.KEY_RIGHT_SECONDARY):
return KeyValues.RIGHT
elif key in (settings.KEY_UP_PRIMARY,
settings.KEY_UP_SECONDARY):
return KeyValues.UP
elif key == settings.KEY_ENTER:
return KeyValues.ENTER
elif key == ' ':
return KeyValues.SPACE
return None

View File

@ -1,147 +0,0 @@
from random import randint
from typing import Any, Optional
import json
import os
import sys
from .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions
from .interfaces import Map
from .settings import Settings
from . import menus
from typing import Callable
class Game:
"""
The game object controls all actions in the game.
"""
map: Map
player: Player
# display_actions is a display interface set by the bootstrapper
display_actions: Callable[[DisplayActions], None]
def __init__(self) -> None:
"""
Init the game.
"""
self.state = GameMode.MAINMENU
self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings = Settings()
self.settings.load_settings()
self.settings.write_settings()
self.settings_menu.update_values(self.settings)
def new_game(self) -> None:
"""
Create a new game on the screen.
"""
# TODO generate a new map procedurally
self.map = Map.load("resources/example_map_2.txt")
self.player = Player()
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
self.map.spawn_random_entities(randint(3, 10))
def run(self, screen: Any) -> None:
"""
Main infinite loop.
We wait for the player's action, then we do what that should be done
when the given key gets pressed.
"""
while True: # pragma no cover
screen.clear()
screen.refresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None:
"""
Indicates what should be done when the given key is pressed,
according to the current game state.
"""
if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU:
self.handle_key_pressed_main_menu(key)
elif self.state == GameMode.SETTINGS:
self.settings_menu.handle_key_pressed(key, raw_key, self)
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
"""
In play mode, arrows or zqsd move the main character.
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
"""
In the main menu, we can navigate through options.
"""
if key == KeyValues.DOWN:
self.main_menu.go_down()
if key == KeyValues.UP:
self.main_menu.go_up()
if key == KeyValues.ENTER:
option = self.main_menu.validate()
if option == menus.MainMenuValues.START:
self.new_game()
self.display_actions(DisplayActions.UPDATE)
self.state = GameMode.PLAY
if option == menus.MainMenuValues.RESUME:
self.state = GameMode.PLAY
elif option == menus.MainMenuValues.SAVE:
self.save_game()
elif option == menus.MainMenuValues.LOAD:
self.load_game()
elif option == menus.MainMenuValues.SETTINGS:
self.state = GameMode.SETTINGS
elif option == menus.MainMenuValues.EXIT:
sys.exit(0)
def save_state(self) -> dict:
"""
Saves the game to a dictionary
"""
return self.map.save_state()
def load_state(self, d: dict) -> None:
"""
Loads the game from a dictionary
"""
self.map.load_state(d)
# noinspection PyTypeChecker
self.player = self.map.find_entities(Player)[0]
self.display_actions(DisplayActions.UPDATE)
def load_game(self) -> None:
"""
Loads the game from a file
"""
if os.path.isfile("save.json"):
with open("save.json", "r") as f:
self.load_state(json.loads(f.read()))
def save_game(self) -> None:
"""
Saves the game to a file
"""
with open("save.json", "w") as f:
f.write(json.dumps(self.save_state()))

View File

@ -1,399 +0,0 @@
#!/usr/bin/env python
from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List, Optional
from dungeonbattle.display.texturepack import TexturePack
class Map:
"""
Object that represents a Map with its width, height
and tiles, that have their custom properties.
"""
width: int
height: int
start_y: int
start_x: int
tiles: List[List["Tile"]]
entities: List["Entity"]
# coordinates of the point that should be
# on the topleft corner of the screen
currentx: int
currenty: int
def __init__(self, width: int, height: int, tiles: list,
start_y: int, start_x: int):
self.width = width
self.height = height
self.start_y = start_y
self.start_x = start_x
self.tiles = tiles
self.entities = []
def add_entity(self, entity: "Entity") -> None:
"""
Register a new entity in the map.
"""
self.entities.append(entity)
entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
"""
self.entities.remove(entity)
def find_entities(self, entity_class: type) -> list:
return [entity for entity in self.entities
if isinstance(entity, entity_class)]
def is_free(self, y: int, x: int) -> bool:
"""
Indicates that the case at the coordinates (y, x) is empty.
"""
return 0 <= y < self.height and 0 <= x < self.width and \
self.tiles[y][x].can_walk() and \
not any(entity.x == x and entity.y == y for entity in self.entities)
@staticmethod
def load(filename: str) -> "Map":
"""
Read a file that contains the content of a map, and build a Map object.
"""
with open(filename, "r") as f:
file = f.read()
return Map.load_from_string(file)
@staticmethod
def load_from_string(content: str) -> "Map":
"""
Load a map represented by its characters and build a Map object.
"""
lines = content.split("\n")
first_line = lines[0]
start_y, start_x = map(int, first_line.split(" "))
lines = [line for line in lines[1:] if line]
height = len(lines)
width = len(lines[0])
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return Map(width, height, tiles, start_y, start_x)
@staticmethod
def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
"""
Transforms a string into the list of corresponding tiles
"""
lines = content.split("\n")
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return tiles
def draw_string(self, pack: TexturePack) -> str:
"""
Draw the current map as a string object that can be rendered
in the window.
"""
return "\n".join("".join(tile.char(pack) for tile in line)
for line in self.tiles)
def spawn_random_entities(self, count: int) -> None:
"""
Put randomly {count} hedgehogs on the map, where it is available.
"""
for _ in range(count):
y, x = 0, 0
while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
tile = self.tiles[y][x]
if tile.can_walk():
break
entity = choice(Entity.get_all_entity_classes())()
entity.move(y, x)
self.add_entity(entity)
def tick(self) -> None:
"""
Trigger all entity events.
"""
for entity in self.entities:
entity.act(self)
def save_state(self) -> dict:
"""
Saves the map's attributes to a dictionary
"""
d = dict()
d["width"] = self.width
d["height"] = self.height
d["start_y"] = self.start_y
d["start_x"] = self.start_x
d["currentx"] = self.currentx
d["currenty"] = self.currenty
d["entities"] = []
for enti in self.entities:
d["entities"].append(enti.save_state())
d["map"] = self.draw_string(TexturePack.ASCII_PACK)
return d
def load_state(self, d: dict) -> None:
"""
Loads the map's attributes from a dictionary
"""
self.width = d["width"]
self.height = d["height"]
self.start_y = d["start_y"]
self.start_x = d["start_x"]
self.currentx = d["currentx"]
self.currenty = d["currenty"]
self.tiles = self.load_dungeon_from_string(d["map"])
self.entities = []
dictclasses = Entity.get_all_entity_classes_in_a_dict()
for entisave in d["entities"]:
self.add_entity(dictclasses[entisave["type"]](**entisave))
class Tile(Enum):
"""
The internal representation of the tiles of the map
"""
EMPTY = auto()
WALL = auto()
FLOOR = auto()
@staticmethod
def from_ascii_char(ch: str) -> "Tile":
"""
Maps an ascii character to its equivalent in the texture pack
"""
for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch:
return tile
raise ValueError(ch)
def char(self, pack: TexturePack) -> str:
"""
Translates a Tile to the corresponding character according
to the texture pack
"""
return getattr(pack, self.name)
def is_wall(self) -> bool:
"""
Is this Tile a wall?
"""
return self == Tile.WALL
def can_walk(self) -> bool:
"""
Check if an entity (player or not) can move in this tile.
"""
return not self.is_wall() and self != Tile.EMPTY
class Entity:
"""
An Entity object represents any entity present on the map
"""
y: int
x: int
name: str
map: Map
# noinspection PyShadowingBuiltins
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
map: Optional[Map] = None, *ignored, **ignored2):
self.y = y
self.x = x
self.name = name
self.map = map
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool:
"""
Checks if moving to (y,x) is authorized
"""
free = self.map.is_free(y, x)
if free and move_if_possible:
self.move(y, x)
return free
def move(self, y: int, x: int) -> bool:
"""
Moves an entity to (y,x) coordinates
"""
self.y = y
self.x = x
return True
def move_up(self, force: bool = False) -> bool:
"""
Moves the entity up one tile, if possible
"""
return self.move(self.y - 1, self.x) if force else \
self.check_move(self.y - 1, self.x, True)
def move_down(self, force: bool = False) -> bool:
"""
Moves the entity down one tile, if possible
"""
return self.move(self.y + 1, self.x) if force else \
self.check_move(self.y + 1, self.x, True)
def move_left(self, force: bool = False) -> bool:
"""
Moves the entity left one tile, if possible
"""
return self.move(self.y, self.x - 1) if force else \
self.check_move(self.y, self.x - 1, True)
def move_right(self, force: bool = False) -> bool:
"""
Moves the entity right one tile, if possible
"""
return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True)
def act(self, m: Map) -> None:
"""
Define the action of the entity that is ran each tick.
By default, does nothing.
"""
pass
def distance_squared(self, other: "Entity") -> int:
"""
Get the square of the distance to another entity.
Useful to check distances since square root takes time.
"""
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
def distance(self, other: "Entity") -> float:
"""
Get the cartesian distance to another entity.
"""
return sqrt(self.distance_squared(other))
def is_fighting_entity(self) -> bool:
"""
Is this entity a fighting entity?
"""
return isinstance(self, FightingEntity)
def is_item(self) -> bool:
"""
Is this entity an item?
"""
from dungeonbattle.entities.items import Item
return isinstance(self, Item)
@staticmethod
def get_all_entity_classes():
"""
Returns all entities subclasses
"""
from dungeonbattle.entities.items import Heart, Bomb
from dungeonbattle.entities.monsters import Beaver, Hedgehog, \
Rabbit, TeddyBear
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear]
@staticmethod
def get_all_entity_classes_in_a_dict() -> dict:
"""
Returns all entities subclasses in a dictionary
"""
from dungeonbattle.entities.player import Player
from dungeonbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \
TeddyBear
from dungeonbattle.entities.items import Bomb, Heart
return {
"Beaver": Beaver,
"Bomb": Bomb,
"Heart": Heart,
"Hedgehog": Hedgehog,
"Rabbit": Rabbit,
"TeddyBear": TeddyBear,
"Player": Player,
}
def save_state(self) -> dict:
"""
Saves the coordinates of the entity
"""
d = dict()
d["x"] = self.x
d["y"] = self.y
d["type"] = self.__class__.__name__
return d
class FightingEntity(Entity):
"""
A FightingEntity is an entity that can fight, and thus has a health,
level and stats
"""
maxhealth: int
health: int
strength: int
intelligence: int
charisma: int
dexterity: int
constitution: int
level: int
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
strength: int = 0, intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0, level: int = 0,
*args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.maxhealth = maxhealth
self.health = maxhealth if health is None else health
self.strength = strength
self.intelligence = intelligence
self.charisma = charisma
self.dexterity = dexterity
self.constitution = constitution
self.level = level
@property
def dead(self) -> bool:
return self.health <= 0
def hit(self, opponent: "FightingEntity") -> None:
"""
Deals damage to the opponent, based on the stats
"""
opponent.take_damage(self, self.strength)
def take_damage(self, attacker: "Entity", amount: int) -> None:
"""
Take damage from the attacker, based on the stats
"""
self.health -= amount
if self.health <= 0:
self.die()
def die(self) -> None:
"""
If a fighting entity has no more health, it dies and is removed
"""
self.map.remove_entity(self)
def keys(self) -> list:
"""
Returns a fighting entities specific attributes
"""
return ["maxhealth", "health", "level", "strength",
"intelligence", "charisma", "dexterity", "constitution"]
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
for name in self.keys():
d[name] = getattr(self, name)
return d

View File

@ -1,107 +0,0 @@
from enum import Enum
from typing import Any, Optional
from .display.texturepack import TexturePack
from .enums import GameMode, KeyValues, DisplayActions
from .settings import Settings
class Menu:
"""
A Menu object is the logical representation of a menu in the game
"""
values: list
def __init__(self):
self.position = 0
def go_up(self) -> None:
"""
Moves the pointer of the menu on the previous value
"""
self.position = max(0, self.position - 1)
def go_down(self) -> None:
"""
Moves the pointer of the menu on the next value
"""
self.position = min(len(self.values) - 1, self.position + 1)
def validate(self) -> Any:
"""
Selects the value that is pointed by the menu pointer
"""
return self.values[self.position]
class MainMenuValues(Enum):
"""
Values of the main menu
"""
START = 'Nouvelle partie'
RESUME = 'Continuer'
SAVE = 'Sauvegarder'
LOAD = 'Charger'
SETTINGS = 'Paramètres'
EXIT = 'Quitter'
def __str__(self):
return self.value
class MainMenu(Menu):
"""
A special instance of a menu : the main menu
"""
values = [e for e in MainMenuValues]
class SettingsMenu(Menu):
"""
A special instance of a menu : the settings menu
"""
waiting_for_key: bool = False
def update_values(self, settings: Settings) -> None:
self.values = list(settings.__dict__.items())
self.values.append(("RETURN", ["", "Retour"]))
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None:
"""
In the setting menu, we van select a setting and change it
"""
if not self.waiting_for_key:
# Navigate normally through the menu.
if key == KeyValues.SPACE or \
key == KeyValues.ENTER and \
self.position == len(self.values) - 1:
# Go back
game.display_actions(DisplayActions.UPDATE)
game.state = GameMode.MAINMENU
if key == KeyValues.DOWN:
self.go_down()
if key == KeyValues.UP:
self.go_up()
if key == KeyValues.ENTER and self.position < len(self.values) - 1:
# Change a setting
option = self.values[self.position][0]
if option == "TEXTURE_PACK":
game.settings.TEXTURE_PACK = \
TexturePack.get_next_pack_name(
game.settings.TEXTURE_PACK)
game.settings.write_settings()
self.update_values(game.settings)
else:
self.waiting_for_key = True
self.update_values(game.settings)
else:
option = self.values[self.position][0]
# Don't use an already mapped key
if any(getattr(game.settings, opt) == raw_key
for opt in game.settings.settings_keys if opt != option):
return
setattr(game.settings, option, raw_key)
game.settings.write_settings()
self.waiting_for_key = False
self.update_values(game.settings)

View File

@ -1,93 +0,0 @@
import json
import os
from typing import Any, Generator
class Settings:
"""
This class stores the settings of the game.
Settings can be get by using for example settings.TEXTURE_PACK directly.
The comment can be get by using settings.get_comment('TEXTURE_PACK').
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
"""
def __init__(self):
self.KEY_UP_PRIMARY = \
['z', 'Touche principale pour aller vers le haut']
self.KEY_UP_SECONDARY = \
['KEY_UP', 'Touche secondaire pour aller vers le haut']
self.KEY_DOWN_PRIMARY = \
['s', 'Touche principale pour aller vers le bas']
self.KEY_DOWN_SECONDARY = \
['KEY_DOWN', 'Touche secondaire pour aller vers le bas']
self.KEY_LEFT_PRIMARY = \
['q', 'Touche principale pour aller vers la gauche']
self.KEY_LEFT_SECONDARY = \
['KEY_LEFT', 'Touche secondaire pour aller vers la gauche']
self.KEY_RIGHT_PRIMARY = \
['d', 'Touche principale pour aller vers la droite']
self.KEY_RIGHT_SECONDARY = \
['KEY_RIGHT', 'Touche secondaire pour aller vers la droite']
self.KEY_ENTER = \
['\n', 'Touche pour valider un menu']
self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé']
def __getattribute__(self, item: str) -> Any:
superattribute = super().__getattribute__(item)
if item.isupper() and item in self.settings_keys:
return superattribute[0]
return superattribute
def __setattr__(self, name: str, value: Any) -> None:
if name in self.settings_keys:
object.__getattribute__(self, name)[0] = value
return
return super().__setattr__(name, value)
def get_comment(self, item: str) -> str:
"""
Retrieve the comment of a setting.
"""
if item in self.settings_keys:
return object.__getattribute__(self, item)[1]
for key in self.settings_keys:
if getattr(self, key) == item:
return object.__getattribute__(self, key)[1]
@property
def settings_keys(self) -> Generator[str, Any, None]:
"""
Get the list of all parameters.
"""
return (key for key in self.__dict__)
def loads_from_string(self, json_str: str) -> None:
"""
Dump settings
"""
d = json.loads(json_str)
for key in d:
setattr(self, key, d[key])
def dumps_to_string(self) -> str:
"""
Dump settings
"""
d = dict()
for key in self.settings_keys:
d[key] = getattr(self, key)
return json.dumps(d, indent=4)
def load_settings(self) -> None:
"""
Loads the settings from a file
"""
if os.path.isfile("settings.json"):
with open("settings.json", "r") as f:
self.loads_from_string(f.read())
def write_settings(self) -> None:
"""
Dumps the settings into a file
"""
with open("settings.json", "w") as f:
f.write(self.dumps_to_string())

View File

@ -1,33 +0,0 @@
import curses
from types import TracebackType
class TermManager: # pragma: no cover
"""
The TermManager object initializes the terminal, returns a screen object and
de-initializes the terminal after use
"""
def __init__(self):
self.screen = curses.initscr()
# convert escapes sequences to curses abstraction
self.screen.keypad(True)
# stop printing typed keys to the terminal
curses.noecho()
# send keys through without having to press <enter>
curses.cbreak()
# make cursor invisible
curses.curs_set(False)
# Enable colors
curses.start_color()
def __enter__(self):
return self
def __exit__(self, exc_type: type, exc_value: Exception,
exc_traceback: TracebackType) -> None:
# restore the terminal to its original state
self.screen.keypad(False)
curses.echo()
curses.nocbreak()
curses.curs_set(True)
curses.endwin()

View File

@ -1,174 +0,0 @@
import unittest
from dungeonbattle.entities.items import Bomb, Heart, Item
from dungeonbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear
from dungeonbattle.entities.player import Player
from dungeonbattle.interfaces import Entity, Map
class TestEntities(unittest.TestCase):
def setUp(self) -> None:
"""
Load example map that can be used in tests.
"""
self.map = Map.load("resources/example_map.txt")
self.player = Player()
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
def test_basic_entities(self) -> None:
"""
Test some random stuff with basic entities.
"""
entity = Entity()
entity.move(42, 64)
self.assertEqual(entity.y, 42)
self.assertEqual(entity.x, 64)
self.assertIsNone(entity.act(self.map))
other_entity = Entity()
other_entity.move(45, 68)
self.assertEqual(entity.distance_squared(other_entity), 25)
self.assertEqual(entity.distance(other_entity), 5)
def test_fighting_entities(self) -> None:
"""
Test some random stuff with fighting entities.
"""
entity = Beaver()
self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 20)
self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 2)
for _ in range(9):
self.assertIsNone(entity.hit(entity))
self.assertFalse(entity.dead)
self.assertIsNone(entity.hit(entity))
self.assertTrue(entity.dead)
entity = Rabbit()
self.map.add_entity(entity)
entity.move(15, 44)
# Move randomly
self.map.tick()
self.assertFalse(entity.y == 15 and entity.x == 44)
# Move to the player
entity.move(3, 6)
self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6)
# Rabbit should fight
old_health = self.player.health
self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health)
# Fight the rabbit
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertFalse(entity.dead)
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertFalse(entity.dead)
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertTrue(entity.dead)
self.assertGreaterEqual(self.player.current_xp, 3)
def test_items(self) -> None:
"""
Test some random stuff with items.
"""
item = Item()
self.map.add_entity(item)
self.assertFalse(item.held)
item.hold(self.player)
self.assertTrue(item.held)
item.drop(2, 6)
self.assertEqual(item.y, 2)
self.assertEqual(item.x, 6)
# Pick up item
self.player.move_down()
self.assertTrue(item.held)
self.assertEqual(item.held_by, self.player)
self.assertIn(item, self.player.inventory)
self.assertNotIn(item, self.map.entities)
def test_bombs(self) -> None:
"""
Test some random stuff with bombs.
"""
item = Bomb()
hedgehog = Hedgehog()
teddy_bear = TeddyBear()
self.map.add_entity(item)
self.map.add_entity(hedgehog)
self.map.add_entity(teddy_bear)
hedgehog.health = 2
teddy_bear.health = 2
hedgehog.move(41, 42)
teddy_bear.move(42, 41)
item.act(self.map)
self.assertFalse(hedgehog.dead)
self.assertFalse(teddy_bear.dead)
item.drop(42, 42)
self.assertEqual(item.y, 42)
self.assertEqual(item.x, 42)
item.act(self.map)
self.assertTrue(hedgehog.dead)
self.assertTrue(teddy_bear.dead)
bomb_state = item.save_state()
self.assertEqual(bomb_state["damage"], item.damage)
def test_hearts(self) -> None:
"""
Test some random stuff with hearts.
"""
item = Heart()
self.map.add_entity(item)
item.move(2, 6)
self.player.health -= 2 * item.healing
self.player.move_down()
self.assertNotIn(item, self.map.entities)
self.assertEqual(self.player.health,
self.player.maxhealth - item.healing)
heart_state = item.save_state()
self.assertEqual(heart_state["healing"], item.healing)
def test_players(self) -> None:
"""
Test some random stuff with players.
"""
player = Player()
self.map.add_entity(player)
player.move(1, 6)
self.assertEqual(player.strength, 5)
self.assertEqual(player.health, player.maxhealth)
self.assertEqual(player.maxhealth, 20)
# Test movements and ensure that collisions are working
self.assertFalse(player.move_up())
self.assertTrue(player.move_left())
self.assertFalse(player.move_left())
for i in range(8):
self.assertTrue(player.move_down())
self.assertFalse(player.move_down())
self.assertTrue(player.move_right())
self.assertTrue(player.move_right())
self.assertTrue(player.move_right())
self.assertFalse(player.move_right())
self.assertTrue(player.move_down())
self.assertTrue(player.move_down())
player.add_xp(70)
self.assertEqual(player.current_xp, 10)
self.assertEqual(player.max_xp, 40)
self.assertEqual(player.level, 4)
player_state = player.save_state()
self.assertEqual(player_state["current_xp"], 10)

View File

@ -1,279 +0,0 @@
import os
import unittest
from dungeonbattle.bootstrap import Bootstrap
from dungeonbattle.display.display import Display
from dungeonbattle.display.display_manager import DisplayManager
from dungeonbattle.entities.player import Player
from dungeonbattle.game import Game, KeyValues, GameMode
from dungeonbattle.menus import MainMenuValues
from dungeonbattle.settings import Settings
class TestGame(unittest.TestCase):
def setUp(self) -> None:
"""
Setup game.
"""
self.game = Game()
self.game.new_game()
display = DisplayManager(None, self.game)
self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None:
"""
Save a game and reload it.
"""
old_state = self.game.save_state()
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.ENTER) # Save game
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.ENTER) # Load game
new_state = self.game.save_state()
self.assertEqual(old_state, new_state)
def test_bootstrap_fail(self) -> None:
"""
Ensure that the test can't play the game,
because there is no associated shell.
Yeah, that's only for coverage.
"""
self.assertRaises(Exception, Bootstrap.run_game)
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
def test_key_translation(self) -> None:
"""
Test key bindings.
"""
self.game.settings = Settings()
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_PRIMARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_SECONDARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_PRIMARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_SECONDARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_PRIMARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_SECONDARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_PRIMARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_ENTER, self.game.settings),
KeyValues.ENTER)
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
KeyValues.SPACE)
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
None)
def test_key_press(self) -> None:
"""
Press a key and see what is done.
"""
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.RESUME)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.EXIT)
self.assertRaises(SystemExit, self.game.handle_key_pressed,
KeyValues.ENTER)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.RESUME)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.PLAY)
# Kill entities
for entity in self.game.map.entities.copy():
if not isinstance(entity, Player):
self.game.map.remove_entity(entity)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.DOWN)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y + 1)
self.assertEqual(new_x, x)
y, x = new_y, new_x
self.game.handle_key_pressed(KeyValues.RIGHT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x + 1)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.UP)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y - 1)
self.assertEqual(new_x, x)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.LEFT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x - 1)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_new_game(self) -> None:
"""
Ensure that the start button starts a new game.
"""
old_map = self.game.map
old_player = self.game.player
self.game.handle_key_pressed(KeyValues.ENTER) # Start new game
new_map = self.game.map
new_player = self.game.player
# Ensure that
self.assertNotEqual(old_map, new_map)
self.assertNotEqual(old_player, new_player)
self.game.handle_key_pressed(KeyValues.SPACE)
old_map = new_map
old_player = new_player
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER) # Resume game
new_map = self.game.map
new_player = self.game.player
self.assertEqual(old_map, new_map)
self.assertEqual(old_player, new_player)
def test_settings_menu(self) -> None:
"""
Ensure that the settings menu is working properly.
"""
self.game.settings = Settings()
# Open settings menu
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
# Define the "move up" key to 'w'
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'w')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
# Navigate to "move left"
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.UP)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
# Define the "move up" key to 'a'
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
# Can't used a mapped key
self.game.handle_key_pressed(None, 's')
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'a')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack"
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
# Change texture pack
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "squirrel")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
# Navigate to "back" button
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_dead_screen(self) -> None:
"""
Kill player and render dead screen.
"""
self.game.state = GameMode.PLAY
# Kill player
self.game.player.take_damage(self.game.player,
self.game.player.health + 2)
y, x = self.game.player.y, self.game.player.x
for key in [KeyValues.UP, KeyValues.DOWN,
KeyValues.LEFT, KeyValues.RIGHT]:
self.game.handle_key_pressed(key)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x)
def test_not_implemented(self) -> None:
"""
Check that some functions are not implemented, only for coverage.
"""
self.assertRaises(NotImplementedError, Display.display, None)

View File

@ -1,35 +0,0 @@
import unittest
from dungeonbattle.display.texturepack import TexturePack
from dungeonbattle.interfaces import Map, Tile
class TestInterfaces(unittest.TestCase):
def test_map(self) -> None:
"""
Create a map and check that it is well parsed.
"""
m = Map.load_from_string("0 0\n.#\n#.\n")
self.assertEqual(m.width, 2)
self.assertEqual(m.height, 2)
self.assertEqual(m.draw_string(TexturePack.ASCII_PACK), ".#\n#.")
def test_load_map(self) -> None:
"""
Try to load a map from a file.
"""
m = Map.load("resources/example_map.txt")
self.assertEqual(m.width, 52)
self.assertEqual(m.height, 17)
def test_tiles(self) -> None:
"""
Test some things about tiles.
"""
self.assertFalse(Tile.FLOOR.is_wall())
self.assertTrue(Tile.WALL.is_wall())
self.assertFalse(Tile.EMPTY.is_wall())
self.assertTrue(Tile.FLOOR.can_walk())
self.assertFalse(Tile.WALL.can_walk())
self.assertFalse(Tile.EMPTY.can_walk())
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')

View File

@ -1,17 +0,0 @@
class FakePad:
"""
In order to run tests, we simulate a fake curses pad that accepts functions
but does nothing with them.
"""
def addstr(self, y: int, x: int, message: str, color: int = 0) -> None:
pass
def refresh(self, pminrow: int, pmincol: int, sminrow: int,
smincol: int, smaxrow: int, smaxcol: int) -> None:
pass
def clear(self) -> None:
pass
def resize(self, height: int, width: int) -> None:
pass

View File

@ -1,32 +0,0 @@
import unittest
from dungeonbattle.settings import Settings
class TestSettings(unittest.TestCase):
def test_settings(self) -> None:
"""
Ensure that settings are well loaded.
"""
settings = Settings()
self.assertEqual(settings.KEY_UP_PRIMARY, 'z')
self.assertEqual(settings.KEY_DOWN_PRIMARY, 's')
self.assertEqual(settings.KEY_LEFT_PRIMARY, 'q')
self.assertEqual(settings.KEY_RIGHT_PRIMARY, 'd')
self.assertEqual(settings.KEY_UP_SECONDARY, 'KEY_UP')
self.assertEqual(settings.KEY_DOWN_SECONDARY, 'KEY_DOWN')
self.assertEqual(settings.KEY_LEFT_SECONDARY, 'KEY_LEFT')
self.assertEqual(settings.KEY_RIGHT_SECONDARY, 'KEY_RIGHT')
self.assertEqual(settings.TEXTURE_PACK, 'ascii')
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
settings.get_comment('TEXTURE_PACK'))
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
'Pack de textures utilisé')
settings.TEXTURE_PACK = 'squirrel'
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')
settings.write_settings()
settings.load_settings()
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')