Merge branch 'display' into 'master'
Meilleur affichage See merge request ynerant/dungeon-battle!8
This commit is contained in:
commit
3b3b8ee8da
15
dungeonbattle/bootstrap.py
Normal file
15
dungeonbattle/bootstrap.py
Normal 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)
|
0
dungeonbattle/display/__init__.py
Normal file
0
dungeonbattle/display/__init__.py
Normal file
44
dungeonbattle/display/display.py
Normal file
44
dungeonbattle/display/display.py
Normal 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
|
56
dungeonbattle/display/display_manager.py
Normal file
56
dungeonbattle/display/display_manager.py
Normal 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
|
36
dungeonbattle/display/mapdisplay.py
Normal file
36
dungeonbattle/display/mapdisplay.py
Normal 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)
|
79
dungeonbattle/display/menudisplay.py
Normal file
79
dungeonbattle/display/menudisplay.py
Normal 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)
|
41
dungeonbattle/display/statsdisplay.py
Normal file
41
dungeonbattle/display/statsdisplay.py
Normal 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)
|
37
dungeonbattle/display/texturepack.py
Normal file
37
dungeonbattle/display/texturepack.py
Normal 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='🐿️',
|
||||
)
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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__()
|
||||
|
@ -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)
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
17
dungeonbattle/tests/screen.py
Normal file
17
dungeonbattle/tests/screen.py
Normal 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
|
@ -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')
|
||||
|
@ -1,8 +0,0 @@
|
||||
# This is the base ascii texturepack
|
||||
|
||||
ascii = {
|
||||
"EMPTY": ' ',
|
||||
"WALL": '#',
|
||||
"FLOOR": '.',
|
||||
"PLAYER": '@'
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
███████ █████████████
|
||||
█.....█ █...........█
|
||||
█.....█ █████...........█
|
||||
█.....█ █...............█
|
||||
█.█████ █.███...........█
|
||||
█.█ █.█ █...........█
|
||||
█.█ █.█ █████████████
|
||||
█.█ █.█
|
||||
█.████ █.█
|
||||
█....█ █.█
|
||||
████.███████████████████.█
|
||||
█.....................█ █████████████████
|
||||
█.....................█ █...............█
|
||||
█.....................███████...............█
|
||||
█...........................................█
|
||||
█.....................███████...............█
|
||||
███████████████████████ █████████████████
|
8
main.py
8
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()
|
||||
|
11
resources/ascii_art.txt
Normal file
11
resources/ascii_art.txt
Normal file
@ -0,0 +1,11 @@
|
||||
██████ █████ █ ██ ██▓ ██▀███ ██▀███ ▓█████ ██▓ ▄▄▄▄ ▄▄▄ ▄▄▄█████▓▄▄▄█████▓ ██▓ ▓█████
|
||||
▒██ ▒ ▒██▓ ██▒ ██ ▓██▒▓██▒▓██ ▒ ██▒▓██ ▒ ██▒▓█ ▀ ▓██▒ ▓█████▄ ▒████▄ ▓ ██▒ ▓▒▓ ██▒ ▓▒▓██▒ ▓█ ▀
|
||||
░ ▓██▄ ▒██▒ ██░▓██ ▒██░▒██▒▓██ ░▄█ ▒▓██ ░▄█ ▒▒███ ▒██░ ▒██▒ ▄██▒██ ▀█▄ ▒ ▓██░ ▒░▒ ▓██░ ▒░▒██░ ▒███
|
||||
▒ ██▒░██ █▀ ░▓▓█ ░██░░██░▒██▀▀█▄ ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ▒██░█▀ ░██▄▄▄▄██░ ▓██▓ ░ ░ ▓██▓ ░ ▒██░ ▒▓█ ▄
|
||||
▒██████▒▒░▒███▒█▄ ▒▒█████▓ ░██░░██▓ ▒██▒░██▓ ▒██▒░▒████▒░██████▒ ░▓█ ▀█▓ ▓█ ▓██▒ ▒██▒ ░ ▒██▒ ░ ░██████▒░▒████▒
|
||||
▒ ▒▓▒ ▒ ░░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ▒▓ ░▒▓░░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░ ░▒▓███▀▒ ▒▒ ▓▒█░ ▒ ░░ ▒ ░░ ░ ▒░▓ ░░░ ▒░ ░
|
||||
░ ░▒ ░ ░ ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░▒ ░ ▒░ ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒░▒ ░ ▒ ▒▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░
|
||||
░ ░ ░ ░ ░ ░░░ ░ ░ ▒ ░ ░░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░
|
||||
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
||||
░
|
||||
|
17
resources/example_map.txt
Normal file
17
resources/example_map.txt
Normal file
@ -0,0 +1,17 @@
|
||||
####### #############
|
||||
#.....# #...........#
|
||||
#.....# #####...........#
|
||||
#.....# #...............#
|
||||
#.##### #.###...........#
|
||||
#.# #.# #...........#
|
||||
#.# #.# #############
|
||||
#.# #.#
|
||||
#.#### #.#
|
||||
#....# #.#
|
||||
####.###################.#
|
||||
#.....................# #################
|
||||
#.....................# #...............#
|
||||
#.....................#######...............#
|
||||
#...........................................#
|
||||
#.....................#######...............#
|
||||
####################### #################
|
Loading…
Reference in New Issue
Block a user