Merge branch 'entities' into 'master'

Entities

See merge request ynerant/dungeon-battle!10
This commit is contained in:
ynerant 2020-11-13 14:10:40 +01:00
commit eca6b9af1f
24 changed files with 763 additions and 170 deletions

View File

@ -2,12 +2,19 @@ stages:
- test - test
- quality-assurance - quality-assurance
py37:
stage: test
image: python:3.7-alpine
before_script:
- pip install tox
script: tox -e py3
py38: py38:
stage: test stage: test
image: python:3.8-alpine image: python:3.8-alpine
before_script: before_script:
- pip install tox - pip install tox
script: tox -e py38 script: tox -e py3
py39: py39:
@ -15,7 +22,7 @@ py39:
image: python:3.9-alpine image: python:3.9-alpine
before_script: before_script:
- pip install tox - pip install tox
script: tox -e py39 script: tox -e py3
linters: linters:
stage: quality-assurance stage: quality-assurance

View File

@ -11,5 +11,5 @@ class Bootstrap:
game = Game() game = Game()
game.new_game() game.new_game()
display = DisplayManager(term_manager.screen, game) display = DisplayManager(term_manager.screen, game)
game.display_refresh = display.refresh game.display_actions = display.handle_display_action
game.run(term_manager.screen) game.run(term_manager.screen)

View File

@ -19,17 +19,25 @@ class Display:
def newpad(self, height: int, width: int) -> Union[FakePad, Any]: def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
return curses.newpad(height, width) if self.screen else FakePad() return curses.newpad(height, width) if self.screen else FakePad()
def resize(self, y: int, x: int, height: int, width: int) -> None: 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.x = x
self.y = y self.y = y
self.width = width self.width = width
self.height = height self.height = height
if self.pad: if hasattr(self, "pad") and resize_pad:
self.pad.resize(height - 1, width - 1) self.pad.resize(self.height - 1, self.width - 1)
def refresh(self, *args) -> None: def refresh(self, *args, resize_pad: bool = True) -> None:
if len(args) == 4: if len(args) == 4:
self.resize(*args) self.resize(*args, resize_pad)
self.display() self.display()
def display(self) -> None: def display(self) -> None:

View File

@ -1,10 +1,11 @@
import curses import curses
from dungeonbattle.display.mapdisplay import MapDisplay from dungeonbattle.display.mapdisplay import MapDisplay
from dungeonbattle.display.statsdisplay import StatsDisplay from dungeonbattle.display.statsdisplay import StatsDisplay
from dungeonbattle.display.menudisplay import MainMenuDisplay from dungeonbattle.display.menudisplay import MenuDisplay, MainMenuDisplay
from dungeonbattle.display.texturepack import TexturePack from dungeonbattle.display.texturepack import TexturePack
from typing import Any from typing import Any
from dungeonbattle.game import Game, GameMode from dungeonbattle.game import Game, GameMode
from dungeonbattle.enums import DisplayActions
class DisplayManager: class DisplayManager:
@ -17,8 +18,15 @@ class DisplayManager:
self.statsdisplay = StatsDisplay(screen, pack) self.statsdisplay = StatsDisplay(screen, pack)
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu, self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack) screen, pack)
self.settingsmenudisplay = MenuDisplay(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay, self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay] 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() self.update_game_components()
def update_game_components(self) -> None: def update_game_components(self) -> None:
@ -26,14 +34,19 @@ class DisplayManager:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK) d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map) self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player) self.statsdisplay.update_player(self.game.player)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
def refresh(self) -> None: def refresh(self) -> None:
if self.game.state == GameMode.PLAY: if self.game.state == GameMode.PLAY:
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols) # 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.statsdisplay.refresh(self.rows * 4 // 5, 0,
self.rows // 5, self.cols) self.rows // 5, self.cols)
if self.game.state == GameMode.MAINMENU: if self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) 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() self.resize_window()
def resize_window(self) -> bool: def resize_window(self) -> bool:

View File

