diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index 00169f5..9cc1456 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -139,16 +139,22 @@ class HorizontalSplit(Display): class Box(Display): - def __init__(self, *args, **kwargs): + def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs): super().__init__(*args, **kwargs) self.pad = self.newpad(self.rows, self.cols) + self.fg_border_color = fg_border_color or curses.COLOR_WHITE + + pair_number = 4 + self.fg_border_color + self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK) + self.pair = self.color_pair(pair_number) def display(self) -> None: - self.addstr(self.pad, 0, 0, "┏" + "━" * (self.width - 2) + "┓") + self.addstr(self.pad, 0, 0, "┏" + "━" * (self.width - 2) + "┓", + self.pair) for i in range(1, self.height - 1): - self.addstr(self.pad, i, 0, "┃") - self.addstr(self.pad, i, self.width - 1, "┃") + self.addstr(self.pad, i, 0, "┃", self.pair) + self.addstr(self.pad, i, self.width - 1, "┃", self.pair) self.addstr(self.pad, self.height - 1, 0, - "┗" + "━" * (self.width - 2) + "┛") + "┗" + "━" * (self.width - 2) + "┛", self.pair) 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 061d8c0..f7a0882 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -4,6 +4,7 @@ import curses from squirrelbattle.display.display import VerticalSplit, HorizontalSplit from squirrelbattle.display.mapdisplay import MapDisplay +from squirrelbattle.display.messagedisplay import MessageDisplay from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \ MainMenuDisplay @@ -26,11 +27,12 @@ class DisplayManager: screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack) + self.messagedisplay = MessageDisplay(screen=screen, pack=None) self.hbar = HorizontalSplit(screen, pack) self.vbar = VerticalSplit(screen, pack) self.displays = [self.statsdisplay, self.mapdisplay, self.mainmenudisplay, self.settingsmenudisplay, - self.logsdisplay] + self.logsdisplay, self.messagedisplay] self.update_game_components() def handle_display_action(self, action: DisplayActions) -> None: @@ -46,6 +48,7 @@ class DisplayManager: self.statsdisplay.update_player(self.game.player) self.settingsmenudisplay.update_menu(self.game.settings_menu) self.logsdisplay.update_logs(self.game.logs) + self.messagedisplay.update_message(self.game.message) def refresh(self) -> None: if self.game.state == GameMode.PLAY: @@ -65,6 +68,15 @@ class DisplayManager: self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) if self.game.state == GameMode.SETTINGS: self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1) + + if self.game.message: + height, width = 0, 0 + for line in self.game.message.split("\n"): + height += 1 + width = max(width, len(line)) + y, x = (self.rows - height) // 2, (self.cols - width) // 2 + self.messagedisplay.refresh(y, x, height, width) + self.resize_window() def resize_window(self) -> bool: diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py new file mode 100644 index 0000000..bcc2539 --- /dev/null +++ b/squirrelbattle/display/messagedisplay.py @@ -0,0 +1,31 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later +import curses + +from squirrelbattle.display.display import Box, Display + + +class MessageDisplay(Display): + """ + Display a message in a popup. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.box = Box(fg_border_color=curses.COLOR_RED, *args, **kwargs) + self.message = "" + self.pad = self.newpad(1, 1) + + def update_message(self, msg: str) -> None: + self.message = msg + + def display(self) -> None: + self.box.refresh(self.y - 1, self.x - 2, + self.height + 2, self.width + 4) + self.box.display() + self.pad.erase() + self.addstr(self.pad, 0, 0, self.message, curses.A_BOLD) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.height + self.y - 1, + self.width + self.x - 1) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index c60d93f..a64d82e 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -1,6 +1,6 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later - +from json import JSONDecodeError from random import randint from typing import Any, Optional import json @@ -37,6 +37,7 @@ class Game: self.settings.write_settings() self.settings_menu.update_values(self.settings) self.logs = Logs() + self.message = None def new_game(self) -> None: """ @@ -71,6 +72,11 @@ class Game: Indicates what should be done when the given key is pressed, according to the current game state. """ + if self.message: + self.message = None + self.display_actions(DisplayActions.REFRESH) + return + if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.MAINMENU: @@ -133,9 +139,24 @@ class Game: """ Loads the game from a dictionary """ - self.map.load_state(d) - # noinspection PyTypeChecker - self.player = self.map.find_entities(Player)[0] + try: + self.map.load_state(d) + except KeyError: + self.message = "Some keys are missing in your save file.\n" \ + "Your save seems to be corrupt. It got deleted." + os.unlink(ResourceManager.get_config_path("save.json")) + self.display_actions(DisplayActions.UPDATE) + return + + players = self.map.find_entities(Player) + if not players: + self.message = "No player was found on this map!\n" \ + "Maybe you died?" + self.player.health = 0 + self.display_actions(DisplayActions.UPDATE) + return + + self.player = players[0] self.display_actions(DisplayActions.UPDATE) def load_game(self) -> None: @@ -145,7 +166,14 @@ class Game: file_path = ResourceManager.get_config_path("save.json") if os.path.isfile(file_path): with open(file_path, "r") as f: - self.load_state(json.loads(f.read())) + try: + state = json.loads(f.read()) + self.load_state(state) + except JSONDecodeError: + self.message = "The JSON file is not correct.\n" \ + "Your save seems corrupted. It got deleted." + os.unlink(file_path) + self.display_actions(DisplayActions.UPDATE) def save_game(self) -> None: """ diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 28b354d..5081912 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -4,6 +4,10 @@ import os import unittest +from squirrelbattle.resources import ResourceManager + +from squirrelbattle.enums import DisplayActions + from squirrelbattle.bootstrap import Bootstrap from squirrelbattle.display.display import Display from squirrelbattle.display.display_manager import DisplayManager @@ -41,6 +45,27 @@ class TestGame(unittest.TestCase): new_state = self.game.save_state() self.assertEqual(old_state, new_state) + # Error on loading save + with open(ResourceManager.get_config_path("save.json"), "w") as f: + f.write("I am not a JSON file") + self.assertIsNone(self.game.message) + self.game.load_game() + self.assertIsNotNone(self.game.message) + self.game.message = None + + with open(ResourceManager.get_config_path("save.json"), "w") as f: + f.write("{}") + self.assertIsNone(self.game.message) + self.game.load_game() + self.assertIsNotNone(self.game.message) + self.game.message = None + + # Load game with a dead player + self.game.map.remove_entity(self.game.player) + self.game.save_game() + self.game.load_game() + self.assertIsNotNone(self.game.message) + def test_bootstrap_fail(self) -> None: """ Ensure that the test can't play the game, @@ -292,3 +317,13 @@ class TestGame(unittest.TestCase): Check that some functions are not implemented, only for coverage. """ self.assertRaises(NotImplementedError, Display.display, None) + + def test_messages(self) -> None: + """ + Display error messages. + """ + self.game.message = "I am an error" + self.game.display_actions(DisplayActions.UPDATE) + self.game.display_actions(DisplayActions.REFRESH) + self.game.handle_key_pressed(None, "random key") + self.assertIsNone(self.game.message)