Merge branch 'display' into 'master'

Meilleur affichage

See merge request ynerant/dungeon-battle!8
This commit is contained in:
ynerant 2020-11-10 21:34:34 +01:00
commit 3b3b8ee8da
25 changed files with 527 additions and 117 deletions

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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='🐿️',
)

View File

@ -2,8 +2,15 @@ from ..interfaces import FightingEntity
class Player(FightingEntity): class Player(FightingEntity):
maxhealth = 20 maxhealth: int = 20
strength = 5 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: def move_up(self) -> bool:
return self.check_move(self.y - 1, self.x, True) return self.check_move(self.y - 1, self.x, True)
@ -16,3 +23,13 @@ class Player(FightingEntity):
def move_right(self) -> bool: def move_right(self) -> bool:
return self.check_move(self.y, self.x + 1, True) 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()

View File

@ -3,10 +3,10 @@ from typing import Any
from .entities.player import Player from .entities.player import Player
from .interfaces import Map from .interfaces import Map
from .mapdisplay import MapDisplay
from .settings import Settings from .settings import Settings
from enum import Enum, auto from enum import Enum, auto
from . import menus from . import menus
from typing import Callable
class GameMode(Enum): class GameMode(Enum):
@ -22,23 +22,35 @@ class KeyValues(Enum):
LEFT = auto() LEFT = auto()
RIGHT = auto() RIGHT = auto()
ENTER = auto() ENTER = auto()
SPACE = auto()
class Game: class Game:
map: Map
player: Player
display_refresh: Callable[[], None]
def __init__(self) -> None: def __init__(self) -> None:
"""
Init the game.
"""
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.main_menu = menus.MainMenu() self.main_menu = menus.MainMenu()
self.settings = Settings() self.settings = Settings()
self.settings.load_settings() self.settings.load_settings()
self.settings.write_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 # 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 = Player()
self.player.move(1, 6) self.player.move(1, 6)
self.m.add_entity(self.player) self.map.add_entity(self.player)
self.d = MapDisplay(self.m, self.player, init_pad)
@staticmethod @staticmethod
def load_game(filename: str) -> None: def load_game(filename: str) -> None:
@ -46,14 +58,22 @@ class Game:
raise NotImplementedError() raise NotImplementedError()
def run(self, screen: Any) -> None: 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: while True:
screen.clear() screen.clear()
screen.refresh() screen.refresh()
self.d.display(self.player.y, self.player.x) self.display_refresh()
key = screen.getkey() key = screen.getkey()
self.handle_key_pressed(self.translate_key(key)) self.handle_key_pressed(self.translate_key(key))
def translate_key(self, key: str) -> KeyValues: 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, if key in (self.settings.KEY_DOWN_SECONDARY,
self.settings.KEY_DOWN_PRIMARY): self.settings.KEY_DOWN_PRIMARY):
return KeyValues.DOWN return KeyValues.DOWN
@ -68,27 +88,57 @@ class Game:
return KeyValues.UP return KeyValues.UP
elif key == self.settings.KEY_ENTER: elif key == self.settings.KEY_ENTER:
return KeyValues.ENTER return KeyValues.ENTER
elif key == ' ':
return KeyValues.SPACE
def handle_key_pressed(self, key: KeyValues) -> None: 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 self.state == GameMode.PLAY:
if key == KeyValues.UP: self.handle_key_pressed_play(key)
self.player.move_up() elif self.state == GameMode.MAINMENU:
if key == KeyValues.DOWN: self.handle_key_pressed_main_menu(key)
self.player.move_down() elif self.state == GameMode.SETTINGS:
if key == KeyValues.LEFT: self.handle_key_pressed_settings(key)
self.player.move_left() self.display_refresh()
if key == KeyValues.RIGHT:
self.player.move_right() def handle_key_pressed_play(self, key: KeyValues) -> None:
if self.state == GameMode.MAINMENU: """
if key == KeyValues.DOWN: In play mode, arrows or zqsd should move the main character.
self.main_menu.go_down() """
if key == KeyValues.UP: if key == KeyValues.UP:
self.main_menu.go_up() self.player.move_up()
if key == KeyValues.ENTER: elif key == KeyValues.DOWN:
option = self.main_menu.validate() self.player.move_down()
if option == menus.MainMenuValues.START: elif key == KeyValues.LEFT:
self.state = GameMode.PLAY self.player.move_left()
elif option == menus.MainMenuValues.SETTINGS: elif key == KeyValues.RIGHT:
self.state = GameMode.SETTINGS self.player.move_right()
elif option == menus.MainMenuValues.EXIT: elif key == KeyValues.SPACE:
sys.exit(0) 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,7 @@
#!/usr/bin/env python #!/usr/bin/env python
from enum import Enum from enum import Enum, auto
from dungeonbattle.display.texturepack import TexturePack
class Map: class Map:
@ -10,6 +12,10 @@ class Map:
width: int width: int
height: int height: int
tiles: list 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): def __init__(self, width: int, height: int, tiles: list):
self.width = width self.width = width
@ -42,24 +48,34 @@ class Map:
lines = [line for line in lines if line] lines = [line for line in lines if line]
height = len(lines) height = len(lines)
width = len(lines[0]) 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)] for x, c in enumerate(line)] for y, line in enumerate(lines)]
return Map(width, height, tiles) 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 Draw the current map as a string object that can be rendered
in the window. 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) for line in self.tiles)
class Tile(Enum): class Tile(Enum):
EMPTY = ' ' EMPTY = auto()
WALL = '' WALL = auto()
FLOOR = '.' 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: def is_wall(self) -> bool:
return self == Tile.WALL return self == Tile.WALL
@ -74,6 +90,7 @@ class Tile(Enum):
class Entity: class Entity:
y: int y: int
x: int x: int
name: str
map: Map map: Map
def __init__(self): def __init__(self):
@ -104,6 +121,11 @@ class FightingEntity(Entity):
health: int health: int
strength: int strength: int
dead: bool dead: bool
intelligence: int
charisma: int
dexterity: int
constitution: int
level: int
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

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

