Merge branch 'error-messages' into 'master'

Error messages

Closes #24 et #17

See merge request ynerant/squirrel-battle!29
This commit is contained in:
ynerant 2020-11-27 18:22:10 +01:00
commit ec4ac13231
5 changed files with 123 additions and 11 deletions

View File

@ -139,16 +139,22 @@ class HorizontalSplit(Display):
class Box(Display): class Box(Display):
def __init__(self, *args, **kwargs): def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols) 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: 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): for i in range(1, self.height - 1):
self.addstr(self.pad, i, 0, "") self.addstr(self.pad, i, 0, "", self.pair)
self.addstr(self.pad, i, self.width - 1, "") self.addstr(self.pad, i, self.width - 1, "", self.pair)
self.addstr(self.pad, self.height - 1, 0, 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.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1) self.y + self.height - 1, self.x + self.width - 1)

View File

@ -4,6 +4,7 @@
import curses import curses
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit from squirrelbattle.display.display import VerticalSplit, HorizontalSplit
from squirrelbattle.display.mapdisplay import MapDisplay from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.statsdisplay import StatsDisplay
from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \ from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \
MainMenuDisplay MainMenuDisplay
@ -26,11 +27,12 @@ class DisplayManager:
screen, pack) screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.logsdisplay = LogsDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack)
self.messagedisplay = MessageDisplay(screen=screen, pack=None)
self.hbar = HorizontalSplit(screen, pack) self.hbar = HorizontalSplit(screen, pack)
self.vbar = VerticalSplit(screen, pack) self.vbar = VerticalSplit(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay, self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay, self.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay] self.logsdisplay, self.messagedisplay]
self.update_game_components() self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None: def handle_display_action(self, action: DisplayActions) -> None:
@ -46,6 +48,7 @@ class DisplayManager:
self.statsdisplay.update_player(self.game.player) self.statsdisplay.update_player(self.game.player)
self.settingsmenudisplay.update_menu(self.game.settings_menu) self.settingsmenudisplay.update_menu(self.game.settings_menu)
self.logsdisplay.update_logs(self.game.logs) self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message)
def refresh(self) -> None: def refresh(self) -> None:
if self.game.state == GameMode.PLAY: if self.game.state == GameMode.PLAY:
@ -65,6 +68,15 @@ class DisplayManager:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
if self.game.state == GameMode.SETTINGS: if self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1) 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() self.resize_window()
def resize_window(self) -> bool: def resize_window(self) -> bool:

View File

@ -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)

View File

@ -1,6 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from json import JSONDecodeError
from random import randint from random import randint
from typing import Any, Optional from typing import Any, Optional
import json import json
@ -37,6 +37,7 @@ class Game:
self.settings.write_settings() self.settings.write_settings()
self.settings_menu.update_values(self.settings) self.settings_menu.update_values(self.settings)
self.logs = Logs() self.logs = Logs()
self.message = None
def new_game(self) -> None: def new_game(self) -> None:
""" """
@ -71,6 +72,11 @@ class Game:
Indicates what should be done when the given key is pressed, Indicates what should be done when the given key is pressed,
according to the current game state. according to the current game state.
""" """
if self.message:
self.message = None
self.display_actions(DisplayActions.REFRESH)
return
if self.state == GameMode.PLAY: if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key) self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU: elif self.state == GameMode.MAINMENU:
@ -133,9 +139,24 @@ class Game:
""" """
Loads the game from a dictionary Loads the game from a dictionary
""" """
self.map.load_state(d) try:
# noinspection PyTypeChecker self.map.load_state(d)
self.player = self.map.find_entities(Player)[0] 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) self.display_actions(DisplayActions.UPDATE)
def load_game(self) -> None: def load_game(self) -> None:
@ -145,7 +166,14 @@ class Game:
file_path = ResourceManager.get_config_path("save.json") file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path): if os.path.isfile(file_path):
with open(file_path, "r") as f: 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: def save_game(self) -> None:
""" """

View File

@ -4,6 +4,10 @@
import os import os
import unittest import unittest
from squirrelbattle.resources import ResourceManager
from squirrelbattle.enums import DisplayActions
from squirrelbattle.bootstrap import Bootstrap from squirrelbattle.bootstrap import Bootstrap
from squirrelbattle.display.display import Display from squirrelbattle.display.display import Display
from squirrelbattle.display.display_manager import DisplayManager from squirrelbattle.display.display_manager import DisplayManager
@ -41,6 +45,27 @@ class TestGame(unittest.TestCase):
new_state = self.game.save_state() new_state = self.game.save_state()
self.assertEqual(old_state, new_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: def test_bootstrap_fail(self) -> None:
""" """
Ensure that the test can't play the game, 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. Check that some functions are not implemented, only for coverage.
""" """
self.assertRaises(NotImplementedError, Display.display, None) 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)