From 5cdb12e8a83a84ed6d83cfdee1d91ae5bea6ed08 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 17:32:26 +0100 Subject: [PATCH 1/7] Display a message on a popup --- squirrelbattle/display/display_manager.py | 14 +++++++++++- squirrelbattle/display/messagedisplay.py | 26 +++++++++++++++++++++++ squirrelbattle/game.py | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 squirrelbattle/display/messagedisplay.py diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index 061d8c0..869b7d4 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) 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..34c6586 --- /dev/null +++ b/squirrelbattle/display/messagedisplay.py @@ -0,0 +1,26 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + +from squirrelbattle.display.display import Box, Display + + +class MessageDisplay(Display): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.box = Box(*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) + 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..4553392 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -37,6 +37,7 @@ class Game: self.settings.write_settings() self.settings_menu.update_values(self.settings) self.logs = Logs() + self.message = "Vive les écureuils" def new_game(self) -> None: """ From b7f61d9485473398cd92eced6f76d2c9631e4398 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 17:35:51 +0100 Subject: [PATCH 2/7] Close popup if there is a message --- squirrelbattle/display/messagedisplay.py | 4 ++++ squirrelbattle/game.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py index 34c6586..9e4ffae 100644 --- a/squirrelbattle/display/messagedisplay.py +++ b/squirrelbattle/display/messagedisplay.py @@ -5,6 +5,10 @@ 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) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 4553392..4eb38c1 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -72,6 +72,10 @@ 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 + return + if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.MAINMENU: From fb8d8f033b5be942c9a8bed84ff903111189201f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 17:52:26 +0100 Subject: [PATCH 3/7] Popup border color is red --- squirrelbattle/display/display.py | 16 +++++++++++----- squirrelbattle/display/display_manager.py | 2 +- squirrelbattle/display/messagedisplay.py | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) 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 869b7d4..f7a0882 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -27,7 +27,7 @@ class DisplayManager: screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack) - self.messagedisplay = MessageDisplay(screen) + self.messagedisplay = MessageDisplay(screen=screen, pack=None) self.hbar = HorizontalSplit(screen, pack) self.vbar = VerticalSplit(screen, pack) self.displays = [self.statsdisplay, self.mapdisplay, diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py index 9e4ffae..c237b6b 100644 --- a/squirrelbattle/display/messagedisplay.py +++ b/squirrelbattle/display/messagedisplay.py @@ -1,5 +1,6 @@ # 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 @@ -12,7 +13,7 @@ class MessageDisplay(Display): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.box = Box(*args, **kwargs) + self.box = Box(fg_border_color=curses.COLOR_RED, *args, **kwargs) self.message = "" self.pad = self.newpad(1, 1) From be9c726fa02d8b014b7c765ad4303d8a1987b883 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 17:54:31 +0100 Subject: [PATCH 4/7] Display message in bold format --- squirrelbattle/display/messagedisplay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py index c237b6b..bcc2539 100644 --- a/squirrelbattle/display/messagedisplay.py +++ b/squirrelbattle/display/messagedisplay.py @@ -25,7 +25,7 @@ class MessageDisplay(Display): self.height + 2, self.width + 4) self.box.display() self.pad.erase() - self.addstr(self.pad, 0, 0, self.message) + 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) From 25ba94b9ac5a1fcbf0007e1f5434fee28c29b15e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 18:09:08 +0100 Subject: [PATCH 5/7] Game displays an error message when a save file could not be loaded. --- squirrelbattle/game.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 4eb38c1..28d20de 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,7 +37,7 @@ class Game: self.settings.write_settings() self.settings_menu.update_values(self.settings) self.logs = Logs() - self.message = "Vive les écureuils" + self.message = None def new_game(self) -> None: """ @@ -139,8 +139,15 @@ class Game: Loads the game from a dictionary """ self.map.load_state(d) - # noinspection PyTypeChecker - self.player = self.map.find_entities(Player)[0] + 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: @@ -150,7 +157,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: """ From 5faebfe556b31131a11924ea68d37643975564d8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 18:12:27 +0100 Subject: [PATCH 6/7] Test message display --- squirrelbattle/game.py | 1 + squirrelbattle/tests/game_test.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 28d20de..93af352 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -74,6 +74,7 @@ class Game: """ if self.message: self.message = None + self.display_actions(DisplayActions.REFRESH) return if self.state == GameMode.PLAY: diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 28b354d..a466fa9 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -4,6 +4,8 @@ import os import unittest +from squirrelbattle.enums import DisplayActions + from squirrelbattle.bootstrap import Bootstrap from squirrelbattle.display.display import Display from squirrelbattle.display.display_manager import DisplayManager @@ -292,3 +294,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) From b0e352444bdb13cb62d846d653f17a762d8c7fff Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 27 Nov 2020 18:16:54 +0100 Subject: [PATCH 7/7] Test loading wrong saves --- squirrelbattle/game.py | 10 +++++++++- squirrelbattle/tests/game_test.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 93af352..a64d82e 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -139,7 +139,15 @@ class Game: """ Loads the game from a dictionary """ - self.map.load_state(d) + 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" \ diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index a466fa9..5081912 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -4,6 +4,8 @@ import os import unittest +from squirrelbattle.resources import ResourceManager + from squirrelbattle.enums import DisplayActions from squirrelbattle.bootstrap import Bootstrap @@ -43,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,