diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index 0cdba98..bd16344 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -19,6 +19,24 @@ class Display: def newpad(self, height: int, width: int) -> Union[FakePad, Any]: return curses.newpad(height, width) if self.screen else FakePad() + def truncate(self, msg: str, height: int, width: int) -> str: + height = max(0, height) + width = max(0, width) + lines = msg.split("\n") + lines = lines[:height] + lines = [line[:width] for line in lines] + return "\n".join(lines) + + def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None: + """ + Display a message onto the pad. + If the message is too large, it is truncated vertically and horizontally + """ + height, width = pad.getmaxyx() + msg = self.truncate(msg, height - y, width - x - 1) + if msg.replace("\n", "") and x >= 0 and y >= 0: + return pad.addstr(y, x, msg, *options) + def init_pair(self, number: int, foreground: int, background: int) -> None: return curses.init_pair(number, foreground, background) \ if self.screen else None @@ -32,7 +50,8 @@ class Display: self.y = y self.width = width self.height = height - if hasattr(self, "pad") and resize_pad: + if hasattr(self, "pad") and resize_pad and \ + self.height >= 0 and self.width >= 0: self.pad.resize(self.height + 1, self.width + 1) def refresh(self, *args, resize_pad: bool = True) -> None: @@ -40,6 +59,27 @@ class Display: self.resize(*args, resize_pad) self.display() + def refresh_pad(self, pad: Any, top_y: int, top_x: int, + window_y: int, window_x: int, + last_y: int, last_x: int) -> None: + """ + Refresh a pad on a part of the window. + The refresh starts at coordinates (top_y, top_x) from the pad, + and is drawn from (window_y, window_x) to (last_y, last_x). + If coordinates are invalid (negative indexes/length..., then nothing + is drawn and no error is raised. + """ + top_y, top_x = max(0, top_y), max(0, top_x) + window_y, window_x = max(0, window_y), max(0, window_x) + screen_max_y, screen_max_x = self.screen.getmaxyx() if self.screen \ + else 42, 42 + last_y, last_x = min(screen_max_y - 1, last_y), \ + min(screen_max_x - 1, last_x) + + if last_y >= window_y and last_x >= window_x: + # Refresh the pad only if coordinates are valid + pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x) + def display(self) -> None: raise NotImplementedError @@ -68,8 +108,9 @@ class VerticalSplit(Display): def display(self) -> None: for i in range(self.height): - self.pad.addstr(i, 0, "┃") - self.pad.refresh(0, 0, self.y, self.x, self.y + self.height - 1, self.x) + self.addstr(self.pad, i, 0, "┃") + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.x) class HorizontalSplit(Display): @@ -88,8 +129,9 @@ class HorizontalSplit(Display): def display(self) -> None: for i in range(self.width): - self.pad.addstr(0, i, "━") - self.pad.refresh(0, 0, self.y, self.x, self.y, self.x + self.width - 1) + self.addstr(self.pad, 0, i, "━") + self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y, + self.x + self.width - 1) class Box(Display): @@ -99,10 +141,11 @@ class Box(Display): self.pad = self.newpad(self.rows, self.cols) def display(self) -> None: - self.pad.addstr(0, 0, "┏" + "━" * (self.width - 2) + "┓") + self.addstr(self.pad, 0, 0, "┏" + "━" * (self.width - 2) + "┓") for i in range(1, self.height - 1): - self.pad.addstr(i, 0, "┃") - self.pad.addstr(i, self.width - 1, "┃") - self.pad.addstr(self.height - 1, 0, "┗" + "━" * (self.width - 2) + "┛") - self.pad.refresh(0, 0, self.y, self.x, self.y + self.height - 1, - self.x + self.width - 1) + self.addstr(self.pad, i, 0, "┃") + self.addstr(self.pad, i, self.width - 1, "┃") + self.addstr(self.pad, self.height - 1, 0, + "┗" + "━" * (self.width - 2) + "┛") + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.x + self.width - 1) diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index d50dfa1..2c7baf1 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -48,7 +48,10 @@ class DisplayManager: if self.game.state == GameMode.PLAY: # The map pad has already the good size self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, - self.cols * 4 // 5, resize_pad=False) + self.mapdisplay.pack.tile_width + * (self.cols * 4 // 5 + // self.mapdisplay.pack.tile_width), + resize_pad=False) self.statsdisplay.refresh(0, self.cols * 4 // 5 + 1, self.rows, self.cols // 5 - 1) self.logsdisplay.refresh(self.rows * 4 // 5 + 1, 0, diff --git a/squirrelbattle/display/logsdisplay.py b/squirrelbattle/display/logsdisplay.py index 368c036..7bed61e 100644 --- a/squirrelbattle/display/logsdisplay.py +++ b/squirrelbattle/display/logsdisplay.py @@ -12,12 +12,11 @@ class LogsDisplay(Display): self.logs = logs def display(self) -> None: - print(type(self.logs.messages), flush=True) messages = self.logs.messages[-self.height:] messages = messages[::-1] - self.pad.clear() + self.pad.erase() for i in range(min(self.height, len(messages))): - self.pad.addstr(self.height - i - 1, self.x, - messages[i][:self.width]) - self.pad.refresh(0, 0, self.y, self.x, self.y + self.height - 1, - self.x + self.width - 1) + self.addstr(self.pad, self.height - i - 1, self.x, + messages[i][:self.width]) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.x + self.width - 1) diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index e6cc278..9599a54 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -15,11 +15,11 @@ class MapDisplay(Display): def update_pad(self) -> None: self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color) self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color) - self.pad.addstr(0, 0, self.map.draw_string(self.pack), - self.color_pair(1)) + self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack), + self.color_pair(1)) for e in self.map.entities: - self.pad.addstr(e.y, self.pack.tile_width * e.x, - self.pack[e.name.upper()], self.color_pair(2)) + self.addstr(self.pad, e.y, self.pack.tile_width * e.x, + self.pack[e.name.upper()], self.color_pair(2)) def display(self) -> None: y, x = self.map.currenty, self.pack.tile_width * self.map.currentx @@ -31,9 +31,18 @@ class MapDisplay(Display): smaxrow = min(smaxrow, self.height - 1) smaxcol = self.pack.tile_width * self.map.width - \ (x + deltax) + self.width - 1 + + # Wrap perfectly the map according to the width of the tiles + pmincol = self.pack.tile_width * (pmincol // self.pack.tile_width) + smincol = self.pack.tile_width * (smincol // self.pack.tile_width) + smaxcol = self.pack.tile_width \ + * (smaxcol // self.pack.tile_width + 1) - 1 + smaxcol = min(smaxcol, self.width - 1) pminrow = max(0, min(self.map.height, pminrow)) pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol)) - self.pad.clear() + + self.pad.erase() self.update_pad() - self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) + self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow, + smaxcol) diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index 3c4e5a1..b04ac37 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -20,13 +20,13 @@ class MenuDisplay(Display): # 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]) + self.addstr(self.pad, i, 0, " " + self.values[i]) def update_pad(self) -> None: for i in range(self.trueheight): - self.pad.addstr(i, 0, " " + self.values[i]) + self.addstr(self.pad, i, 0, " " + self.values[i]) # set a marker on the selected line - self.pad.addstr(self.menu.position, 0, ">") + self.addstr(self.pad, self.menu.position, 0, ">") def display(self) -> None: cornery = 0 if self.height - 2 >= self.menu.position - 1 \ @@ -35,9 +35,9 @@ class MenuDisplay(Display): # Menu box self.menubox.refresh(self.y, self.x, self.height, self.width) - self.pad.clear() + self.pad.erase() self.update_pad() - self.pad.refresh(cornery, 0, self.y + 1, self.x + 2, + self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2, self.height - 2 + self.y, self.width - 2 + self.x) @@ -79,9 +79,10 @@ class MainMenuDisplay(Display): def display(self) -> None: for i in range(len(self.title)): - self.pad.addstr(4 + i, max(self.width // 2 - - len(self.title[0]) // 2 - 1, 0), self.title[i]) - self.pad.refresh(0, 0, self.y, self.x, self.height + self.y - 1, + self.addstr(self.pad, 4 + i, max(self.width // 2 + - len(self.title[0]) // 2 - 1, 0), self.title[i]) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.height + self.y - 1, self.width + self.x - 1) menuwidth = min(self.menudisplay.preferred_width, self.width) menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1 diff --git a/squirrelbattle/display/statsdisplay.py b/squirrelbattle/display/statsdisplay.py index f47862e..d33f55c 100644 --- a/squirrelbattle/display/statsdisplay.py +++ b/squirrelbattle/display/statsdisplay.py @@ -21,24 +21,24 @@ class StatsDisplay(Display): .format(self.player.level, self.player.current_xp, self.player.max_xp, self.player.health, self.player.maxhealth) - self.pad.addstr(0, 0, string2) + self.addstr(self.pad, 0, 0, string2) string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\ .format(self.player.strength, self.player.intelligence, self.player.charisma, self.player.dexterity, self.player.constitution) - self.pad.addstr(3, 0, string3) + self.addstr(self.pad, 3, 0, string3) inventory_str = "Inventaire : " + "".join( self.pack[item.name.upper()] for item in self.player.inventory) - self.pad.addstr(8, 0, inventory_str) + self.addstr(self.pad, 8, 0, inventory_str) if self.player.dead: - self.pad.addstr(10, 0, "VOUS ÊTES MORT", - curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT - | self.color_pair(3)) + self.addstr(self.pad, 10, 0, "VOUS ÊTES MORT", + curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT + | self.color_pair(3)) def display(self) -> None: - self.pad.clear() + self.pad.erase() self.update_pad() - self.pad.refresh(0, 0, self.y, self.x, + self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y + self.height - 1, self.width + self.x - 1) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 0bb3024..dce1165 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -55,7 +55,7 @@ class Game: when the given key gets pressed. """ while True: # pragma no cover - screen.clear() + screen.erase() screen.refresh() self.display_actions(DisplayActions.REFRESH) key = screen.getkey() diff --git a/squirrelbattle/tests/screen.py b/squirrelbattle/tests/screen.py index 6eb2cd0..a6b3c9a 100644 --- a/squirrelbattle/tests/screen.py +++ b/squirrelbattle/tests/screen.py @@ -1,3 +1,6 @@ +from typing import Tuple + + class FakePad: """ In order to run tests, we simulate a fake curses pad that accepts functions @@ -10,8 +13,11 @@ class FakePad: smincol: int, smaxrow: int, smaxcol: int) -> None: pass - def clear(self) -> None: + def erase(self) -> None: pass def resize(self, height: int, width: int) -> None: pass + + def getmaxyx(self) -> Tuple[int, int]: + return 42, 42