View File

@ -1,4 +1,4 @@
from enum import Enum, auto from enum import Enum
from typing import Any from typing import Any
@ -19,9 +19,12 @@ class Menu:
class MainMenuValues(Enum): class MainMenuValues(Enum):
START = auto() START = 'Jouer'
SETTINGS = auto() SETTINGS = 'Paramètres'
EXIT = auto() EXIT = 'Quitter'
def __str__(self):
return self.value
class MainMenu(Menu): class MainMenu(Menu):

View File

@ -29,7 +29,7 @@ class Settings:
['KEY_RIGHT', 'Touche secondaire pour aller vers la droite'] ['KEY_RIGHT', 'Touche secondaire pour aller vers la droite']
self.KEY_ENTER = \ self.KEY_ENTER = \
['\n', 'Touche pour valider un menu'] ['\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: def __getattribute__(self, item: str) -> Any:
superattribute = super().__getattribute__(item) superattribute = super().__getattribute__(item)

View File

@ -2,7 +2,7 @@ import curses
from types import TracebackType from types import TracebackType
class TermManager: class TermManager: # pragma: no cover
def __init__(self): def __init__(self):
self.screen = curses.initscr() self.screen = curses.initscr()
# convert escapes sequences to curses abstraction # convert escapes sequences to curses abstraction

View File

@ -11,7 +11,7 @@ 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("example_map.txt") self.map = Map.load("resources/example_map.txt")
def test_basic_entities(self) -> None: def test_basic_entities(self) -> None:
""" """
@ -95,3 +95,8 @@ class TestEntities(unittest.TestCase):
self.assertFalse(player.move_right()) self.assertFalse(player.move_right())
self.assertTrue(player.move_down()) self.assertTrue(player.move_down())
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)

View File

@ -1,5 +1,9 @@
import os
import unittest 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.game import Game, KeyValues, GameMode
from dungeonbattle.menus import MainMenuValues from dungeonbattle.menus import MainMenuValues
@ -10,10 +14,22 @@ class TestGame(unittest.TestCase):
Setup game. Setup game.
""" """
self.game = 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: def test_load_game(self) -> None:
self.assertRaises(NotImplementedError, Game.load_game, "game.save") 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: def test_key_translation(self) -> None:
""" """
@ -37,6 +53,7 @@ class TestGame(unittest.TestCase):
self.game.settings.KEY_RIGHT_SECONDARY), KeyValues.RIGHT) self.game.settings.KEY_RIGHT_SECONDARY), KeyValues.RIGHT)
self.assertEqual(self.game.translate_key( self.assertEqual(self.game.translate_key(
self.game.settings.KEY_ENTER), KeyValues.ENTER) self.game.settings.KEY_ENTER), KeyValues.ENTER)
self.assertEqual(self.game.translate_key(' '), KeyValues.SPACE)
def test_key_press(self) -> None: def test_key_press(self) -> None:
""" """
@ -54,7 +71,8 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS) 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.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), 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 new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y) self.assertEqual(new_y, y)
self.assertEqual(new_x, x - 1) self.assertEqual(new_x, x - 1)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)

View File

