Merge branch 'display' into 'master'
Meilleur affichage See merge request ynerant/dungeon-battle!8
This commit is contained in:
commit
3b3b8ee8da
|
@ -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,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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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):
|
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()
|
||||||
|
|
|
@ -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,18 +88,41 @@ 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:
|
||||||
|
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:
|
if key == KeyValues.UP:
|
||||||
self.player.move_up()
|
self.player.move_up()
|
||||||
if key == KeyValues.DOWN:
|
elif key == KeyValues.DOWN:
|
||||||
self.player.move_down()
|
self.player.move_down()
|
||||||
if key == KeyValues.LEFT:
|
elif key == KeyValues.LEFT:
|
||||||
self.player.move_left()
|
self.player.move_left()
|
||||||
if key == KeyValues.RIGHT:
|
elif key == KeyValues.RIGHT:
|
||||||
self.player.move_right()
|
self.player.move_right()
|
||||||
if self.state == GameMode.MAINMENU:
|
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:
|
if key == KeyValues.DOWN:
|
||||||
self.main_menu.go_down()
|
self.main_menu.go_down()
|
||||||
if key == KeyValues.UP:
|
if key == KeyValues.UP:
|
||||||
|
@ -92,3 +135,10 @@ class Game:
|
||||||
self.state = GameMode.SETTINGS
|
self.state = GameMode.SETTINGS
|
||||||
elif option == menus.MainMenuValues.EXIT:
|
elif option == menus.MainMenuValues.EXIT:
|
||||||
sys.exit(0)
|
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
|
#!/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__()
|
||||||
|
|
|
@ -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
|
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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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_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')
|
||||||
|
|
|
@ -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
|
#!/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)
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
██████ █████ █ ██ ██▓ ██▀███ ██▀███ ▓█████ ██▓ ▄▄▄▄ ▄▄▄ ▄▄▄█████▓▄▄▄█████▓ ██▓ ▓█████
|
||||||
|
▒██ ▒ ▒██▓ ██▒ ██ ▓██▒▓██▒▓██ ▒ ██▒▓██ ▒ ██▒▓█ ▀ ▓██▒ ▓█████▄ ▒████▄ ▓ ██▒ ▓▒▓ ██▒ ▓▒▓██▒ ▓█ ▀
|
||||||
|
░ ▓██▄ ▒██▒ ██░▓██ ▒██░▒██▒▓██ ░▄█ ▒▓██ ░▄█ ▒▒███ ▒██░ ▒██▒ ▄██▒██ ▀█▄ ▒ ▓██░ ▒░▒ ▓██░ ▒░▒██░ ▒███
|
||||||
|
▒ ██▒░██ █▀ ░▓▓█ ░██░░██░▒██▀▀█▄ ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ▒██░█▀ ░██▄▄▄▄██░ ▓██▓ ░ ░ ▓██▓ ░ ▒██░ ▒▓█ ▄
|
||||||
|
▒██████▒▒░▒███▒█▄ ▒▒█████▓ ░██░░██▓ ▒██▒░██▓ ▒██▒░▒████▒░██████▒ ░▓█ ▀█▓ ▓█ ▓██▒ ▒██▒ ░ ▒██▒ ░ ░██████▒░▒████▒
|
||||||
|
▒ ▒▓▒ ▒ ░░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ▒▓ ░▒▓░░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░ ░▒▓███▀▒ ▒▒ ▓▒█░ ▒ ░░ ▒ ░░ ░ ▒░▓ ░░░ ▒░ ░
|
||||||
|
░ ░▒ ░ ░ ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░▒ ░ ▒░ ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒░▒ ░ ▒ ▒▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░
|
||||||
|
░ ░ ░ ░ ░ ░░░ ░ ░ ▒ ░ ░░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░
|
||||||
|
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
||||||
|
░
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
####### #############
|
||||||
|
#.....# #...........#
|
||||||
|
#.....# #####...........#
|
||||||
|
#.....# #...............#
|
||||||
|
#.##### #.###...........#
|
||||||
|
#.# #.# #...........#
|
||||||
|
#.# #.# #############
|
||||||
|
#.# #.#
|
||||||
|
#.#### #.#
|
||||||
|
#....# #.#
|
||||||
|
####.###################.#
|
||||||
|
#.....................# #################
|
||||||
|
#.....................# #...............#
|
||||||
|
#.....................#######...............#
|
||||||
|
#...........................................#
|
||||||
|
#.....................#######...............#
|
||||||
|
####################### #################
|
Loading…
Reference in New Issue