@ -12,25 +12,30 @@ class MapDisplay(Display):
def update_map(self, m: Map) -> None: def update_map(self, m: Map) -> None:
self.map = m self.map = m
self.pad = self.newpad(m.height, m.width + 1) self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update_pad(self) -> None: def update_pad(self) -> None:
self.pad.addstr(0, 0, self.map.draw_string(self.pack)) 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: for e in self.map.entities:
self.pad.addstr(e.y, e.x, self.pack.PLAYER) self.pad.addstr(e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
def display(self) -> None: def display(self) -> None:
y, x = self.map.currenty, self.map.currentx y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1 deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
pminrow, pmincol = y - deltay, x - deltax pminrow, pmincol = y - deltay, x - deltax
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0) sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
deltay, deltax = self.height - deltay, self.width - deltax deltay, deltax = self.height - deltay, self.width - deltax
smaxrow = self.map.height - (y + deltay) + self.height - 1 smaxrow = self.map.height - (y + deltay) + self.height - 1
smaxrow = min(smaxrow, self.height - 1) smaxrow = min(smaxrow, self.height - 1)
smaxcol = self.map.width - (x + deltax) + self.width - 1 smaxcol = self.pack.tile_width * self.map.width - \
(x + deltax) + self.width - 1
smaxcol = min(smaxcol, self.width - 1) smaxcol = min(smaxcol, self.width - 1)
pminrow = max(0, min(self.map.height, pminrow)) pminrow = max(0, min(self.map.height, pminrow))
pmincol = max(0, min(self.map.width, pmincol)) pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
self.pad.clear() self.pad.clear()
self.update_pad() self.update_pad()
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)

View File

@ -1,3 +1,5 @@
from typing import List
from dungeonbattle.menus import Menu, MainMenu from dungeonbattle.menus import Menu, MainMenu
from .display import Display from .display import Display
@ -11,7 +13,6 @@ class MenuDisplay(Display):
def update_menu(self, menu: Menu) -> None: def update_menu(self, menu: Menu) -> None:
self.menu = menu self.menu = menu
self.values = [str(a) for a in menu.values]
self.trueheight = len(self.values) self.trueheight = len(self.values)
self.truewidth = max([len(a) for a in self.values]) self.truewidth = max([len(a) for a in self.values])
@ -22,7 +23,7 @@ class MenuDisplay(Display):
def update_pad(self) -> None: def update_pad(self) -> None:
for i in range(self.trueheight): for i in range(self.trueheight):
self.pad.addstr(i, 0, " ") self.pad.addstr(i, 0, " " + self.values[i])
# set a marker on the selected line # set a marker on the selected line
self.pad.addstr(self.menu.position, 0, ">") self.pad.addstr(self.menu.position, 0, ">")
@ -54,6 +55,10 @@ class MenuDisplay(Display):
def preferred_height(self) -> int: def preferred_height(self) -> int:
return self.trueheight + 2 return self.trueheight + 2
@property
def values(self) -> List[str]:
return [str(a) for a in self.menu.values]
class MainMenuDisplay(Display): class MainMenuDisplay(Display):
def __init__(self, menu: MainMenu, *args): def __init__(self, menu: MainMenu, *args):

View File

