diff --git a/dungeonbattle/bootstrap.py b/dungeonbattle/bootstrap.py new file mode 100644 index 0000000..a2b5c72 --- /dev/null +++ b/dungeonbattle/bootstrap.py @@ -0,0 +1,15 @@ +from dungeonbattle.game import Game +from dungeonbattle.display.display_manager import DisplayManager +from dungeonbattle.term_manager import TermManager + + +class Bootstrap: + + @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_refresh = display.refresh + game.run(term_manager.screen) diff --git a/dungeonbattle/display/__init__.py b/dungeonbattle/display/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dungeonbattle/display/display.py b/dungeonbattle/display/display.py new file mode 100644 index 0000000..64314e9 --- /dev/null +++ b/dungeonbattle/display/display.py @@ -0,0 +1,44 @@ +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 resize(self, y: int, x: int, height: int, width: int) -> None: + self.x = x + self.y = y + self.width = width + self.height = height + if self.pad: + self.pad.resize(height - 1, width - 1) + + def refresh(self, *args) -> None: + if len(args) == 4: + self.resize(*args) + 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 diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py new file mode 100644 index 0000000..5c249c8 --- /dev/null +++ b/dungeonbattle/display/display_manager.py @@ -0,0 +1,56 @@ +import curses +from dungeonbattle.display.mapdisplay import MapDisplay +from dungeonbattle.display.statsdisplay import StatsDisplay +from dungeonbattle.display.menudisplay import MainMenuDisplay +from dungeonbattle.display.texturepack import TexturePack +from typing import Any +from dungeonbattle.game import Game, GameMode + + +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.displays = [self.statsdisplay, self.mapdisplay, + self.mainmenudisplay] + 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) + + def refresh(self) -> None: + if self.game.state == GameMode.PLAY: + self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols) + 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) + 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 diff --git a/dungeonbattle/display/mapdisplay.py b/dungeonbattle/display/mapdisplay.py new file mode 100644 index 0000000..0a07ec0 --- /dev/null +++ b/dungeonbattle/display/mapdisplay.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +from dungeonbattle.entities.player import Player +from dungeonbattle.interfaces import Map +from .display import Display + + +class MapDisplay(Display): + player: Player + + def __init__(self, *args): + super().__init__(*args) + + def update_map(self, m: Map) -> None: + self.map = m + self.pad = self.newpad(m.height, m.width + 1) + + def update_pad(self) -> None: + self.pad.addstr(0, 0, self.map.draw_string(self.pack)) + for e in self.map.entities: + self.pad.addstr(e.y, e.x, self.pack.PLAYER) + + def display(self) -> None: + y, x = self.map.currenty, 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.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.map.width, pmincol)) + self.pad.clear() + self.update_pad() + self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) diff --git a/dungeonbattle/display/menudisplay.py b/dungeonbattle/display/menudisplay.py new file mode 100644 index 0000000..aeed447 --- /dev/null +++ b/dungeonbattle/display/menudisplay.py @@ -0,0 +1,79 @@ +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.values = [str(a) for a in menu.values] + 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, " ") + # 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 + + +class MainMenuDisplay(Display): + def __init__(self, menu: MainMenu, *args): + super().__init__(*args) + self.menu = menu + self.pad = self.newpad(self.rows, self.cols) + + with open("resources/ascii_art.txt", "r") as file: + self.title = file.read().split("\n") + + 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, self.width // 2 + - len(self.title[0]) // 2 - 1, 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) diff --git a/dungeonbattle/display/statsdisplay.py b/dungeonbattle/display/statsdisplay.py new file mode 100644 index 0000000..bf204dd --- /dev/null +++ b/dungeonbattle/display/statsdisplay.py @@ -0,0 +1,41 @@ +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) + + 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) + + def display(self) -> None: + self.pad.clear() + self.update_pad() + self.pad.refresh(0, 0, self.y, self.x, + 2 + self.y, + self.width + self.x) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py new file mode 100644 index 0000000..6929a9c --- /dev/null +++ b/dungeonbattle/display/texturepack.py @@ -0,0 +1,37 @@ +class TexturePack: + _packs = dict() + + name: str + 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 + + @classmethod + def get_pack(cls, name: str) -> "TexturePack": + return cls._packs[name.lower()] + + +TexturePack.ASCII_PACK = TexturePack( + name="ascii", + EMPTY=' ', + WALL='#', + FLOOR='.', + PLAYER='@', +) + +TexturePack.SQUIRREL_PACK = TexturePack( + name="squirrel", + EMPTY=' ', + WALL='█', + FLOOR='.', + PLAYER='🐿️', +) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index df96b29..9f01d0d 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -2,8 +2,15 @@ from ..interfaces import FightingEntity class Player(FightingEntity): - maxhealth = 20 - strength = 5 + 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 def move_up(self) -> bool: return self.check_move(self.y - 1, self.x, True) @@ -16,3 +23,13 @@ class Player(FightingEntity): def move_right(self) -> bool: return self.check_move(self.y, self.x + 1, True) + + def level_up(self) -> None: + while self.current_xp > self.max_xp: + self.level += 1 + self.current_xp -= self.max_xp + self.max_xp = self.level * 10 + + def add_xp(self, xp: int) -> None: + self.current_xp += xp + self.level_up() diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index e0e707d..3dc60cc 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -3,10 +3,10 @@ from typing import Any from .entities.player import Player from .interfaces import Map -from .mapdisplay import MapDisplay from .settings import Settings from enum import Enum, auto from . import menus +from typing import Callable class GameMode(Enum): @@ -22,23 +22,35 @@ class KeyValues(Enum): LEFT = auto() RIGHT = auto() ENTER = auto() + SPACE = auto() class Game: + map: Map + player: Player + display_refresh: Callable[[], None] + def __init__(self) -> None: + """ + Init the game. + """ self.state = GameMode.MAINMENU self.main_menu = menus.MainMenu() self.settings = Settings() self.settings.load_settings() self.settings.write_settings() - def new_game(self, init_pad: bool = True) -> None: + def new_game(self) -> None: + """ + Create a new game on the screen. + """ # TODO generate a new map procedurally - self.m = Map.load("example_map.txt") + self.map = Map.load("resources/example_map.txt") + self.map.currenty = 1 + self.map.currentx = 6 self.player = Player() self.player.move(1, 6) - self.m.add_entity(self.player) - self.d = MapDisplay(self.m, self.player, init_pad) + self.map.add_entity(self.player) @staticmethod def load_game(filename: str) -> None: @@ -46,14 +58,22 @@ class Game: raise NotImplementedError() def run(self, screen: Any) -> None: + """ + Main infinite loop. + We wait for a player action, then we do what that should be done + when the given key got pressed. + """ while True: screen.clear() screen.refresh() - self.d.display(self.player.y, self.player.x) + self.display_refresh() key = screen.getkey() self.handle_key_pressed(self.translate_key(key)) def translate_key(self, key: str) -> KeyValues: + """ + 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 @@ -68,27 +88,57 @@ class Game: 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, + according to the current game state. + """ if self.state == GameMode.PLAY: - if key == KeyValues.UP: - self.player.move_up() - if key == KeyValues.DOWN: - self.player.move_down() - if key == KeyValues.LEFT: - self.player.move_left() - if key == KeyValues.RIGHT: - self.player.move_right() - if self.state == GameMode.MAINMENU: - 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) + self.handle_key_pressed_play(key) + elif self.state == GameMode.MAINMENU: + self.handle_key_pressed_main_menu(key) + elif self.state == GameMode.SETTINGS: + self.handle_key_pressed_settings(key) + self.display_refresh() + + def handle_key_pressed_play(self, key: KeyValues) -> None: + """ + In play mode, arrows or zqsd should move the main character. + """ + if key == KeyValues.UP: + self.player.move_up() + elif key == KeyValues.DOWN: + self.player.move_down() + elif key == KeyValues.LEFT: + self.player.move_left() + elif key == KeyValues.RIGHT: + self.player.move_right() + 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.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 diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index f9d177d..bbcd25e 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -1,5 +1,7 @@ #!/usr/bin/env python -from enum import Enum +from enum import Enum, auto + +from dungeonbattle.display.texturepack import TexturePack class Map: @@ -10,6 +12,10 @@ class Map: width: int height: int tiles: list + # 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): self.width = width @@ -42,24 +48,34 @@ class Map: lines = [line for line in lines if line] height = len(lines) width = len(lines[0]) - tiles = [[Tile(c) + tiles = [[Tile.from_ascii_char(c) for x, c in enumerate(line)] for y, line in enumerate(lines)] return Map(width, height, tiles) - def draw_string(self) -> str: + 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.value for tile in line) + return "\n".join("".join(tile.char(pack) for tile in line) for line in self.tiles) class Tile(Enum): - EMPTY = ' ' - WALL = '█' - FLOOR = '.' + EMPTY = auto() + WALL = auto() + FLOOR = auto() + + @classmethod + def from_ascii_char(cls, ch: str) -> "Tile": + for tile in Tile: + if tile.char(TexturePack.ASCII_PACK) == ch: + return tile + raise ValueError(ch) + + def char(self, pack: TexturePack) -> str: + return getattr(pack, self.name) def is_wall(self) -> bool: return self == Tile.WALL @@ -74,6 +90,7 @@ class Tile(Enum): class Entity: y: int x: int + name: str map: Map def __init__(self): @@ -104,6 +121,11 @@ class FightingEntity(Entity): health: int strength: int dead: bool + intelligence: int + charisma: int + dexterity: int + constitution: int + level: int def __init__(self): super().__init__() diff --git a/dungeonbattle/mapdisplay.py b/dungeonbattle/mapdisplay.py deleted file mode 100644 index 095b7cb..0000000 --- a/dungeonbattle/mapdisplay.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -import curses - -from dungeonbattle.entities.player import Player -from dungeonbattle.interfaces import Map - - -class MapDisplay: - def __init__(self, m: Map, player: Player, init_pad: bool = True): - self.map = m - self.player = player - if init_pad: - self.pad = curses.newpad(m.height, m.width + 1) - - def update_pad(self) -> None: - self.pad.addstr(0, 0, self.map.draw_string()) - # TODO Not all entities should be a player - for e in self.map.entities: - self.pad.addstr(e.y, e.x, '🐿') - - def display(self, y: int, x: int) -> None: - deltay, deltax = (curses.LINES // 2) + 1, (curses.COLS // 2) + 1 - pminrow, pmincol = y - deltay, x - deltax - sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0) - deltay, deltax = curses.LINES - deltay, curses.COLS - deltax - smaxrow = self.map.height - (y + deltay) + curses.LINES - 1 - smaxrow = min(smaxrow, curses.LINES - 1) - smaxcol = self.map.width - (x + deltax) + curses.COLS - 1 - smaxcol = min(smaxcol, curses.COLS - 1) - pminrow = max(0, min(self.map.height, pminrow)) - pmincol = max(0, min(self.map.width, pmincol)) - self.pad.clear() - self.update_pad() - self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 82d6d4e..68eeab7 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum from typing import Any @@ -19,9 +19,12 @@ class Menu: class MainMenuValues(Enum): - START = auto() - SETTINGS = auto() - EXIT = auto() + START = 'Jouer' + SETTINGS = 'Paramètres' + EXIT = 'Quitter' + + def __str__(self): + return self.value class MainMenu(Menu): diff --git a/dungeonbattle/settings.py b/dungeonbattle/settings.py index db216df..258d88f 100644 --- a/dungeonbattle/settings.py +++ b/dungeonbattle/settings.py @@ -29,7 +29,7 @@ class Settings: ['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é'] + self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé'] def __getattribute__(self, item: str) -> Any: superattribute = super().__getattribute__(item) diff --git a/dungeonbattle/term_manager.py b/dungeonbattle/term_manager.py index 1f1d364..ab7f4dd 100644 --- a/dungeonbattle/term_manager.py +++ b/dungeonbattle/term_manager.py @@ -2,7 +2,7 @@ import curses from types import TracebackType -class TermManager: +class TermManager: # pragma: no cover def __init__(self): self.screen = curses.initscr() # convert escapes sequences to curses abstraction diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index 7b7902b..00ea33b 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -11,7 +11,7 @@ class TestEntities(unittest.TestCase): """ Load example map that can be used in tests. """ - self.map = Map.load("example_map.txt") + self.map = Map.load("resources/example_map.txt") def test_basic_entities(self) -> None: """ @@ -95,3 +95,8 @@ class TestEntities(unittest.TestCase): 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) diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 4183cbe..2498b48 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -1,5 +1,9 @@ +import os import unittest +from dungeonbattle.bootstrap import Bootstrap +from dungeonbattle.display.display import Display +from dungeonbattle.display.display_manager import DisplayManager from dungeonbattle.game import Game, KeyValues, GameMode from dungeonbattle.menus import MainMenuValues @@ -10,10 +14,22 @@ class TestGame(unittest.TestCase): Setup game. """ self.game = Game() - self.game.new_game(False) + self.game.new_game() + display = DisplayManager(None, self.game) + self.game.display_refresh = display.refresh def test_load_game(self) -> None: self.assertRaises(NotImplementedError, Game.load_game, "game.save") + self.assertRaises(NotImplementedError, Display(None).display) + + 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: """ @@ -37,6 +53,7 @@ class TestGame(unittest.TestCase): self.game.settings.KEY_RIGHT_SECONDARY), KeyValues.RIGHT) self.assertEqual(self.game.translate_key( self.game.settings.KEY_ENTER), KeyValues.ENTER) + self.assertEqual(self.game.translate_key(' '), KeyValues.SPACE) def test_key_press(self) -> None: """ @@ -54,7 +71,8 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.ENTER) self.assertEqual(self.game.state, GameMode.SETTINGS) - self.game.state = GameMode.MAINMENU + 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(), @@ -95,3 +113,6 @@ class TestGame(unittest.TestCase): 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) diff --git a/dungeonbattle/tests/interfaces_test.py b/dungeonbattle/tests/interfaces_test.py index d6ab078..c36e895 100644 --- a/dungeonbattle/tests/interfaces_test.py +++ b/dungeonbattle/tests/interfaces_test.py @@ -1,5 +1,6 @@ import unittest +from dungeonbattle.display.texturepack import TexturePack from dungeonbattle.interfaces import Map, Tile @@ -8,16 +9,16 @@ class TestInterfaces(unittest.TestCase): """ Create a map and check that it is well parsed. """ - m = Map.load_from_string(".█\n█.\n") + m = Map.load_from_string(".#\n#.\n") self.assertEqual(m.width, 2) self.assertEqual(m.height, 2) - self.assertEqual(m.draw_string(), ".█\n█.") + 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("example_map.txt") + m = Map.load("resources/example_map.txt") self.assertEqual(m.width, 52) self.assertEqual(m.height, 17) @@ -31,3 +32,4 @@ class TestInterfaces(unittest.TestCase): self.assertTrue(Tile.FLOOR.can_walk()) self.assertFalse(Tile.WALL.can_walk()) self.assertTrue(Tile.EMPTY.can_walk()) + self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown') diff --git a/dungeonbattle/tests/screen.py b/dungeonbattle/tests/screen.py new file mode 100644 index 0000000..b9ec851 --- /dev/null +++ b/dungeonbattle/tests/screen.py @@ -0,0 +1,17 @@ +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) -> 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 diff --git a/dungeonbattle/tests/settings_test.py b/dungeonbattle/tests/settings_test.py index 5821521..9a56048 100644 --- a/dungeonbattle/tests/settings_test.py +++ b/dungeonbattle/tests/settings_test.py @@ -17,16 +17,16 @@ class TestSettings(unittest.TestCase): 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.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 = 'UNICODE' - self.assertEqual(settings.TEXTURE_PACK, 'UNICODE') + settings.TEXTURE_PACK = 'squirrel' + self.assertEqual(settings.TEXTURE_PACK, 'squirrel') settings.write_settings() settings.load_settings() - self.assertEqual(settings.TEXTURE_PACK, 'UNICODE') + self.assertEqual(settings.TEXTURE_PACK, 'squirrel') diff --git a/dungeonbattle/texturepack.py b/dungeonbattle/texturepack.py index d9b5818..e69de29 100644 --- a/dungeonbattle/texturepack.py +++ b/dungeonbattle/texturepack.py @@ -1,8 +0,0 @@ -# This is the base ascii texturepack - -ascii = { - "EMPTY": ' ', - "WALL": '#', - "FLOOR": '.', - "PLAYER": '@' -} diff --git a/example_map.txt b/example_map.txt deleted file mode 100644 index bc0c464..0000000 --- a/example_map.txt +++ /dev/null @@ -1,17 +0,0 @@ - ███████ █████████████ - █.....█ █...........█ - █.....█ █████...........█ - █.....█ █...............█ - █.█████ █.███...........█ - █.█ █.█ █...........█ - █.█ █.█ █████████████ - █.█ █.█ - █.████ █.█ - █....█ █.█ - ████.███████████████████.█ - █.....................█ █████████████████ - █.....................█ █...............█ - █.....................███████...............█ - █...........................................█ - █.....................███████...............█ - ███████████████████████ █████████████████ diff --git a/main.py b/main.py index 2e44d9d..e918f0d 100755 --- a/main.py +++ b/main.py @@ -1,9 +1,5 @@ #!/usr/bin/env python -from dungeonbattle.game import Game -from dungeonbattle.term_manager import TermManager +from dungeonbattle.bootstrap import Bootstrap if __name__ == "__main__": - with TermManager() as term_manager: - game = Game() - game.new_game() - game.run(term_manager.screen) + Bootstrap.run_game() diff --git a/resources/ascii_art.txt b/resources/ascii_art.txt new file mode 100644 index 0000000..966e832 --- /dev/null +++ b/resources/ascii_art.txt @@ -0,0 +1,11 @@ + ██████ █████ █ ██ ██▓ ██▀███ ██▀███ ▓█████ ██▓ ▄▄▄▄ ▄▄▄ ▄▄▄█████▓▄▄▄█████▓ ██▓ ▓█████ +▒██ ▒ ▒██▓ ██▒ ██ ▓██▒▓██▒▓██ ▒ ██▒▓██ ▒ ██▒▓█ ▀ ▓██▒ ▓█████▄ ▒████▄ ▓ ██▒ ▓▒▓ ██▒ ▓▒▓██▒ ▓█ ▀ +░ ▓██▄ ▒██▒ ██░▓██ ▒██░▒██▒▓██ ░▄█ ▒▓██ ░▄█ ▒▒███ ▒██░ ▒██▒ ▄██▒██ ▀█▄ ▒ ▓██░ ▒░▒ ▓██░ ▒░▒██░ ▒███ + ▒ ██▒░██ █▀ ░▓▓█ ░██░░██░▒██▀▀█▄ ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ▒██░█▀ ░██▄▄▄▄██░ ▓██▓ ░ ░ ▓██▓ ░ ▒██░ ▒▓█ ▄ +▒██████▒▒░▒███▒█▄ ▒▒█████▓ ░██░░██▓ ▒██▒░██▓ ▒██▒░▒████▒░██████▒ ░▓█ ▀█▓ ▓█ ▓██▒ ▒██▒ ░ ▒██▒ ░ ░██████▒░▒████▒ +▒ ▒▓▒ ▒ ░░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ▒▓ ░▒▓░░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░ ░▒▓███▀▒ ▒▒ ▓▒█░ ▒ ░░ ▒ ░░ ░ ▒░▓ ░░░ ▒░ ░ +░ ░▒ ░ ░ ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░▒ ░ ▒░ ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒░▒ ░ ▒ ▒▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ +░ ░ ░ ░ ░ ░░░ ░ ░ ▒ ░ ░░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ + ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ + ░ + diff --git a/resources/example_map.txt b/resources/example_map.txt new file mode 100644 index 0000000..4111fae --- /dev/null +++ b/resources/example_map.txt @@ -0,0 +1,17 @@ + ####### ############# + #.....# #...........# + #.....# #####...........# + #.....# #...............# + #.##### #.###...........# + #.# #.# #...........# + #.# #.# ############# + #.# #.# + #.#### #.# + #....# #.# + ####.###################.# + #.....................# ################# + #.....................# #...............# + #.....................#######...............# + #...........................................# + #.....................#######...............# + ####################### #################