@ -1,5 +1,6 @@
import unittest import unittest
from dungeonbattle.display.texturepack import TexturePack
from dungeonbattle.interfaces import Map, Tile from dungeonbattle.interfaces import Map, Tile
@ -8,16 +9,16 @@ 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(".#\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(), ".█\n.") self.assertEqual(m.draw_string(TexturePack.ASCII_PACK), ".#\n#.")
def test_load_map(self) -> None: def test_load_map(self) -> None:
""" """
Try to load a map from a file. 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.width, 52)
self.assertEqual(m.height, 17) self.assertEqual(m.height, 17)
@ -31,3 +32,4 @@ class TestInterfaces(unittest.TestCase):
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.assertTrue(Tile.EMPTY.can_walk())
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')

View File

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

View File

@ -17,16 +17,16 @@ class TestSettings(unittest.TestCase):
self.assertEqual(settings.KEY_DOWN_SECONDARY, 'KEY_DOWN') self.assertEqual(settings.KEY_DOWN_SECONDARY, 'KEY_DOWN')
self.assertEqual(settings.KEY_LEFT_SECONDARY, 'KEY_LEFT') self.assertEqual(settings.KEY_LEFT_SECONDARY, 'KEY_LEFT')
self.assertEqual(settings.KEY_RIGHT_SECONDARY, 'KEY_RIGHT') 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), self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
settings.get_comment('TEXTURE_PACK')) settings.get_comment('TEXTURE_PACK'))
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK), self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
'Pack de textures utilisé') 'Pack de textures utilisé')
settings.TEXTURE_PACK = 'UNICODE' settings.TEXTURE_PACK = 'squirrel'
self.assertEqual(settings.TEXTURE_PACK, 'UNICODE') self.assertEqual(settings.TEXTURE_PACK, 'squirrel')
settings.write_settings() settings.write_settings()
settings.load_settings() settings.load_settings()
self.assertEqual(settings.TEXTURE_PACK, 'UNICODE') self.assertEqual(settings.TEXTURE_PACK, 'squirrel')

View File

@ -1,8 +0,0 @@
# This is the base ascii texturepack
ascii = {
"EMPTY": ' ',
"WALL": '#',
"FLOOR": '.',
"PLAYER": '@'
}

View File

@ -1,17 +0,0 @@
███████ █████████████
█.....█ █...........█
█.....█ █████...........█
█.....█ █...............█
█.█████ █.███...........█
█.█ █.█ █...........█
█.█ █.█ █████████████
█.█ █.█
█.████ █.█
█....█ █.█
████.███████████████████.█
█.....................█ █████████████████
█.....................█ █...............█
█.....................███████...............█
█...........................................█
█.....................███████...............█
███████████████████████ █████████████████

View File

@ -1,9 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
from dungeonbattle.game import Game from dungeonbattle.bootstrap import Bootstrap
from dungeonbattle.term_manager import TermManager
if __name__ == "__main__": if __name__ == "__main__":
with TermManager() as term_manager: Bootstrap.run_game()
game = Game()
game.new_game()
game.run(term_manager.screen)

11
resources/ascii_art.txt Normal file
View File

@ -0,0 +1,11 @@
██████ █████ █ ██ ██▓ ██▀███ ██▀███ ▓█████ ██▓ ▄▄▄▄ ▄▄▄ ▄▄▄█████▓▄▄▄█████▓ ██▓ ▓█████
▒██ ▒ ▒██▓ ██▒ ██ ▓██▒▓██▒▓██ ▒ ██▒▓██ ▒ ██▒▓█ ▀ ▓██▒ ▓█████▄ ▒████▄ ▓ ██▒ ▓▒▓ ██▒ ▓▒▓██▒ ▓█ ▀
░ ▓██▄ ▒██▒ ██░▓██ ▒██░▒██▒▓██ ░▄█ ▒▓██ ░▄█ ▒▒███ ▒██░ ▒██▒ ▄██▒██ ▀█▄ ▒ ▓██░ ▒░▒ ▓██░ ▒░▒██░ ▒███
▒ ██▒░██ █▀ ░▓▓█ ░██░░██░▒██▀▀█▄ ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ▒██░█▀ ░██▄▄▄▄██░ ▓██▓ ░ ░ ▓██▓ ░ ▒██░ ▒▓█ ▄
▒██████▒▒░▒███▒█▄ ▒▒█████▓ ░██░░██▓ ▒██▒░██▓ ▒██▒░▒████▒░██████▒ ░▓█ ▀█▓ ▓█ ▓██▒ ▒██▒ ░ ▒██▒ ░ ░██████▒░▒████▒
▒ ▒▓▒ ▒ ░░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ▒▓ ░▒▓░░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░ ░▒▓███▀▒ ▒▒ ▓▒█░ ▒ ░░ ▒ ░░ ░ ▒░▓ ░░░ ▒░ ░
░ ░▒ ░ ░ ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░▒ ░ ▒░ ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒░▒ ░ ▒ ▒▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░
░ ░ ░ ░ ░ ░░░ ░ ░ ▒ ░ ░░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░

17
resources/example_map.txt Normal file
View File

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