@ -1,3 +1,5 @@
import curses
from .display import Display from .display import Display
from dungeonbattle.entities.player import Player from dungeonbattle.entities.player import Player
@ -9,6 +11,7 @@ class StatsDisplay(Display):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols) 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: def update_player(self, p: Player) -> None:
self.player = p self.player = p
@ -33,9 +36,17 @@ class StatsDisplay(Display):
string3 = string3 + " " string3 = string3 + " "
self.pad.addstr(2, 0, 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: def display(self) -> None:
self.pad.clear() self.pad.clear()
self.update_pad() self.update_pad()
self.pad.refresh(0, 0, self.y, self.x, self.pad.refresh(0, 0, self.y, self.x,
2 + self.y, 4 + self.y, self.width + self.x)
self.width + self.x)

View File

@ -1,7 +1,16 @@
import curses
from typing import Any
class TexturePack: class TexturePack:
_packs = dict() _packs = dict()
name: str name: str
tile_width: int
tile_fg_color: int
tile_bg_color: int
entity_fg_color: int
entity_bg_color: int
EMPTY: str EMPTY: str
WALL: str WALL: str
FLOOR: str FLOOR: str
@ -15,23 +24,52 @@ class TexturePack:
self.__dict__.update(**kwargs) self.__dict__.update(**kwargs)
TexturePack._packs[name] = self TexturePack._packs[name] = self
def __getitem__(self, item: str) -> Any:
return self.__dict__[item]
@classmethod @classmethod
def get_pack(cls, name: str) -> "TexturePack": def get_pack(cls, name: str) -> "TexturePack":
return cls._packs[name.lower()] 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( TexturePack.ASCII_PACK = TexturePack(
name="ascii", 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=' ', EMPTY=' ',
WALL='#', WALL='#',
FLOOR='.', FLOOR='.',
PLAYER='@', PLAYER='@',
HEDGEHOG='*',
HEART='',
BOMB='o',
RABBIT='Y',
BEAVER='_',
TEDDY_BEAR='8',
) )
TexturePack.SQUIRREL_PACK = TexturePack( TexturePack.SQUIRREL_PACK = TexturePack(
name="squirrel", 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=' ', EMPTY=' ',
WALL='', WALL='🧱',
FLOOR='.', FLOOR='██',
PLAYER='🐿 ', PLAYER='🐿 ',
HEDGEHOG='🦔',
HEART='💜',
BOMB='💣',
RABBIT='🐇',
BEAVER='🦫',
TEDDY_BEAR='🧸',
) )

View File

@ -1,22 +1,46 @@
from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map from ..interfaces import Entity, FightingEntity, Map
class Item(Entity): class Item(Entity):
held: bool held: bool
held_by: Optional["Player"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.held = False self.held = False
def drop(self, y: int, x: int) -> None: def drop(self, y: int, x: int) -> None:
if self.held:
self.held_by.inventory.remove(self)
self.held = False self.held = False
self.held_by = None
self.map.add_entity(self)
self.move(y, x) self.move(y, x)
def hold(self) -> None: def hold(self, player: "Player") -> None:
self.held = True self.held = True
self.held_by = player
self.map.remove_entity(self)
player.inventory.append(self)
class Heart(Item):
name: str = "heart"
healing: int = 5
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)
class Bomb(Item): class Bomb(Item):
name: str = "bomb"
damage: int = 5 damage: int = 5
exploding: bool exploding: bool

View File

@ -1,11 +1,58 @@
from random import choice
from .player import Player
from ..interfaces import FightingEntity, Map from ..interfaces import FightingEntity, Map
class Monster(FightingEntity): class Monster(FightingEntity):
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
pass """
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 Squirrel(Monster): class Beaver(Monster):
name = "beaver"
maxhealth = 30
strength = 2
class Hedgehog(Monster):
name = "hedgehog"
maxhealth = 10 maxhealth = 10
strength = 3 strength = 3
class Rabbit(Monster):
name = "rabbit"
maxhealth = 15
strength = 1
class TeddyBear(Monster):
name = "teddy_bear"
maxhealth = 50
strength = 0

View File

@ -1,7 +1,11 @@
from random import randint
from typing import Dict, Tuple
from ..interfaces import FightingEntity from ..interfaces import FightingEntity
class Player(FightingEntity): class Player(FightingEntity):
name = "player"
maxhealth: int = 20 maxhealth: int = 20
strength: int = 5 strength: int = 5
intelligence: int = 1 intelligence: int = 1
@ -11,25 +15,88 @@ class Player(FightingEntity):
level: int = 1 level: int = 1
current_xp: int = 0 current_xp: int = 0
max_xp: int = 10 max_xp: int = 10
inventory: list
paths: Dict[Tuple[int, int], Tuple[int, int]]
def move_up(self) -> bool: def __init__(self):
return self.check_move(self.y - 1, self.x, True) super().__init__()
self.inventory = list()
def move_down(self) -> bool: def move(self, y: int, x: int) -> None:
return self.check_move(self.y + 1, self.x, True) """
When the player moves, move the camera of the map.
def move_left(self) -> bool: """
return self.check_move(self.y, self.x - 1, True) super().move(y, x)
self.map.currenty = y
def move_right(self) -> bool: self.map.currentx = x
return self.check_move(self.y, self.x + 1, True) self.recalculate_paths()
def level_up(self) -> None: def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
"""
while self.current_xp > self.max_xp: while self.current_xp > self.max_xp:
self.level += 1 self.level += 1
self.current_xp -= self.max_xp self.current_xp -= self.max_xp
self.max_xp = self.level * 10 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: 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.current_xp += xp
self.level_up() 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

48
dungeonbattle/enums.py Normal file
View File

@ -0,0 +1,48 @@
from enum import Enum, auto
from typing import Optional
from dungeonbattle.settings import Settings
class DisplayActions(Enum):
REFRESH = auto()
UPDATE = auto()
class GameMode(Enum):
MAINMENU = auto()
PLAY = auto()
SETTINGS = auto()
INVENTORY = auto()
class KeyValues(Enum):
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,34 +1,18 @@
import sys from random import randint
from typing import Any from typing import Any, Optional
from .entities.player import Player from .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions
from .interfaces import Map from .interfaces import Map
from .settings import Settings from .settings import Settings
from enum import Enum, auto
from . import menus from . import menus
from typing import Callable from typing import Callable
class GameMode(Enum):
MAINMENU = auto()
PLAY = auto()
SETTINGS = auto()
INVENTORY = auto()
class KeyValues(Enum):
UP = auto()
DOWN = auto()
LEFT = auto()
RIGHT = auto()
ENTER = auto()
SPACE = auto()
class Game: class Game:
map: Map map: Map
player: Player player: Player
display_refresh: Callable[[], None] display_actions: Callable[[DisplayActions], None]
def __init__(self) -> None: def __init__(self) -> None:
""" """
@ -36,21 +20,22 @@ class Game:
""" """
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.main_menu = menus.MainMenu() self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings = Settings() self.settings = Settings()
self.settings.load_settings() self.settings.load_settings()
self.settings.write_settings() self.settings.write_settings()
self.settings_menu.update_values(self.settings)
def new_game(self) -> None: def new_game(self) -> None:
""" """
Create a new game on the screen. Create a new game on the screen.
""" """
# TODO generate a new map procedurally # TODO generate a new map procedurally
self.map = Map.load("resources/example_map.txt") self.map = Map.load("resources/example_map_2.txt")
self.map.currenty = 1
self.map.currentx = 6
self.player = Player() self.player = Player()
self.player.move(1, 6)
self.map.add_entity(self.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))
@staticmethod @staticmethod
def load_game(filename: str) -> None: def load_game(filename: str) -> None:
@ -63,35 +48,16 @@ class Game:
We wait for a player action, then we do what that should be done We wait for a player action, then we do what that should be done
when the given key got pressed. when the given key got pressed.
""" """
while True: while True: # pragma no cover
screen.clear() screen.clear()
screen.refresh() screen.refresh()
self.display_refresh() self.display_actions(DisplayActions.REFRESH)
key = screen.getkey() key = screen.getkey()
self.handle_key_pressed(self.translate_key(key)) self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
def translate_key(self, key: str) -> KeyValues: def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
""" -> None:
Translate the raw string key into an enum value that we can use.
"""
if key in (self.settings.KEY_DOWN_SECONDARY,
self.settings.KEY_DOWN_PRIMARY):
return KeyValues.DOWN
elif key in (self.settings.KEY_LEFT_PRIMARY,
self.settings.KEY_LEFT_SECONDARY):
return KeyValues.LEFT
elif key in (self.settings.KEY_RIGHT_PRIMARY,
self.settings.KEY_RIGHT_SECONDARY):
return KeyValues.RIGHT
elif key in (self.settings.KEY_UP_PRIMARY,
self.settings.KEY_UP_SECONDARY):
return KeyValues.UP
elif key == self.settings.KEY_ENTER:
return KeyValues.ENTER
elif key == ' ':
return KeyValues.SPACE
def handle_key_pressed(self, key: KeyValues) -> None:
""" """
Indicates what should be done when the given key is pressed, Indicates what should be done when the given key is pressed,
according to the current game state. according to the current game state.
@ -99,46 +65,26 @@ class Game:
if self.state == GameMode.PLAY: if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key) self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU: elif self.state == GameMode.MAINMENU:
self.handle_key_pressed_main_menu(key) self.main_menu.handle_key_pressed(key, self)
elif self.state == GameMode.SETTINGS: elif self.state == GameMode.SETTINGS:
self.handle_key_pressed_settings(key) self.settings_menu.handle_key_pressed(key, raw_key, self)
self.display_refresh() self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None: def handle_key_pressed_play(self, key: KeyValues) -> None:
""" """
In play mode, arrows or zqsd should move the main character. In play mode, arrows or zqsd should move the main character.
""" """
if key == KeyValues.UP: if key == KeyValues.UP:
self.player.move_up() if self.player.move_up():
self.map.tick()
elif key == KeyValues.DOWN: elif key == KeyValues.DOWN:
self.player.move_down() if self.player.move_down():
self.map.tick()
elif key == KeyValues.LEFT: elif key == KeyValues.LEFT:
self.player.move_left() if self.player.move_left():
self.map.tick()
elif key == KeyValues.RIGHT: elif key == KeyValues.RIGHT:
self.player.move_right() if self.player.move_right():
self.map.tick()
elif key == KeyValues.SPACE: elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU 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.state = GameMode.PLAY
elif option == menus.MainMenuValues.SETTINGS:
self.state = GameMode.SETTINGS
elif option == menus.MainMenuValues.EXIT:
sys.exit(0)
def handle_key_pressed_settings(self, key: KeyValues) -> None:
"""
For now, in the settings mode, we can only go backwards.
"""
if key == KeyValues.SPACE:
self.state = GameMode.MAINMENU

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
from enum import Enum, auto from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List
from dungeonbattle.display.texturepack import TexturePack from dungeonbattle.display.texturepack import TexturePack
@ -11,15 +14,21 @@ class Map:
""" """
width: int width: int
height: int height: int
tiles: list start_y: int
start_x: int
tiles: List[List["Tile"]]
entities: List["Entity"]
# coordinates of the point that should be # coordinates of the point that should be
# on the topleft corner of the screen # on the topleft corner of the screen
currentx: int currentx: int
currenty: int currenty: int
def __init__(self, width: int, height: int, tiles: list): def __init__(self, width: int, height: int, tiles: list,
start_y: int, start_x: int):
self.width = width self.width = width
self.height = height self.height = height
self.start_y = start_y
self.start_x = start_x
self.tiles = tiles self.tiles = tiles
self.entities = [] self.entities = []
@ -30,8 +39,22 @@ class Map:
self.entities.append(entity) self.entities.append(entity)
entity.map = self entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
"""
self.entities.remove(entity)
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 @staticmethod
def load(filename: str): def load(filename: str) -> "Map":
""" """
Read a file that contains the content of a map, and build a Map object. Read a file that contains the content of a map, and build a Map object.
""" """
@ -40,18 +63,20 @@ class Map:
return Map.load_from_string(file) return Map.load_from_string(file)
@staticmethod @staticmethod
def load_from_string(content: str): def load_from_string(content: str) -> "Map":
""" """
Load a map represented by its characters and build a Map object. Load a map represented by its characters and build a Map object.
""" """
lines = content.split("\n") lines = content.split("\n")
lines = [line for line in lines if line] 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) height = len(lines)
width = len(lines[0]) width = len(lines[0])
tiles = [[Tile.from_ascii_char(c) tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)] for x, c in enumerate(line)] for y, line in enumerate(lines)]
return Map(width, height, tiles) return Map(width, height, tiles, start_y, start_x)
def draw_string(self, pack: TexturePack) -> str: def draw_string(self, pack: TexturePack) -> str:
""" """
@ -61,6 +86,28 @@ class Map:
return "\n".join("".join(tile.char(pack) for tile in line) return "\n".join("".join(tile.char(pack) for tile in line)
for line in self.tiles) 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)
class Tile(Enum): class Tile(Enum):
EMPTY = auto() EMPTY = auto()
@ -84,7 +131,7 @@ class Tile(Enum):
""" """
Check if an entity (player or not) can move in this tile. Check if an entity (player or not) can move in this tile.
""" """
return not self.is_wall() return not self.is_wall() and self != Tile.EMPTY
class Entity: class Entity:
@ -99,14 +146,31 @@ class Entity:
def check_move(self, y: int, x: int, move_if_possible: bool = False)\ def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool: -> bool:
tile = self.map.tiles[y][x] free = self.map.is_free(y, x)
if tile.can_walk() and move_if_possible: if free and move_if_possible:
self.move(y, x) self.move(y, x)
return tile.can_walk() return free
def move(self, y: int, x: int) -> None: def move(self, y: int, x: int) -> bool:
self.y = y self.y = y
self.x = x self.x = x
return True
def move_up(self, force: bool = False) -> bool:
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:
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:
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:
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: def act(self, m: Map) -> None:
""" """
@ -115,6 +179,33 @@ class Entity:
""" """
pass 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:
return isinstance(self, FightingEntity)
def is_item(self) -> bool:
from dungeonbattle.entities.items import Item
return isinstance(self, Item)
@staticmethod
def get_all_entity_classes():
from dungeonbattle.entities.items import Heart, Bomb
from dungeonbattle.entities.monsters import Beaver, Hedgehog, \
Rabbit, TeddyBear
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear]
class FightingEntity(Entity): class FightingEntity(Entity):
maxhealth: int maxhealth: int
@ -142,3 +233,4 @@ class FightingEntity(Entity):
def die(self) -> None: def die(self) -> None:
self.dead = True self.dead = True
self.map.remove_entity(self)

View File

@ -1,5 +1,10 @@
import sys
from enum import Enum from enum import Enum
from typing import Any from typing import Any, Optional
from .display.texturepack import TexturePack
from .enums import GameMode, KeyValues, DisplayActions
from .settings import Settings
class Menu: class Menu:
@ -30,6 +35,84 @@ class MainMenuValues(Enum):
class MainMenu(Menu): class MainMenu(Menu):
values = [e for e in MainMenuValues] values = [e for e in MainMenuValues]
def handle_key_pressed(self, key: KeyValues, game: Any) -> None:
"""
In the main menu, we can navigate through options.
"""
if key == KeyValues.DOWN:
self.go_down()
if key == KeyValues.UP:
self.go_up()
if key == KeyValues.ENTER:
option = self.validate()
if option == MainMenuValues.START:
game.state = GameMode.PLAY
elif option == MainMenuValues.SETTINGS:
game.state = GameMode.SETTINGS
elif option == MainMenuValues.EXIT:
sys.exit(0)
class SettingsMenu(Menu):
waiting_for_key: bool = False
def update_values(self, settings: Settings) -> None:
self.values = []
for i, key in enumerate(settings.settings_keys):
s = settings.get_comment(key)
s += " : "
if self.waiting_for_key and i == self.position:
s += "?"
else:
s += getattr(settings, key).replace("\n", "\\n")
s += 8 * " " # Write over old text
self.values.append(s)
self.values.append("")
self.values.append("Changer le pack de textures n'aura effet")
self.values.append("qu'après avoir relancé le jeu.")
self.values.append("")
self.values.append("Retour (espace)")
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None:
"""
Update settings
"""
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) - 3:
# Change a setting
option = list(game.settings.settings_keys)[self.position]
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 = list(game.settings.settings_keys)[self.position]
# 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)
class ArbitraryMenu(Menu): class ArbitraryMenu(Menu):
def __init__(self, values: list): def __init__(self, values: list):

View File

@ -13,6 +13,8 @@ class TermManager: # pragma: no cover
curses.cbreak() curses.cbreak()
# make cursor invisible # make cursor invisible
curses.curs_set(False) curses.curs_set(False)
# Enable colors
curses.start_color()
def __enter__(self): def __enter__(self):
return self return self

View File

@ -1,7 +1,7 @@
import unittest import unittest
from dungeonbattle.entities.items import Bomb, Item from dungeonbattle.entities.items import Bomb, Heart, Item
from dungeonbattle.entities.monsters import Squirrel from dungeonbattle.entities.monsters import Hedgehog
from dungeonbattle.entities.player import Player from dungeonbattle.entities.player import Player
from dungeonbattle.interfaces import Entity, Map from dungeonbattle.interfaces import Entity, Map
@ -12,6 +12,9 @@ class TestEntities(unittest.TestCase):
Load example map that can be used in tests. Load example map that can be used in tests.
""" """
self.map = Map.load("resources/example_map.txt") 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: def test_basic_entities(self) -> None:
""" """
@ -23,12 +26,17 @@ class TestEntities(unittest.TestCase):
self.assertEqual(entity.x, 64) self.assertEqual(entity.x, 64)
self.assertIsNone(entity.act(self.map)) 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: def test_fighting_entities(self) -> None:
""" """
Test some random stuff with fighting entities. Test some random stuff with fighting entities.
""" """
entity = Squirrel() entity = Hedgehog()
self.assertIsNone(entity.act(self.map)) self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 10) self.assertEqual(entity.maxhealth, 10)
self.assertEqual(entity.maxhealth, entity.health) self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 3) self.assertEqual(entity.strength, 3)
@ -41,35 +49,85 @@ class TestEntities(unittest.TestCase):
self.assertIsNone(entity.hit(entity)) self.assertIsNone(entity.hit(entity))
self.assertTrue(entity.dead) self.assertTrue(entity.dead)
entity = Hedgehog()
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)
# Hedgehog 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 hedgehog
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: def test_items(self) -> None:
""" """
Test some random stuff with items. Test some random stuff with items.
""" """
item = Item() item = Item()
self.map.add_entity(item)
self.assertFalse(item.held) self.assertFalse(item.held)
item.hold() item.hold(self.player)
self.assertTrue(item.held) self.assertTrue(item.held)
item.drop(42, 42) item.drop(2, 6)
self.assertEqual(item.y, 42) self.assertEqual(item.y, 2)
self.assertEqual(item.x, 42) 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: def test_bombs(self) -> None:
""" """
Test some random stuff with bombs. Test some random stuff with bombs.
""" """
item = Bomb() item = Bomb()
squirrel = Squirrel() hedgehog = Hedgehog()
self.map.add_entity(item) self.map.add_entity(item)
self.map.add_entity(squirrel) self.map.add_entity(hedgehog)
squirrel.health = 2 hedgehog.health = 2
squirrel.move(41, 42) hedgehog.move(41, 42)
item.act(self.map) item.act(self.map)
self.assertFalse(squirrel.dead) self.assertFalse(hedgehog.dead)
item.drop(42, 42) item.drop(42, 42)
self.assertEqual(item.y, 42) self.assertEqual(item.y, 42)
self.assertEqual(item.x, 42) self.assertEqual(item.x, 42)
item.act(self.map) item.act(self.map)
self.assertTrue(squirrel.dead) self.assertTrue(hedgehog.dead)
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)
def test_players(self) -> None: def test_players(self) -> None:
""" """

View File

@ -4,8 +4,10 @@ import unittest
from dungeonbattle.bootstrap import Bootstrap from dungeonbattle.bootstrap import Bootstrap
from dungeonbattle.display.display import Display from dungeonbattle.display.display import Display
from dungeonbattle.display.display_manager import DisplayManager from dungeonbattle.display.display_manager import DisplayManager
from dungeonbattle.entities.player import Player
from dungeonbattle.game import Game, KeyValues, GameMode from dungeonbattle.game import Game, KeyValues, GameMode
from dungeonbattle.menus import MainMenuValues from dungeonbattle.menus import MainMenuValues
from dungeonbattle.settings import Settings
class TestGame(unittest.TestCase): class TestGame(unittest.TestCase):
@ -16,7 +18,7 @@ class TestGame(unittest.TestCase):
self.game = Game() self.game = Game()
self.game.new_game() self.game.new_game()
display = DisplayManager(None, self.game) display = DisplayManager(None, self.game)
self.game.display_refresh = display.refresh self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None: def test_load_game(self) -> None:
self.assertRaises(NotImplementedError, Game.load_game, "game.save") self.assertRaises(NotImplementedError, Game.load_game, "game.save")
@ -35,25 +37,39 @@ class TestGame(unittest.TestCase):
""" """
Test key bindings. Test key bindings.
""" """
self.assertEqual(self.game.translate_key( self.game.settings = Settings()
self.game.settings.KEY_UP_PRIMARY), KeyValues.UP)
self.assertEqual(self.game.translate_key( self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_SECONDARY), KeyValues.UP) self.game.settings.KEY_UP_PRIMARY, self.game.settings),
self.assertEqual(self.game.translate_key( KeyValues.UP)
self.game.settings.KEY_DOWN_PRIMARY), KeyValues.DOWN) self.assertEqual(KeyValues.translate_key(
self.assertEqual(self.game.translate_key( self.game.settings.KEY_UP_SECONDARY, self.game.settings),
self.game.settings.KEY_DOWN_SECONDARY), KeyValues.DOWN) KeyValues.UP)
self.assertEqual(self.game.translate_key( self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_PRIMARY), KeyValues.LEFT) self.game.settings.KEY_DOWN_PRIMARY, self.game.settings),
self.assertEqual(self.game.translate_key( KeyValues.DOWN)
self.game.settings.KEY_LEFT_SECONDARY), KeyValues.LEFT) self.assertEqual(KeyValues.translate_key(
self.assertEqual(self.game.translate_key( self.game.settings.KEY_DOWN_SECONDARY, self.game.settings),
self.game.settings.KEY_RIGHT_PRIMARY), KeyValues.RIGHT) KeyValues.DOWN)
self.assertEqual(self.game.translate_key( self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_SECONDARY), KeyValues.RIGHT) self.game.settings.KEY_LEFT_PRIMARY, self.game.settings),
self.assertEqual(self.game.translate_key( KeyValues.LEFT)
self.game.settings.KEY_ENTER), KeyValues.ENTER) self.assertEqual(KeyValues.translate_key(
self.assertEqual(self.game.translate_key(' '), KeyValues.SPACE) 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: def test_key_press(self) -> None:
""" """
@ -90,6 +106,11 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.PLAY) 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 y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.DOWN) self.game.handle_key_pressed(KeyValues.DOWN)
new_y, new_x = self.game.player.y, self.game.player.x new_y, new_x = self.game.player.y, self.game.player.x
@ -116,3 +137,80 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.SPACE) self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU) self.assertEqual(self.game.state, GameMode.MAINMENU)
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.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)

View File

@ -9,7 +9,7 @@ class TestInterfaces(unittest.TestCase):
""" """
Create a map and check that it is well parsed. Create a map and check that it is well parsed.
""" """
m = Map.load_from_string(".#\n#.\n") m = Map.load_from_string("0 0\n.#\n#.\n")
self.assertEqual(m.width, 2) self.assertEqual(m.width, 2)
self.assertEqual(m.height, 2) self.assertEqual(m.height, 2)
self.assertEqual(m.draw_string(TexturePack.ASCII_PACK), ".#\n#.") self.assertEqual(m.draw_string(TexturePack.ASCII_PACK), ".#\n#.")
@ -31,5 +31,5 @@ class TestInterfaces(unittest.TestCase):
self.assertFalse(Tile.EMPTY.is_wall()) self.assertFalse(Tile.EMPTY.is_wall())
self.assertTrue(Tile.FLOOR.can_walk()) self.assertTrue(Tile.FLOOR.can_walk())
self.assertFalse(Tile.WALL.can_walk()) self.assertFalse(Tile.WALL.can_walk())
self.assertTrue(Tile.EMPTY.can_walk()) self.assertFalse(Tile.EMPTY.can_walk())
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown') self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')

View File

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

View File

@ -1,3 +1,4 @@
1 6
####### ############# ####### #############
#.....# #...........# #.....# #...........#
#.....# #####...........# #.....# #####...........#

View File

@ -0,0 +1,41 @@
1 17
########### #########
#.........# #.......#
#.........# ############.......#
#.........###############..........#.......##############
#.........#........................#....................#
#.........#.............#..........#.......#............#
########.########.............#..................#............#
#.........# #.............####.#######.......#............#
#.........# #.............##.........######################
#.........# #####.##########.........# ###########
#.........# #......# #.........# #.........#
########.##########......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# ################.######
#...........##......# #.........# #.................############
#...........##......# ########.########.......#.........#..........#
#...........##......# #...............#.......#.........#..........#
#...........######### #...............#.......#.........#..........#
#...........# #...............#.......#....................#
#####.####### #.......................#.........#..........#
#.........# #...............###################..........#
#.........############ #...............# #..........#
#.........#..........# #...............# ############
#....................#####.###########.#############
########.#########...................# #.............#
#........# #..........#........# #.............#########
#........# ######.##########........# #.............#.......#
#........# #..........# #........# #.....................#
#........# #..........# #........# #.............#.......#
#........# #..........# #........# #.............#.......#
#........# #..........# #........# #.............#.......#
#........# #..........#########.##### #.............#.......#
#........# #..........#.........# ##########.############.#######
#........# #..........#.........# #..............# #..........#
########## #..........#.........# #..............# #..........#
############.........# #..............# #..........#
#.........# #..............# #..........#
########### #..............# #..........#
################ ############

View File

@ -1,7 +1,6 @@
[tox] [tox]
envlist = envlist =
py38 py3
py39
linters linters
skipsdist = True skipsdist = True