Reworked graphics to make it more modular

This commit is contained in:
Nicolas Margulies 2020-11-10 18:08:06 +01:00
parent e522047c74
commit 17530f386c
9 changed files with 155 additions and 132 deletions

View File

@ -0,0 +1,14 @@
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:
game = Game()
game.new_game()
display = DisplayManager(term_manager.screen, game)
game.display_refresh = display.refresh
game.run(term_manager.screen)

View File

@ -1,57 +1,36 @@
import curses import curses
from typing import Any, Union from typing import Any, Union
from typing import Any
from dungeonbattle.tests.screen import FakePad from dungeonbattle.tests.screen import FakePad
from dungeonbattle.display.mapdisplay import MapDisplay
from dungeonbattle.display.statsdisplay import StatsDisplay
from dungeonbattle.display.menudisplay import MenuDisplay
from dungeonbattle.interfaces import Map
from dungeonbattle.entities.player import Player
class Display: class Display:
player: Player def __init__(self, screen: Any, pack: Any):
def __init__(self, screen: Any, m: Map, texture: Any):
self.screen = screen self.screen = screen
self.texture = texture self.pack = pack
self.map = m
self.mapdisplay = MapDisplay(self.map, self.texture, self.rows * 4//5, self.cols)
self.statsdisplay = StatsDisplay(self.rows//5, self.cols, self.rows * 4//5, 0)
def refresh(self, m : Map, p : Player) -> None:
self.map = m
self.player = p
self.mapdisplay.refresh(self.map, self.player)
self.statsdisplay.refresh(self.player)
# self.menudisplay.refresh(self.position)
def newpad(self, height: int, width: int) -> Union[FakePad, Any]: def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
return curses.newpad(height, width) if self.screen else FakePad() return curses.newpad(height, width) if self.screen else FakePad()
def ensure_resized(self, *pads) -> bool: def resize(self, y, x, height, width):
""" self.x = x
If the window got resized, ensure that the pads are also resized. self.y = y
""" self.width = width
y, x = self.screen.getmaxyx() if self.screen else (0, 0) self.height = height
for pad in pads:
pad.resize(y, x) def refresh(self, *args):
if self.screen and curses.is_term_resized(self.rows, self.cols): if len(args) == 4:
curses.resizeterm(y, x) self.resize(*args)
return True self.display()
return False
def display(self):
pass
@property @property
def rows(self) -> int: def rows(self) -> int:
return curses.LINES if self.screen else 42 return curses.LINES if self.screen else 42
@property
def height(self) -> int:
return self.rows
@property @property
def cols(self) -> int: def cols(self) -> int:
return curses.COLS if self.screen else 42 return curses.COLS if self.screen else 42
@property
def width(self) -> int:
return self.cols

View File

@ -0,0 +1,48 @@
import curses
from dungeonbattle.display.mapdisplay import MapDisplay
from dungeonbattle.display.statsdisplay import StatsDisplay
from dungeonbattle.display.texturepack import TexturePack
from typing import Any
from dungeonbattle.game import Game
class DisplayManager:
def __init__(self, screen: Any, g: Game):
self.game = g
self.screen = screen
self.mapdisplay = MapDisplay(screen, self.game.settings.TEXTURE_PACK)
self.statsdisplay = StatsDisplay(screen, self.game.settings.TEXTURE_PACK)
self.displays = [self.statsdisplay, self.mapdisplay]
self.update_game_components()
def update_game_components(self):
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:
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols)
self.statsdisplay.refresh(self.rows*4//5, 0, self.rows//5, self.cols)
# self.menudisplay.refresh(self.position)
def ensure_resized(self, *pads) -> bool:
"""
If the window got resized, ensure that the pads are also resized.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
for pad in pads:
pad.resize(y, x)
if self.screen and curses.is_term_resized(self.rows, self.cols):
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

@ -5,15 +5,15 @@ import curses
from dungeonbattle.display.texturepack import TexturePack from dungeonbattle.display.texturepack import TexturePack
from dungeonbattle.entities.player import Player from dungeonbattle.entities.player import Player
from dungeonbattle.interfaces import Map from dungeonbattle.interfaces import Map
from .display import Display
class MapDisplay(Display):
class MapDisplay:
player: Player player: Player
def __init__(self, m: Map, pack: TexturePack, height : int, width : int): def __init__(self, *args):
self.height = height super().__init__(*args)
self.width = width
self.pack = pack def update_map(self, m: Map):
self.map = m self.map = m
self.pad = curses.newpad(m.height, m.width + 1) self.pad = curses.newpad(m.height, m.width + 1)
@ -22,7 +22,8 @@ class MapDisplay:
for e in self.map.entities: for e in self.map.entities:
self.pad.addstr(e.y, e.x, self.pack.PLAYER) self.pad.addstr(e.y, e.x, self.pack.PLAYER)
def display(self, y, x) -> None: def display(self) -> None:
y, x = self.map.currenty, self.map.currentx
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1 deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
pminrow, pmincol = y - deltay, x - deltax pminrow, pmincol = y - deltay, x - deltax
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0) sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
@ -36,9 +37,3 @@ class MapDisplay:
self.pad.clear() self.pad.clear()
self.update_pad() self.update_pad()
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)
def refresh(self, m : Map, p : Player) -> None:
self.map = m
self.player = p
y, x = self.map.currenty, self.map.currentx
self.display(y,x)

View File

@ -1,69 +1,70 @@
from dungeonbattle.menus import Menu, MainMenu from dungeonbattle.menus import Menu, MainMenu
from typing import Any from typing import Any
from .display import Display
import curses import curses
class MenuDisplay: class MenuDisplay(Display):
position: int position: int
def __init__(self, menu : Menu, height : int, width : int, topleftx: int, toplefty: int) : def __init__(self, *args) :
super().__init__(*args)
self.menubox = self.newpad(self.height,self.width)
def update_menu(self, menu: Menu):
self.menu = menu
self.values = menu.values self.values = menu.values
self.width = width
self.height = height
self.trueheight = len(menu.values) self.trueheight = len(menu.values)
self.truewidth = max(map(len,self.values)) self.truewidth = max(map(len,self.values))
self.topleftx = topleftx
self.toplefty = toplefty
#Menu values are printed in pad #Menu values are printed in pad
self.pad = curses.newpad(self.trueheight,self.truewidth+1) self.pad = self.newpad(self.trueheight,self.truewidth+1)
for i in range(self.trueheight-1) : for i in range(self.trueheight-1) :
self.pad.addstr(i,0," " + self.values[i]) self.pad.addstr(i,0," " + self.values[i])
def update_pad(self) -> None:
for i in range(self.trueheight) :
self.pad.addstr(i,0," ")
self.pad.addstr(self.menu.position,0,">") #set a marker on the selected line
def display(self) -> None:
if self.height-2>=self.menu.position-1 :
cornery = 0
elif self.height-2 >= self.trueheight-self.menu.position :
cornery = self.trueheight-self.height+2
#Menu box #Menu box
self.menubox = curses.newpad(self.height,self.width)
self.menubox.addstr(0,0,""+""*(self.width-2)+"") self.menubox.addstr(0,0,""+""*(self.width-2)+"")
for i in range(1,self.height-2) : for i in range(1,self.height-2) :
self.menubox.addstr(i,0,""+" "*(self.width-2)+"") self.menubox.addstr(i,0,""+" "*(self.width-2)+"")
self.menubox.addstr(self.height-2, 0, ""+""*(self.width-2)+"") self.menubox.addstr(self.height-2, 0, ""+""*(self.width-2)+"")
def update_pad(self) -> None: self.menubox.refresh(0, 0, self.y, self.x,
for i in range(self.trueheight) : self.height + self.y,
self.pad.addstr(i,0," ") self.width + self.x)
self.pad.addstr(self.position,0,">") #set a marker on the selected line self.update_pad()
self.pad.refresh(cornery, 0, self.y+1, self.x+1,
self.height-2 + self.y,
self.width-2 + self.x)
def refresh(self, position : int) -> None: class MainMenuDisplay(Display):
self.position = position def __init__(self, menu : MainMenu, *args) :
if self.height-2>=position-1 : super().__init__(*args)
cornery = 0
elif self.height-2 >= self.trueheight-position :
cornery = self.trueheight-self.height+2
self.menubox.refresh(0, 0, self.toplefty, self.topleftx,
self.height + self.toplefty,
self.width + self.topleftx)
self.update_pad(position)
self.pad.refresh(cornery, 0, self.toplefty+1, self.topleftx+1,
self.height-2 + self.toplefty,
self.width-2 + self.topleftx)
class MainMenuDisplay:
def __init__(self, menu : MainMenu) :
self.menu = menu self.menu = menu
self.pad = curses.newpad(curses.LINES, curses.COLS) self.pad = self.newpad(self.rows, self.cols)
with open("ascii_art.txt", "r") as file: with open("ascii_art.txt", "r") as file:
title = file.read().split("\n") self.title = file.read().split("\n")
width = len(title[0]) self.menudisplay = MenuDisplay(self.screen)
height = len(title) self.menudisplay.update_menu(self.menu)
for i in range(len(title)) : def dislpay(self) -> None:
self.pad.addstr(4+i,curses.COLS//2-width//2-1,title[i]) for i in range(len(self.title)) :
self.pad.refresh(0,0,0,0,curses.LINES,curses.COLS) 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.width,self.height)
self.menudisplay = MenuDisplay(self.menu, 15, 15, height+8, curses.COLS//2-15//2-1) menuy, menux = len(self.title)+8, self.width//2-15//2-1
self.menudisplay.refresh(menuy, menux, min(15, self.rows-menuy), min(15,self.cols-menux))
def refresh(self, position) -> None:
self.menudisplay.refresh(position)

View File

@ -1,45 +1,43 @@
from typing import Any from typing import Any
from .display import Display
import curses import curses
from dungeonbattle.entities.player import Player from dungeonbattle.entities.player import Player
class StatsDisplay(): class StatsDisplay(Display):
player: Player player: Player
def __init__(self, height: int, width: int, def __init__(self, *args):
toplefty: int, topleftx: int): super().__init__(*args)
self.width = width self.pad = self.newpad(self.rows, self.cols)
self.height = height
self.topleftx = topleftx
self.toplefty = toplefty
self.pad = curses.newpad(height, width) def update_player(self, p: Player):
self.player = p
def update_pad(self) -> None: def update_pad(self) -> None:
string = "" string = ""
for i in range(self.width - 1): for _ in range(self.width - 1):
string = string + "-" string = string + "-"
self.pad.addstr(0, 0, string) self.pad.addstr(0, 0, string)
string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\ string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\
.format(self.player.level, self.player.current_xp, .format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health, self.player.max_xp, self.player.health,
self.player.maxhealth) self.player.maxhealth)
for i in range(self.width - len(string2) - 1): for _ in range(self.width - len(string2) - 1):
string2 = string2 + " " string2 = string2 + " "
self.pad.addstr(1, 0, string2) self.pad.addstr(1, 0, string2)
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\ string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
.format(self.player.strength, .format(self.player.strength,
self.player.intelligence, self.player.charisma, self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution) self.player.dexterity, self.player.constitution)
for i in range(self.width - len(string3) - 1): for _ in range(self.width - len(string3) - 1):
string3 = string3 + " " string3 = string3 + " "
self.pad.addstr(2, 0, string3) self.pad.addstr(2, 0, string3)
def refresh(self, p : Player) -> None: def display(self) -> None:
self.player = p
self.pad.clear() self.pad.clear()
self.update_pad() self.update_pad()
self.pad.refresh(0, 0, self.toplefty, self.topleftx, self.pad.refresh(0, 0, self.y, self.x,
2 + self.toplefty, 2 + self.y,
self.width + self.topleftx) self.width + self.x)

View File

@ -17,7 +17,7 @@ class TexturePack:
@classmethod @classmethod
def get_pack(cls, name: str) -> "TexturePack": def get_pack(cls, name: str) -> "TexturePack":
return cls._packs[name] return cls._packs[name.lower()]
TexturePack.ASCII_PACK = TexturePack( TexturePack.ASCII_PACK = TexturePack(

View File

@ -2,14 +2,12 @@ import sys
from typing import Any from typing import Any
from .display.display import Display from .display.display import Display
from .display.mapdisplay import MapDisplay
from .display.menudisplay import MenuDisplay
from .display.texturepack import TexturePack
from .entities.player import Player from .entities.player import Player
from .interfaces import Map from .interfaces import Map
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):
@ -31,21 +29,19 @@ class KeyValues(Enum):
class Game: class Game:
map: Map map: Map
player: Player player: Player
menu_display: MenuDisplay display_refresh: Callable[[], None]
map_display: MapDisplay
display: Display
def __init__(self) -> None: def __init__(self) -> None:
""" """
Init the game. Init the game.
""" """
self.state = GameMode.MAINMENU self.state = GameMode.PLAY
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, screen: Any) -> None: def new_game(self) -> None:
""" """
Create a new game on the screen. Create a new game on the screen.
""" """
@ -56,8 +52,6 @@ class Game:
self.player = Player() self.player = Player()
self.player.move(1, 6) self.player.move(1, 6)
self.map.add_entity(self.player) self.map.add_entity(self.player)
self.display = Display(screen, self.map, TexturePack.get_pack(self.settings.TEXTURE_PACK))
# self.menu_display = MenuDisplay(screen, self.main_menu, 0, 0)
@staticmethod @staticmethod
def load_game(filename: str) -> None: def load_game(filename: str) -> None:
@ -73,9 +67,10 @@ class Game:
while True: while True:
screen.clear() screen.clear()
screen.refresh() screen.refresh()
self.display.refresh(self.map, self.player) 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))
self.display_refresh()
def translate_key(self, key: str) -> KeyValues: def translate_key(self, key: str) -> KeyValues:
""" """
@ -109,7 +104,7 @@ class Game:
self.handle_key_pressed_main_menu(key) self.handle_key_pressed_main_menu(key)
elif self.state == GameMode.SETTINGS: elif self.state == GameMode.SETTINGS:
self.handle_key_pressed_settings(key) self.handle_key_pressed_settings(key)
self.display.refresh(self.map, self.player) self.display_refresh()
def handle_key_pressed_play(self, key: KeyValues) -> None: def handle_key_pressed_play(self, key: KeyValues) -> None:
""" """
@ -125,7 +120,6 @@ class Game:
self.player.move_right() self.player.move_right()
elif key == KeyValues.SPACE: elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.display = self.menu_display
def handle_key_pressed_main_menu(self, key: KeyValues) -> None: def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
""" """
@ -139,7 +133,6 @@ class Game:
option = self.main_menu.validate() option = self.main_menu.validate()
if option == menus.MainMenuValues.START: if option == menus.MainMenuValues.START:
self.state = GameMode.PLAY self.state = GameMode.PLAY
self.display = self.map_display
elif option == menus.MainMenuValues.SETTINGS: elif option == menus.MainMenuValues.SETTINGS:
self.state = GameMode.SETTINGS self.state = GameMode.SETTINGS
elif option == menus.MainMenuValues.EXIT: elif option == menus.MainMenuValues.EXIT:
@ -151,4 +144,3 @@ class Game:
""" """
if key == KeyValues.SPACE: if key == KeyValues.SPACE:
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.display = self.menu_display

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(term_manager.screen)
game.run(term_manager.screen)