Merge branch 'master' into 'ladders'

# Conflicts:
#   squirrelbattle/game.py
#   squirrelbattle/interfaces.py
#   squirrelbattle/tests/game_test.py
This commit is contained in:
ynerant 2021-01-06 17:29:26 +01:00
commit d49c138257
27 changed files with 649 additions and 236 deletions

View File

@ -0,0 +1,44 @@
┃|┃
┃|┃ ▓▓▒ ▓▓
┃|┃ ▓▓ ▓▓▒
┃|┃ ▓▓▓ ▓▓ ▓▓▓ ▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▬█▓▓▓▓▓▓▬█▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓░██░░▓▓░░██░▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
━━▓▓▓▓━━ ▓▓░░░░░░░░ ░░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▓ ▓░░░░░░░░░░░░░░░░░░░░▓▓▒▒▒▒▒▒▒▒▒▒▒▒
┃ ▓▓▓▓▓ ▓░░░░░░░░▄▄▄▄░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒
┃ ▓▓▓▓▓ ▓▓░░░░░░░░░░░░░░░▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓ ▓▓▓▓░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▓▒▒░░░░░░░░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▒░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒
▓▒▒░░░░░░░░░░░░▓▓▓▓▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
▓▓▒░░░░░░░░░░░░░░░▓▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒
▓▓▒▒░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒
▓▓▓▒░░░░░░░░░░░░░▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▒▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓ ░
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓ ░░
▓▓▓▓▓▓▓▓▒▒░░░▒▒▒▒░░░░░░▓▓░▒▒▒▓▓▓▓▓▓▓▓▓▓░░░ ░
▓▓▓▓▓▓▓▒░░░░░░░░░▒░░░░░░░░░░░░▒▒▒▓▓▓▓▓▓▓▓░░ ░░▒
░ ░░░░░▒░░░░░░▒░░░▒░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░▒
▒▒░░▓▓░░▒░░░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░░░░░▒░░░░░▒ ░░
▒▒▒▒▒▓▒▒▓░░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░
▒▒█▒█▒▒▒▓░░▒░░░░░░░░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒█▒▒▒▒░░░░▒░░░▒░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░▒░░░
▓█▒▒▒▒█▒█▒▒▒▒░░▒░░░░░▒░░░░▒░░░░░░░░░░░░░░░░░▒░░░░▒░░░░░░░▒░░░░░▒▒
██▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░▒░░░░░░▒░░░░░▒░░░░░
▒▒▒▒█▒▒▒▒▒▒▒░░░░░░░░░░▒░░░░░░░░░░▒░░░░░░░░░░░▒░░░░░░░░░░░░░░░
▒▒█▒▒▒▒▒░▒░▒░░░░▓▓▓░░░░░░░▒░░░░▒░░░▒░░░░░░░▓▓░░░░░░░░░░░░ ░
▒▒▒▒▒▒▒░▒░░░▓▓▓▓▓▓░░░░░░░▒░░░░░░░░▒░░░░▓▓▓▓▓▓░░░░░░░░ ░
░▓▓▓▓▓▓░░░░░░▒░░░░░░░░▒░░░░░░▓▓▓▓▓░░░ ░ ░░

View File

@ -0,0 +1,97 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from ..display.display import Box, Display
from ..game import Game
from ..resources import ResourceManager
from ..translations import gettext as _
class CreditsDisplay(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.box = Box(*args, **kwargs)
self.pad = self.newpad(1, 1)
self.ascii_art_displayed = False
def update(self, game: Game) -> None:
return
def display(self) -> None:
self.box.refresh(self.y, self.x, self.height, self.width)
self.box.display()
self.pad.erase()
messages = [
_("Credits"),
"",
"Squirrel Battle",
"",
_("Developers:"),
"Yohann \"ÿnérant\" D'ANELLO",
"Mathilde \"eichhornchen\" DÉPRÉS",
"Nicolas \"nicomarg\" MARGULIES",
"Charles \"charsle\" PEYRAT",
"",
_("Translators:"),
"Hugo \"ifugao\" JACOB (español)",
]
for i, msg in enumerate(messages):
self.addstr(self.pad, i + (self.height - len(messages)) // 2,
(self.width - len(msg)) // 2, msg,
bold=(i == 0), italic=(":" in msg))
if self.ascii_art_displayed:
self.display_ascii_art()
self.refresh_pad(self.pad, 0, 0, self.y + 1, self.x + 1,
self.height + self.y - 2,
self.width + self.x - 2)
def display_ascii_art(self) -> None:
with open(ResourceManager.get_asset_path("ascii-art-ecureuil.txt"))\
as f:
ascii_art = f.read().split("\n")
height, width = len(ascii_art), len(ascii_art[0])
y_offset, x_offset = (self.height - height) // 2,\
(self.width - width) // 2
for i, line in enumerate(ascii_art):
for j, c in enumerate(line):
bg_color = curses.COLOR_WHITE
fg_color = curses.COLOR_BLACK
bold = False
if c == ' ':
bg_color = curses.COLOR_BLACK
elif c == '' or c == '' or c == '':
bold = True
fg_color = curses.COLOR_WHITE
bg_color = curses.COLOR_BLACK
elif c == '|':
bold = True # c = '┃'
fg_color = (100, 700, 1000)
bg_color = curses.COLOR_BLACK
elif c == '':
fg_color = (700, 300, 0)
elif c == '':
fg_color = (700, 300, 0)
bg_color = curses.COLOR_BLACK
elif c == '':
fg_color = (350, 150, 0)
elif c == '':
fg_color = (0, 0, 0)
bg_color = curses.COLOR_BLACK
elif c == '':
c = ''
fg_color = (1000, 1000, 1000)
bg_color = curses.COLOR_BLACK
self.addstr(self.pad, y_offset + i, x_offset + j, c,
fg_color, bg_color, bold=bold)
def handle_click(self, y: int, x: int, game: Game) -> None:
if self.pad.inch(y - 1, x - 1) != ord(" "):
self.ascii_art_displayed = True

View File

@ -24,9 +24,16 @@ class Display:
self.pack = pack or TexturePack.get_pack("ascii") self.pack = pack or TexturePack.get_pack("ascii")
def newpad(self, height: int, width: int) -> Union[FakePad, Any]: def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
"""
Overwrites the native curses function of the same name.
"""
return curses.newpad(height, width) if self.screen else FakePad() return curses.newpad(height, width) if self.screen else FakePad()
def truncate(self, msg: str, height: int, width: int) -> str: def truncate(self, msg: str, height: int, width: int) -> str:
"""
Truncates a string into a string adapted to the width and height of
the screen.
"""
height = max(0, height) height = max(0, height)
width = max(0, width) width = max(0, width)
lines = msg.split("\n") lines = msg.split("\n")
@ -36,8 +43,8 @@ class Display:
def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int: def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int:
""" """
Translate a tuple (R, G, B) into a curses color index. Translates a tuple (R, G, B) into a curses color index.
If we have already a color index, then nothing is processed. If we already have a color index, then nothing is processed.
If this is a tuple, we construct a new color index if non-existing If this is a tuple, we construct a new color index if non-existing
and we return this index. and we return this index.
The values of R, G and B must be between 0 and 1000, and not The values of R, G and B must be between 0 and 1000, and not
@ -66,9 +73,9 @@ class Display:
low: bool = False, right: bool = False, top: bool = False, low: bool = False, right: bool = False, top: bool = False,
vertical: bool = False, chartext: bool = False) -> None: vertical: bool = False, chartext: bool = False) -> None:
""" """
Display a message onto the pad. Displays a message onto the pad.
If the message is too large, it is truncated vertically and horizontally If the message is too large, it is truncated vertically and horizontally
The text can be bold, italic, blinking, ... if the good parameters are The text can be bold, italic, blinking, ... if the right parameters are
given. These parameters are translated into curses attributes. given. These parameters are translated into curses attributes.
The foreground and background colors can be given as curses constants The foreground and background colors can be given as curses constants
(curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to (curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to
@ -126,6 +133,9 @@ class Display:
def resize(self, y: int, x: int, height: int, width: int, def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None: resize_pad: bool = True) -> None:
"""
Resizes a pad.
"""
self.x = x self.x = x
self.y = y self.y = y
self.width = width self.width = width
@ -136,6 +146,9 @@ class Display:
self.pad.resize(self.height + 1, self.width + 1) self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None: def refresh(self, *args, resize_pad: bool = True) -> None:
"""
Refreshes a pad
"""
if len(args) == 4: if len(args) == 4:
self.resize(*args, resize_pad) self.resize(*args, resize_pad)
self.display() self.display()
@ -144,10 +157,10 @@ class Display:
window_y: int, window_x: int, window_y: int, window_x: int,
last_y: int, last_x: int) -> None: last_y: int, last_x: int) -> None:
""" """
Refresh a pad on a part of the window. Refreshes a pad on a part of the window.
The refresh starts at coordinates (top_y, top_x) from the pad, 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). and is drawn from (window_y, window_x) to (last_y, last_x).
If coordinates are invalid (negative indexes/length..., then nothing If coordinates are invalid (negative indexes/length...), then nothing
is drawn and no error is raised. is drawn and no error is raised.
""" """
top_y, top_x = max(0, top_y), max(0, top_x) top_y, top_x = max(0, top_y), max(0, top_x)
@ -177,7 +190,7 @@ class Display:
def handle_click(self, y: int, x: int, game: Game) -> None: def handle_click(self, y: int, x: int, game: Game) -> None:
""" """
A mouse click was performed on the coordinates (y, x) of the pad. A mouse click was performed on the coordinates (y, x) of the pad.
Maybe it can do something. Maybe it should do something.
""" """
pass pass
@ -191,7 +204,9 @@ class Display:
class VerticalSplit(Display): class VerticalSplit(Display):
"""
A class to split the screen in two vertically with a pretty line.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, 1) self.pad = self.newpad(self.rows, 1)
@ -212,7 +227,9 @@ class VerticalSplit(Display):
class HorizontalSplit(Display): class HorizontalSplit(Display):
"""
A class to split the screen in two horizontally with a pretty line.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pad = self.newpad(1, self.cols) self.pad = self.newpad(1, self.cols)
@ -233,6 +250,9 @@ class HorizontalSplit(Display):
class Box(Display): class Box(Display):
"""
A class for pretty boxes to print menus and other content.
"""
title: str = "" title: str = ""
def update_title(self, title: str) -> None: def update_title(self, title: str) -> None:

View File

@ -2,6 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from squirrelbattle.display.creditsdisplay import CreditsDisplay
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit, \ from squirrelbattle.display.display import VerticalSplit, HorizontalSplit, \
Display Display
from squirrelbattle.display.mapdisplay import MapDisplay from squirrelbattle.display.mapdisplay import MapDisplay
@ -30,17 +32,21 @@ class DisplayManager:
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu, self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack) screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.messagedisplay = MessageDisplay(screen=screen, pack=None) self.messagedisplay = MessageDisplay(screen, pack)
self.hbar = HorizontalSplit(screen, pack) self.hbar = HorizontalSplit(screen, pack)
self.vbar = VerticalSplit(screen, pack) self.vbar = VerticalSplit(screen, pack)
self.creditsdisplay = CreditsDisplay(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.messagedisplay, self.logsdisplay, self.messagedisplay,
self.playerinventorydisplay, self.playerinventorydisplay,
self.storeinventorydisplay] self.storeinventorydisplay, self.creditsdisplay]
self.update_game_components() self.update_game_components()
def handle_display_action(self, action: DisplayActions, *params) -> None: def handle_display_action(self, action: DisplayActions, *params) -> None:
"""
Handles the differents values of display action.
"""
if action == DisplayActions.REFRESH: if action == DisplayActions.REFRESH:
self.refresh() self.refresh()
elif action == DisplayActions.UPDATE: elif action == DisplayActions.UPDATE:
@ -58,6 +64,9 @@ class DisplayManager:
d.update(self.game) d.update(self.game)
def handle_mouse_click(self, y: int, x: int) -> None: def handle_mouse_click(self, y: int, x: int) -> None:
"""
Handles the mouse clicks.
"""
displays = self.refresh() displays = self.refresh()
display = None display = None
for d in displays: for d in displays:
@ -70,6 +79,9 @@ class DisplayManager:
display.handle_click(y - display.y, x - display.x, self.game) display.handle_click(y - display.y, x - display.x, self.game)
def refresh(self) -> List[Display]: def refresh(self) -> List[Display]:
"""
Refreshes all components on the screen.
"""
displays = [] displays = []
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK) pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
@ -118,6 +130,9 @@ class DisplayManager:
elif self.game.state == GameMode.SETTINGS: elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols) self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.settingsmenudisplay) displays.append(self.settingsmenudisplay)
elif self.game.state == GameMode.CREDITS:
self.creditsdisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.creditsdisplay)
if self.game.message: if self.game.message:
height, width = 0, 0 height, width = 0, 0
@ -135,7 +150,7 @@ class DisplayManager:
def resize_window(self) -> bool: def resize_window(self) -> bool:
""" """
If the window got resized, ensure that the screen size got updated. When the window is resized, ensures that the screen size is updated.
""" """
y, x = self.screen.getmaxyx() if self.screen else (0, 0) y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows, if self.screen and curses.is_term_resized(self.rows,
@ -146,8 +161,16 @@ class DisplayManager:
@property @property
def rows(self) -> int: def rows(self) -> int:
"""
Overwrites the native curses attribute of the same name,
for testing purposes.
"""
return curses.LINES if self.screen else 42 return curses.LINES if self.screen else 42
@property @property
def cols(self) -> int: def cols(self) -> int:
"""
Overwrites the native curses attribute of the same name,
for testing purposes.
"""
return curses.COLS if self.screen else 42 return curses.COLS if self.screen else 42

View File

@ -7,6 +7,10 @@ from squirrelbattle.interfaces import Logs
class LogsDisplay(Display): class LogsDisplay(Display):
"""
A class to handle the display of the logs.
"""
logs: Logs logs: Logs
def __init__(self, *args) -> None: def __init__(self, *args) -> None:

View File

@ -7,6 +7,10 @@ from ..game import Game
class MapDisplay(Display): class MapDisplay(Display):
"""
A class to handle the display of the map.
"""
map: Map map: Map
def __init__(self, *args): def __init__(self, *args):

View File

@ -16,7 +16,7 @@ from ..translations import gettext as _
class MenuDisplay(Display): class MenuDisplay(Display):
""" """
A class to display the menu objects A class to display the menu objects.
""" """
menu: Menu menu: Menu
position: int position: int
@ -80,7 +80,7 @@ class MenuDisplay(Display):
class SettingsMenuDisplay(MenuDisplay): class SettingsMenuDisplay(MenuDisplay):
""" """
A class to display specifically a settingsmenu object A class to display specifically a settingsmenu object.
""" """
menu: SettingsMenu menu: SettingsMenu
@ -98,7 +98,7 @@ class SettingsMenuDisplay(MenuDisplay):
class MainMenuDisplay(Display): class MainMenuDisplay(Display):
""" """
A class to display specifically a mainmenu object A class to display specifically a mainmenu object.
""" """
def __init__(self, menu: MainMenu, *args): def __init__(self, menu: MainMenu, *args):
super().__init__(*args) super().__init__(*args)
@ -120,6 +120,8 @@ class MainMenuDisplay(Display):
self.addstr(self.pad, 4 + i, max(self.width // 2 self.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i], - len(self.title[0]) // 2 - 1, 0), self.title[i],
self.fg_color) self.fg_color)
msg = _("Credits")
self.addstr(self.pad, self.height - 1, self.width - 1 - len(msg), msg)
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1, self.height + self.y - 1,
self.width + self.x - 1) self.width + self.x - 1)
@ -143,8 +145,14 @@ class MainMenuDisplay(Display):
if y <= len(self.title): if y <= len(self.title):
self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000) self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000)
if y == self.height - 1 and x >= self.width - 1 - len(_("Credits")):
game.state = GameMode.CREDITS
class PlayerInventoryDisplay(MenuDisplay): class PlayerInventoryDisplay(MenuDisplay):
"""
A class to handle the display of the player's inventory.
"""
player: Player = None player: Player = None
selected: bool = True selected: bool = True
store_mode: bool = False store_mode: bool = False
@ -191,6 +199,9 @@ class PlayerInventoryDisplay(MenuDisplay):
class StoreInventoryDisplay(MenuDisplay): class StoreInventoryDisplay(MenuDisplay):
"""
A class to handle the display of a merchant's inventory.
"""
menu: StoreMenu menu: StoreMenu
selected: bool = False selected: bool = False

View File

@ -8,7 +8,7 @@ from squirrelbattle.game import Game
class MessageDisplay(Display): class MessageDisplay(Display):
""" """
Display a message in a popup. A class to handle the display of popup messages.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -10,6 +10,9 @@ from .display import Display
class StatsDisplay(Display): class StatsDisplay(Display):
"""
A class to handle the display of the stats of the player.
"""
player: Player player: Player
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -6,6 +6,9 @@ from typing import Any
class TexturePack: class TexturePack:
"""
A class to handle displaying several textures.
"""
_packs = dict() _packs = dict()
name: str name: str
@ -29,6 +32,7 @@ class TexturePack:
SWORD: str SWORD: str
TEDDY_BEAR: str TEDDY_BEAR: str
TIGER: str TIGER: str
TRUMPET: str
WALL: str WALL: str
ASCII_PACK: "TexturePack" ASCII_PACK: "TexturePack"
@ -75,6 +79,7 @@ TexturePack.ASCII_PACK = TexturePack(
SWORD='\u2020', SWORD='\u2020',
TEDDY_BEAR='8', TEDDY_BEAR='8',
TIGER='n', TIGER='n',
TRUMPET='/',
WALL='#', WALL='#',
) )
@ -102,5 +107,6 @@ TexturePack.SQUIRREL_PACK = TexturePack(
SWORD='🗡️ ', SWORD='🗡️ ',
TEDDY_BEAR='🧸', TEDDY_BEAR='🧸',
TIGER='🐅', TIGER='🐅',
TRUMPET='🎺',
WALL='🧱', WALL='🧱',
) )

View File

@ -1,17 +1,18 @@
from ..interfaces import FriendlyEntity, InventoryHolder from ..interfaces import FriendlyEntity, InventoryHolder, Map, FightingEntity
from ..translations import gettext as _ from ..translations import gettext as _
from .player import Player from .player import Player
from .monsters import Monster
from .items import Item from .items import Item
from random import choice from random import choice, shuffle
class Merchant(InventoryHolder, FriendlyEntity): class Merchant(InventoryHolder, FriendlyEntity):
""" """
The class for merchants in the dungeon The class of merchants in the dungeon.
""" """
def keys(self) -> list: def keys(self) -> list:
""" """
Returns a friendly entitie's specific attributes Returns a friendly entitie's specific attributes.
""" """
return super().keys() + ["inventory", "hazel"] return super().keys() + ["inventory", "hazel"]
@ -20,7 +21,6 @@ class Merchant(InventoryHolder, FriendlyEntity):
super().__init__(name=name, *args, **kwargs) super().__init__(name=name, *args, **kwargs)
self.inventory = self.translate_inventory(inventory or []) self.inventory = self.translate_inventory(inventory or [])
self.hazel = hazel self.hazel = hazel
if not self.inventory: if not self.inventory:
for i in range(5): for i in range(5):
self.inventory.append(choice(Item.get_all_items())()) self.inventory.append(choice(Item.get_all_items())())
@ -28,20 +28,20 @@ class Merchant(InventoryHolder, FriendlyEntity):
def talk_to(self, player: Player) -> str: def talk_to(self, player: Player) -> str:
""" """
This function is used to open the merchant's inventory in a menu, This function is used to open the merchant's inventory in a menu,
and allow the player to buy/sell objects and allows the player to buy/sell objects.
""" """
return _("I don't sell any squirrel") return _("I don't sell any squirrel")
def change_hazel_balance(self, hz: int) -> None: def change_hazel_balance(self, hz: int) -> None:
""" """
Change the number of hazel the merchant has by hz. Changes the number of hazel the merchant has by hz.
""" """
self.hazel += hz self.hazel += hz
class Sunflower(FriendlyEntity): class Sunflower(FriendlyEntity):
""" """
A friendly sunflower A friendly sunflower.
""" """
def __init__(self, maxhealth: int = 15, def __init__(self, maxhealth: int = 15,
*args, **kwargs) -> None: *args, **kwargs) -> None:
@ -49,4 +49,80 @@ class Sunflower(FriendlyEntity):
@property @property
def dialogue_option(self) -> list: def dialogue_option(self) -> list:
"""
Lists all that a sunflower can say to the player.
"""
return [_("Flower power!!"), _("The sun is warm today")] return [_("Flower power!!"), _("The sun is warm today")]
class Familiar(FightingEntity):
"""
A friendly familiar that helps the player defeat monsters.
"""
def __init__(self, maxhealth: int = 25,
*args, **kwargs) -> None:
super().__init__(maxhealth=maxhealth, *args, **kwargs)
self.target = None
# @property
# def dialogue_option(self) -> list:
# """
# Debug function (to see if used in the real game)
# """
# return [_("My target is"+str(self.target))]
def act(self, p: Player, m: Map) -> None:
"""
By default, the familiar tries to stay at distance at most 2 of the
player and if a monster comes in range 3, it focuses on the monster
and attacks it.
"""
if self.target is None:
# If the previous target is dead(or if there was no previous target)
# the familiar tries to get closer to the player.
self.target = p
elif self.target.dead:
self.target = p
if self.target == p:
# Look for monsters around the player to kill TOFIX : if monster is
# out of range, continue targetting player.
for entity in m.entities:
if (p.y - entity.y) ** 2 + (p.x - entity.x) ** 2 <= 9 and\
isinstance(entity, Monster):
self.target = entity
entity.paths = dict() # Allows the paths to be calculated.
break
# Familiars move according to a Dijkstra algorithm
# that targets their target.
# If they can not move and are already close to their target,
# they hit, except if their target is the player.
if self.target and (self.y, self.x) in self.target.paths:
# Moves to target player by choosing the best available path
for next_y, next_x in self.target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
break
if self.distance_squared(self.target) <= 1 and \
not isinstance(self.target, Player):
self.map.logs.add_message(self.hit(self.target))
break
else:
# Moves in a random direction
# If the direction is not available, tries another one
moves = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
for move in moves:
if move():
break
class Trumpet(Familiar):
"""
A class of familiars.
"""
def __init__(self, name: str = "trumpet", strength: int = 3,
maxhealth: int = 20, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -11,7 +11,7 @@ from ..translations import gettext as _
class Item(Entity): class Item(Entity):
""" """
A class for items A class for items.
""" """
held: bool held: bool
held_by: Optional[InventoryHolder] held_by: Optional[InventoryHolder]
@ -27,7 +27,7 @@ class Item(Entity):
def drop(self) -> None: def drop(self) -> None:
""" """
The item is dropped from the inventory onto the floor The item is dropped from the inventory onto the floor.
""" """
if self.held: if self.held:
self.held_by.inventory.remove(self) self.held_by.inventory.remove(self)
@ -48,7 +48,7 @@ class Item(Entity):
def hold(self, player: InventoryHolder) -> None: def hold(self, player: InventoryHolder) -> None:
""" """
The item is taken from the floor and put into the inventory The item is taken from the floor and put into the inventory.
""" """
self.held = True self.held = True
self.held_by = player self.held_by = player
@ -57,7 +57,7 @@ class Item(Entity):
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the state of the entity into a dictionary Saves the state of the item into a dictionary.
""" """
d = super().save_state() d = super().save_state()
d["held"] = self.held d["held"] = self.held
@ -65,13 +65,16 @@ class Item(Entity):
@staticmethod @staticmethod
def get_all_items() -> list: def get_all_items() -> list:
"""
Returns the list of all item classes.
"""
return [BodySnatchPotion, Bomb, Heart, Sword] return [BodySnatchPotion, Bomb, Heart, Sword]
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool: def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool:
""" """
Does all necessary actions when an object is to be sold. Does all necessary actions when an object is to be sold.
Is overwritten by some classes that cannot exist in the player's Is overwritten by some classes that cannot exist in the player's
inventory inventory.
""" """
if buyer.hazel >= self.price: if buyer.hazel >= self.price:
self.hold(buyer) self.hold(buyer)
@ -85,7 +88,7 @@ class Item(Entity):
class Heart(Item): class Heart(Item):
""" """
A heart item to return health to the player A heart item to return health to the player.
""" """
healing: int healing: int
@ -96,14 +99,15 @@ class Heart(Item):
def hold(self, entity: InventoryHolder) -> None: def hold(self, entity: InventoryHolder) -> None:
""" """
When holding a heart, heal the player and don't put item in inventory. When holding a heart, the player is healed and
the item is not put in the inventory.
""" """
entity.health = min(entity.maxhealth, entity.health + self.healing) entity.health = min(entity.maxhealth, entity.health + self.healing)
entity.map.remove_entity(self) entity.map.remove_entity(self)
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the state of the header into a dictionary Saves the state of the heart into a dictionary.
""" """
d = super().save_state() d = super().save_state()
d["healing"] = self.healing d["healing"] = self.healing
@ -129,7 +133,7 @@ class Bomb(Item):
def use(self) -> None: def use(self) -> None:
""" """
When the bomb is used, throw it and explodes it. When the bomb is used, it is thrown and then it explodes.
""" """
if self.held: if self.held:
self.owner = self.held_by self.owner = self.held_by
@ -138,7 +142,7 @@ class Bomb(Item):
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
""" """
Special exploding action of the bomb Special exploding action of the bomb.
""" """
if self.exploding: if self.exploding:
if self.tick > 0: if self.tick > 0:
@ -164,7 +168,7 @@ class Bomb(Item):
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the state of the bomb into a dictionary Saves the state of the bomb into a dictionary.
""" """
d = super().save_state() d = super().save_state()
d["exploding"] = self.exploding d["exploding"] = self.exploding
@ -181,13 +185,13 @@ class Explosion(Item):
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
""" """
The explosion instant dies. The bomb disappears after exploding.
""" """
m.remove_entity(self) m.remove_entity(self)
def hold(self, player: InventoryHolder) -> None: def hold(self, player: InventoryHolder) -> None:
""" """
The player can't hold any explosion. The player can't hold an explosion.
""" """
pass pass

View File

@ -10,8 +10,8 @@ from ..interfaces import FightingEntity, Map
class Monster(FightingEntity): class Monster(FightingEntity):
""" """
The class for all monsters in the dungeon. The class for all monsters in the dungeon.
A monster must override this class, and the parameters are given All specific monster classes overwrite this class,
in the __init__ function. and the parameters are given in the __init__ function.
An example of the specification of a monster that has a strength of 4 An example of the specification of a monster that has a strength of 4
and 20 max HP: and 20 max HP:
@ -21,7 +21,7 @@ class Monster(FightingEntity):
super().__init__(name="my_monster", strength=strength, super().__init__(name="my_monster", strength=strength,
maxhealth=maxhealth, *args, **kwargs) maxhealth=maxhealth, *args, **kwargs)
With that way, attributes can be overwritten when the entity got created. With that way, attributes can be overwritten when the entity is created.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -29,7 +29,7 @@ class Monster(FightingEntity):
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
""" """
By default, a monster will move randomly where it is possible By default, a monster will move randomly where it is possible
And if a player is close to the monster, the monster run on the player. If the player is closeby, the monster runs to the player.
""" """
target = None target = None
for entity in m.entities: for entity in m.entities:
@ -38,12 +38,12 @@ class Monster(FightingEntity):
target = entity target = entity
break break
# A Dijkstra algorithm has ran that targets the player. # Monsters move according to a Dijkstra algorithm
# With that way, monsters can simply follow the path. # that targets the player.
# If they can't move and they are already close to the player, # If they can not move and are already close to the player,
# They hit. # they hit.
if target and (self.y, self.x) in target.paths: if target and (self.y, self.x) in target.paths:
# Move to target player by choosing the best avaliable path # Moves to target player by choosing the best available path
for next_y, next_x in target.paths[(self.y, self.x)]: for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True) moved = self.check_move(next_y, next_x, True)
if moved: if moved:
@ -52,8 +52,8 @@ class Monster(FightingEntity):
self.map.logs.add_message(self.hit(target)) self.map.logs.add_message(self.hit(target))
break break
else: else:
# Move in a random direction # Moves in a random direction
# If the direction is not available, try another one # If the direction is not available, tries another one
moves = [self.move_up, self.move_down, moves = [self.move_up, self.move_down,
self.move_left, self.move_right] self.move_left, self.move_right]
shuffle(moves) shuffle(moves)
@ -61,10 +61,17 @@ class Monster(FightingEntity):
if move(): if move():
break break
def move(self, y: int, x: int) -> None:
"""
Overwrites the move function to recalculate paths.
"""
super().move(y, x)
self.recalculate_paths()
class Tiger(Monster): class Tiger(Monster):
""" """
A tiger monster A tiger monster.
""" """
def __init__(self, name: str = "tiger", strength: int = 2, def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None: maxhealth: int = 20, *args, **kwargs) -> None:
@ -74,7 +81,7 @@ class Tiger(Monster):
class Hedgehog(Monster): class Hedgehog(Monster):
""" """
A really mean hedgehog monster A really mean hedgehog monster.
""" """
def __init__(self, name: str = "hedgehog", strength: int = 3, def __init__(self, name: str = "hedgehog", strength: int = 3,
maxhealth: int = 10, *args, **kwargs) -> None: maxhealth: int = 10, *args, **kwargs) -> None:
@ -84,7 +91,7 @@ class Hedgehog(Monster):
class Rabbit(Monster): class Rabbit(Monster):
""" """
A rabbit monster A rabbit monster.
""" """
def __init__(self, name: str = "rabbit", strength: int = 1, def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, *args, **kwargs) -> None: maxhealth: int = 15, *args, **kwargs) -> None:
@ -94,7 +101,7 @@ class Rabbit(Monster):
class TeddyBear(Monster): class TeddyBear(Monster):
""" """
A cute teddybear monster A cute teddybear monster.
""" """
def __init__(self, name: str = "teddy_bear", strength: int = 0, def __init__(self, name: str = "teddy_bear", strength: int = 0,
maxhealth: int = 50, *args, **kwargs) -> None: maxhealth: int = 50, *args, **kwargs) -> None:

View File

@ -1,21 +1,17 @@
# 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 functools import reduce
from queue import PriorityQueue
from random import randint from random import randint
from typing import Dict, Tuple
from ..interfaces import FightingEntity, InventoryHolder from ..interfaces import FightingEntity, InventoryHolder
class Player(InventoryHolder, FightingEntity): class Player(InventoryHolder, FightingEntity):
""" """
The class of the player The class of the player.
""" """
current_xp: int = 0 current_xp: int = 0
max_xp: int = 10 max_xp: int = 10
paths: Dict[Tuple[int, int], Tuple[int, int]]
def __init__(self, name: str = "player", maxhealth: int = 20, def __init__(self, name: str = "player", maxhealth: int = 20,
strength: int = 5, intelligence: int = 1, charisma: int = 1, strength: int = 5, intelligence: int = 1, charisma: int = 1,
@ -45,7 +41,7 @@ class Player(InventoryHolder, FightingEntity):
def level_up(self) -> None: def level_up(self) -> None:
""" """
Add levels to the player as much as it is possible. Add as many levels as possible to the player.
""" """
while self.current_xp > self.max_xp: while self.current_xp > self.max_xp:
self.level += 1 self.level += 1
@ -59,8 +55,8 @@ class Player(InventoryHolder, FightingEntity):
def add_xp(self, xp: int) -> None: def add_xp(self, xp: int) -> None:
""" """
Add some experience to the player. Adds some experience to the player.
If the required amount is reached, level up. If the required amount is reached, the player levels up.
""" """
self.current_xp += xp self.current_xp += xp
self.level_up() self.level_up()
@ -87,56 +83,6 @@ class Player(InventoryHolder, FightingEntity):
entity.hold(self) entity.hold(self)
return super().check_move(y, x, move_if_possible) return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths for monsters to go to
the player. Actually, the paths are computed for each tile adjacent to
the player then for each step the monsters use the best path avaliable.
"""
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the state of the entity into a dictionary Saves the state of the entity into a dictionary

View File

@ -21,20 +21,20 @@ class DisplayActions(Enum):
class GameMode(Enum): class GameMode(Enum):
""" """
Game mode options Game mode options.
""" """
MAINMENU = auto() MAINMENU = auto()
PLAY = auto() PLAY = auto()
SETTINGS = auto() SETTINGS = auto()
INVENTORY = auto() INVENTORY = auto()
STORE = auto() STORE = auto()
CREDITS = auto()
class KeyValues(Enum): class KeyValues(Enum):
""" """
Key values options used in the game Key values options used in the game.
""" """
MOUSE = auto()
UP = auto() UP = auto()
DOWN = auto() DOWN = auto()
LEFT = auto() LEFT = auto()
@ -52,7 +52,7 @@ class KeyValues(Enum):
@staticmethod @staticmethod
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]: def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
""" """
Translate the raw string key into an enum value that we can use. Translates the raw string key into an enum value that we can use.
""" """
if key in (settings.KEY_DOWN_SECONDARY, if key in (settings.KEY_DOWN_SECONDARY,
settings.KEY_DOWN_PRIMARY): settings.KEY_DOWN_PRIMARY):

View File

@ -31,7 +31,7 @@ class Game:
def __init__(self) -> None: def __init__(self) -> None:
""" """
Init the game. Initiates the game.
""" """
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.waiting_for_friendly_key = False self.waiting_for_friendly_key = False
@ -50,7 +50,7 @@ class Game:
def new_game(self) -> None: def new_game(self) -> None:
""" """
Create a new game on the screen. Creates a new game on the screen.
""" """
# TODO generate a new map procedurally # TODO generate a new map procedurally
self.maps = [] self.maps = []
@ -85,8 +85,8 @@ class Game:
def run(self, screen: Any) -> None: # pragma no cover def run(self, screen: Any) -> None: # pragma no cover
""" """
Main infinite loop. Main infinite loop.
We wait for the player's action, then we do what that should be done We wait for the player's action, then we do what should be done
when the given key gets pressed. when a key gets pressed.
""" """
screen.refresh() screen.refresh()
while True: while True:
@ -105,7 +105,7 @@ class Game:
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\ def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None: -> None:
""" """
Indicates what should be done when the given key is pressed, Indicates what should be done when a given key is pressed,
according to the current game state. according to the current game state.
""" """
if self.message: if self.message:
@ -127,6 +127,8 @@ class Game:
self.settings_menu.handle_key_pressed(key, raw_key, self) self.settings_menu.handle_key_pressed(key, raw_key, self)
elif self.state == GameMode.STORE: elif self.state == GameMode.STORE:
self.handle_key_pressed_store(key) self.handle_key_pressed_store(key)
elif self.state == GameMode.CREDITS:
self.state = GameMode.MAINMENU
self.display_actions(DisplayActions.REFRESH) self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None: def handle_key_pressed_play(self, key: KeyValues) -> None:
@ -135,16 +137,16 @@ class Game:
""" """
if key == KeyValues.UP: if key == KeyValues.UP:
if self.player.move_up(): if self.player.move_up():
self.map.tick() self.map.tick(self.player)
elif key == KeyValues.DOWN: elif key == KeyValues.DOWN:
if self.player.move_down(): if self.player.move_down():
self.map.tick() self.map.tick(self.player)
elif key == KeyValues.LEFT: elif key == KeyValues.LEFT:
if self.player.move_left(): if self.player.move_left():
self.map.tick() self.map.tick(self.player)
elif key == KeyValues.RIGHT: elif key == KeyValues.RIGHT:
if self.player.move_right(): if self.player.move_right():
self.map.tick() self.map.tick(self.player)
elif key == KeyValues.INVENTORY: elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY self.state = GameMode.INVENTORY
elif key == KeyValues.SPACE: elif key == KeyValues.SPACE:
@ -205,8 +207,9 @@ class Game:
def handle_friendly_entity_chat(self, key: KeyValues) -> None: def handle_friendly_entity_chat(self, key: KeyValues) -> None:
""" """
If the player is talking to a friendly entity, we get the direction If the player tries to talk to a friendly entity, the game waits for
where the entity is, then we interact with it. a directional key to be pressed, verifies there is a friendly entity
in that direction and then lets the player interact with it.
""" """
if not self.waiting_for_friendly_key: if not self.waiting_for_friendly_key:
return return
@ -295,7 +298,7 @@ class Game:
def handle_key_pressed_main_menu(self, key: KeyValues) -> None: def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
""" """
In the main menu, we can navigate through options. In the main menu, we can navigate through different options.
""" """
if key == KeyValues.DOWN: if key == KeyValues.DOWN:
self.main_menu.go_down() self.main_menu.go_down()
@ -320,13 +323,13 @@ class Game:
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the game to a dictionary Saves the game to a dictionary.
""" """
return self.map.save_state() return self.map.save_state()
def load_state(self, d: dict) -> None: def load_state(self, d: dict) -> None:
""" """
Loads the game from a dictionary Loads the game from a dictionary.
""" """
try: try:
self.map.load_state(d) self.map.load_state(d)
@ -350,7 +353,7 @@ class Game:
def load_game(self) -> None: def load_game(self) -> None:
""" """
Loads the game from a file Loads the game from a file.
""" """
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):
@ -367,7 +370,7 @@ class Game:
def save_game(self) -> None: def save_game(self) -> None:
""" """
Saves the game to a file Saves the game to a file.
""" """
with open(ResourceManager.get_config_path("save.json"), "w") as f: with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write(json.dumps(self.save_state())) f.write(json.dumps(self.save_state()))

View File

@ -4,7 +4,9 @@
from enum import Enum, auto from enum import Enum, auto
from math import sqrt from math import sqrt
from random import choice, randint from random import choice, randint
from typing import List, Optional, Any, Tuple from typing import List, Optional, Any, Dict, Tuple
from queue import PriorityQueue
from functools import reduce
from .display.texturepack import TexturePack from .display.texturepack import TexturePack
from .translations import gettext as _ from .translations import gettext as _
@ -12,7 +14,7 @@ from .translations import gettext as _
class Logs: class Logs:
""" """
The logs object stores the messages to display. It is encapsulating a list The logs object stores the messages to display. It encapsulates a list
of such messages, to allow multiple pointers to keep track of it even if of such messages, to allow multiple pointers to keep track of it even if
the list was to be reassigned. the list was to be reassigned.
""" """
@ -32,7 +34,7 @@ class Logs:
class Map: class Map:
""" """
Object that represents a Map with its width, height The Map object represents a with its width, height
and tiles, that have their custom properties. and tiles, that have their custom properties.
""" """
floor: int floor: int
@ -61,14 +63,17 @@ class Map:
def add_entity(self, entity: "Entity") -> None: def add_entity(self, entity: "Entity") -> None:
""" """
Register a new entity in the map. Registers a new entity in the map.
""" """
if entity.is_familiar():
self.entities.insert(1, entity)
else:
self.entities.append(entity) self.entities.append(entity)
entity.map = self entity.map = self
def remove_entity(self, entity: "Entity") -> None: def remove_entity(self, entity: "Entity") -> None:
""" """
Unregister an entity from the map. Unregisters an entity from the map.
""" """
if entity in self.entities: if entity in self.entities:
self.entities.remove(entity) self.entities.remove(entity)
@ -88,7 +93,7 @@ class Map:
def entity_is_present(self, y: int, x: int) -> bool: def entity_is_present(self, y: int, x: int) -> bool:
""" """
Indicates that the tile at the coordinates (y, x) contains a killable Indicates that the tile at the coordinates (y, x) contains a killable
entity entity.
""" """
return 0 <= y < self.height and 0 <= x < self.width and \ return 0 <= y < self.height and 0 <= x < self.width and \
any(entity.x == x and entity.y == y and entity.is_friendly() any(entity.x == x and entity.y == y and entity.is_friendly()
@ -97,7 +102,8 @@ class Map:
@staticmethod @staticmethod
def load(filename: str) -> "Map": def load(filename: str) -> "Map":
""" """
Read a file that contains the content of a map, and build a Map object. Reads a file that contains the content of a map,
and builds a Map object.
""" """
with open(filename, "r") as f: with open(filename, "r") as f:
file = f.read() file = f.read()
@ -106,7 +112,7 @@ class Map:
@staticmethod @staticmethod
def load_from_string(content: str) -> "Map": def load_from_string(content: str) -> "Map":
""" """
Load a map represented by its characters and build a Map object. Loads a map represented by its characters and builds a Map object.
""" """
lines = content.split("\n") lines = content.split("\n")
first_line = lines[0] first_line = lines[0]
@ -122,7 +128,7 @@ class Map:
@staticmethod @staticmethod
def load_dungeon_from_string(content: str) -> List[List["Tile"]]: def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
""" """
Transforms a string into the list of corresponding tiles Transforms a string into the list of corresponding tiles.
""" """
lines = content.split("\n") lines = content.split("\n")
tiles = [[Tile.from_ascii_char(c) tiles = [[Tile.from_ascii_char(c)
@ -131,7 +137,7 @@ class Map:
def draw_string(self, pack: TexturePack) -> str: def draw_string(self, pack: TexturePack) -> str:
""" """
Draw the current map as a string object that can be rendered Draws the current map as a string object that can be rendered
in the window. in the window.
""" """
return "\n".join("".join(tile.char(pack) for tile in line) return "\n".join("".join(tile.char(pack) for tile in line)
@ -139,7 +145,7 @@ class Map:
def spawn_random_entities(self, count: int) -> None: def spawn_random_entities(self, count: int) -> None:
""" """
Put randomly {count} entities on the map, where it is available. Puts randomly {count} entities on the map, only on empty ground tiles.
""" """
for ignored in range(count): for ignored in range(count):
while True: while True:
@ -151,16 +157,19 @@ class Map:
entity.move(y, x) entity.move(y, x)
self.add_entity(entity) self.add_entity(entity)
def tick(self) -> None: def tick(self, p: Any) -> None:
""" """
Trigger all entity events. Triggers all entity events.
""" """
for entity in self.entities: for entity in self.entities:
if entity.is_familiar():
entity.act(p, self)
else:
entity.act(self) entity.act(self)
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the map's attributes to a dictionary Saves the map's attributes to a dictionary.
""" """
d = dict() d = dict()
d["width"] = self.width d["width"] = self.width
@ -177,7 +186,7 @@ class Map:
def load_state(self, d: dict) -> None: def load_state(self, d: dict) -> None:
""" """
Loads the map's attributes from a dictionary Loads the map's attributes from a dictionary.
""" """
self.width = d["width"] self.width = d["width"]
self.height = d["height"] self.height = d["height"]
@ -194,7 +203,7 @@ class Map:
class Tile(Enum): class Tile(Enum):
""" """
The internal representation of the tiles of the map The internal representation of the tiles of the map.
""" """
EMPTY = auto() EMPTY = auto()
WALL = auto() WALL = auto()
@ -204,7 +213,7 @@ class Tile(Enum):
@staticmethod @staticmethod
def from_ascii_char(ch: str) -> "Tile": def from_ascii_char(ch: str) -> "Tile":
""" """
Maps an ascii character to its equivalent in the texture pack Maps an ascii character to its equivalent in the texture pack.
""" """
for tile in Tile: for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch: if tile.char(TexturePack.ASCII_PACK) == ch:
@ -214,7 +223,7 @@ class Tile(Enum):
def char(self, pack: TexturePack) -> str: def char(self, pack: TexturePack) -> str:
""" """
Translates a Tile to the corresponding character according Translates a Tile to the corresponding character according
to the texture pack to the texture pack.
""" """
val = getattr(pack, self.name) val = getattr(pack, self.name)
return val[0] if isinstance(val, tuple) else val return val[0] if isinstance(val, tuple) else val
@ -241,19 +250,20 @@ class Tile(Enum):
def can_walk(self) -> bool: def can_walk(self) -> bool:
""" """
Check if an entity (player or not) can move in this tile. Checks if an entity (player or not) can move in this tile.
""" """
return not self.is_wall() and self != Tile.EMPTY return not self.is_wall() and self != Tile.EMPTY
class Entity: class Entity:
""" """
An Entity object represents any entity present on the map An Entity object represents any entity present on the map.
""" """
y: int y: int
x: int x: int
name: str name: str
map: Map map: Map
paths: Dict[Tuple[int, int], Tuple[int, int]]
# noinspection PyShadowingBuiltins # noinspection PyShadowingBuiltins
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None, def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
@ -262,11 +272,12 @@ class Entity:
self.x = x self.x = x
self.name = name self.name = name
self.map = map self.map = map
self.paths = None
def check_move(self, y: int, x: int, move_if_possible: bool = False)\ def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool: -> bool:
""" """
Checks if moving to (y,x) is authorized Checks if moving to (y,x) is authorized.
""" """
free = self.map.is_free(y, x) free = self.map.is_free(y, x)
if free and move_if_possible: if free and move_if_possible:
@ -275,7 +286,7 @@ class Entity:
def move(self, y: int, x: int) -> bool: def move(self, y: int, x: int) -> bool:
""" """
Moves an entity to (y,x) coordinates Moves an entity to (y,x) coordinates.
""" """
self.y = y self.y = y
self.x = x self.x = x
@ -283,49 +294,100 @@ class Entity:
def move_up(self, force: bool = False) -> bool: def move_up(self, force: bool = False) -> bool:
""" """
Moves the entity up one tile, if possible Moves the entity up one tile, if possible.
""" """
return self.move(self.y - 1, self.x) if force else \ return self.move(self.y - 1, self.x) if force else \
self.check_move(self.y - 1, self.x, True) self.check_move(self.y - 1, self.x, True)
def move_down(self, force: bool = False) -> bool: def move_down(self, force: bool = False) -> bool:
""" """
Moves the entity down one tile, if possible Moves the entity down one tile, if possible.
""" """
return self.move(self.y + 1, self.x) if force else \ return self.move(self.y + 1, self.x) if force else \
self.check_move(self.y + 1, self.x, True) self.check_move(self.y + 1, self.x, True)
def move_left(self, force: bool = False) -> bool: def move_left(self, force: bool = False) -> bool:
""" """
Moves the entity left one tile, if possible Moves the entity left one tile, if possible.
""" """
return self.move(self.y, self.x - 1) if force else \ return self.move(self.y, self.x - 1) if force else \
self.check_move(self.y, self.x - 1, True) self.check_move(self.y, self.x - 1, True)
def move_right(self, force: bool = False) -> bool: def move_right(self, force: bool = False) -> bool:
""" """
Moves the entity right one tile, if possible Moves the entity right one tile, if possible.
""" """
return self.move(self.y, self.x + 1) if force else \ return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True) self.check_move(self.y, self.x + 1, True)
def recalculate_paths(self, max_distance: int = 12) -> None:
"""
Uses Dijkstra algorithm to calculate best paths for other entities to
go to this entity. If self.paths is None, does nothing.
"""
if self.paths is None:
return
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
""" """
Define the action of the entity that is ran each tick. Defines the action the entity will do at each tick.
By default, does nothing. By default, does nothing.
""" """
pass pass
def distance_squared(self, other: "Entity") -> int: def distance_squared(self, other: "Entity") -> int:
""" """
Get the square of the distance to another entity. Gives the square of the distance to another entity.
Useful to check distances since square root takes time. Useful to check distances since taking the square root takes time.
""" """
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2 return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
def distance(self, other: "Entity") -> float: def distance(self, other: "Entity") -> float:
""" """
Get the cartesian distance to another entity. Gives the cartesian distance to another entity.
""" """
return sqrt(self.distance_squared(other)) return sqrt(self.distance_squared(other))
@ -348,6 +410,13 @@ class Entity:
""" """
return isinstance(self, FriendlyEntity) return isinstance(self, FriendlyEntity)
def is_familiar(self) -> bool:
"""
Is this entity a familiar?
"""
from squirrelbattle.entities.friendly import Familiar
return isinstance(self, Familiar)
def is_merchant(self) -> bool: def is_merchant(self) -> bool:
""" """
Is this entity a merchant? Is this entity a merchant?
@ -357,29 +426,34 @@ class Entity:
@property @property
def translated_name(self) -> str: def translated_name(self) -> str:
"""
Translates the name of entities.
"""
return _(self.name.replace("_", " ")) return _(self.name.replace("_", " "))
@staticmethod @staticmethod
def get_all_entity_classes() -> list: def get_all_entity_classes() -> list:
""" """
Returns all entities subclasses Returns all entities subclasses.
""" """
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Merchant, Sunflower from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet
return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear, return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
Sunflower, Tiger, Merchant] Sunflower, Tiger, Merchant, Trumpet]
@staticmethod @staticmethod
def get_all_entity_classes_in_a_dict() -> dict: def get_all_entity_classes_in_a_dict() -> dict:
""" """
Returns all entities subclasses in a dictionary Returns all entities subclasses in a dictionary.
""" """
from squirrelbattle.entities.player import Player from squirrelbattle.entities.player import Player
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear TeddyBear
from squirrelbattle.entities.friendly import Merchant, Sunflower from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \ from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
Heart, Sword Heart, Sword
return { return {
@ -394,11 +468,12 @@ class Entity:
"Merchant": Merchant, "Merchant": Merchant,
"Sunflower": Sunflower, "Sunflower": Sunflower,
"Sword": Sword, "Sword": Sword,
"Trumpet": Trumpet,
} }
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the coordinates of the entity Saves the coordinates of the entity.
""" """
d = dict() d = dict()
d["x"] = self.x d["x"] = self.x
@ -410,7 +485,7 @@ class Entity:
class FightingEntity(Entity): class FightingEntity(Entity):
""" """
A FightingEntity is an entity that can fight, and thus has a health, A FightingEntity is an entity that can fight, and thus has a health,
level and stats level and stats.
""" """
maxhealth: int maxhealth: int
health: int health: int
@ -437,11 +512,15 @@ class FightingEntity(Entity):
@property @property
def dead(self) -> bool: def dead(self) -> bool:
"""
Is this entity dead ?
"""
return self.health <= 0 return self.health <= 0
def hit(self, opponent: "FightingEntity") -> str: def hit(self, opponent: "FightingEntity") -> str:
""" """
Deals damage to the opponent, based on the stats The entity deals damage to the opponent
based on their respective stats.
""" """
return _("{name} hits {opponent}.")\ return _("{name} hits {opponent}.")\
.format(name=_(self.translated_name.capitalize()), .format(name=_(self.translated_name.capitalize()),
@ -450,7 +529,8 @@ class FightingEntity(Entity):
def take_damage(self, attacker: "Entity", amount: int) -> str: def take_damage(self, attacker: "Entity", amount: int) -> str:
""" """
Take damage from the attacker, based on the stats The entity takes damage from the attacker
based on their respective stats.
""" """
self.health -= amount self.health -= amount
if self.health <= 0: if self.health <= 0:
@ -463,20 +543,20 @@ class FightingEntity(Entity):
def die(self) -> None: def die(self) -> None:
""" """
If a fighting entity has no more health, it dies and is removed If a fighting entity has no more health, it dies and is removed.
""" """
self.map.remove_entity(self) self.map.remove_entity(self)
def keys(self) -> list: def keys(self) -> list:
""" """
Returns a fighting entity's specific attributes Returns a fighting entity's specific attributes.
""" """
return ["name", "maxhealth", "health", "level", "strength", return ["name", "maxhealth", "health", "level", "strength",
"intelligence", "charisma", "dexterity", "constitution"] "intelligence", "charisma", "dexterity", "constitution"]
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
Saves the state of the entity into a dictionary Saves the state of the entity into a dictionary.
""" """
d = super().save_state() d = super().save_state()
for name in self.keys(): for name in self.keys():
@ -486,7 +566,7 @@ class FightingEntity(Entity):
class FriendlyEntity(FightingEntity): class FriendlyEntity(FightingEntity):
""" """
Friendly entities are living entities which do not attack the player Friendly entities are living entities which do not attack the player.
""" """
dialogue_option: list dialogue_option: list
@ -497,7 +577,7 @@ class FriendlyEntity(FightingEntity):
def keys(self) -> list: def keys(self) -> list:
""" """
Returns a friendly entity's specific attributes Returns a friendly entity's specific attributes.
""" """
return ["maxhealth", "health"] return ["maxhealth", "health"]
@ -508,7 +588,7 @@ class InventoryHolder(Entity):
def translate_inventory(self, inventory: list) -> list: def translate_inventory(self, inventory: list) -> list:
""" """
Translate the JSON-state of the inventory into a list of the items in Translates the JSON save of the inventory into a list of the items in
the inventory. the inventory.
""" """
for i in range(len(inventory)): for i in range(len(inventory)):
@ -518,7 +598,7 @@ class InventoryHolder(Entity):
def dict_to_inventory(self, item_dict: dict) -> Entity: def dict_to_inventory(self, item_dict: dict) -> Entity:
""" """
Translate a dict object that contains the state of an item Translates a dictionnary that contains the state of an item
into an item object. into an item object.
""" """
entity_classes = self.get_all_entity_classes_in_a_dict() entity_classes = self.get_all_entity_classes_in_a_dict()
@ -528,7 +608,7 @@ class InventoryHolder(Entity):
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
We save the inventory of the merchant formatted as JSON The inventory of the merchant is saved in a JSON format.
""" """
d = super().save_state() d = super().save_state()
d["hazel"] = self.hazel d["hazel"] = self.hazel
@ -537,19 +617,19 @@ class InventoryHolder(Entity):
def add_to_inventory(self, obj: Any) -> None: def add_to_inventory(self, obj: Any) -> None:
""" """
Adds an object to inventory Adds an object to the inventory.
""" """
self.inventory.append(obj) self.inventory.append(obj)
def remove_from_inventory(self, obj: Any) -> None: def remove_from_inventory(self, obj: Any) -> None:
""" """
Removes an object from the inventory Removes an object from the inventory.
""" """
self.inventory.remove(obj) self.inventory.remove(obj)
def change_hazel_balance(self, hz: int) -> None: def change_hazel_balance(self, hz: int) -> None:
""" """
Change the number of hazel the entity has by hz. hz is negative Changes the number of hazel the entity has by hz. hz is negative
when the player loses money and positive when he gains money when the entity loses money and positive when it gains money.
""" """
self.hazel += hz self.hazel += hz

View File

@ -14,7 +14,7 @@ from .translations import gettext as _, Translator
class Menu: class Menu:
""" """
A Menu object is the logical representation of a menu in the game A Menu object is the logical representation of a menu in the game.
""" """
values: list values: list
@ -23,26 +23,26 @@ class Menu:
def go_up(self) -> None: def go_up(self) -> None:
""" """
Moves the pointer of the menu on the previous value Moves the pointer of the menu on the previous value.
""" """
self.position = max(0, self.position - 1) self.position = max(0, self.position - 1)
def go_down(self) -> None: def go_down(self) -> None:
""" """
Moves the pointer of the menu on the next value Moves the pointer of the menu on the next value.
""" """
self.position = min(len(self.values) - 1, self.position + 1) self.position = min(len(self.values) - 1, self.position + 1)
def validate(self) -> Any: def validate(self) -> Any:
""" """
Selects the value that is pointed by the menu pointer Selects the value that is pointed by the menu pointer.
""" """
return self.values[self.position] return self.values[self.position]
class MainMenuValues(Enum): class MainMenuValues(Enum):
""" """
Values of the main menu Values of the main menu.
""" """
START = "New game" START = "New game"
RESUME = "Resume" RESUME = "Resume"
@ -57,14 +57,14 @@ class MainMenuValues(Enum):
class MainMenu(Menu): class MainMenu(Menu):
""" """
A special instance of a menu : the main menu A special instance of a menu : the main menu.
""" """
values = [e for e in MainMenuValues] values = [e for e in MainMenuValues]
class SettingsMenu(Menu): class SettingsMenu(Menu):
""" """
A special instance of a menu : the settings menu A special instance of a menu : the settings menu.
""" """
waiting_for_key: bool = False waiting_for_key: bool = False
@ -75,7 +75,7 @@ class SettingsMenu(Menu):
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None: game: Any) -> None:
""" """
In the setting menu, we van select a setting and change it In the setting menu, we can select a setting and change it.
""" """
if not self.waiting_for_key: if not self.waiting_for_key:
# Navigate normally through the menu. # Navigate normally through the menu.
@ -121,22 +121,40 @@ class SettingsMenu(Menu):
class InventoryMenu(Menu): class InventoryMenu(Menu):
"""
A special instance of a menu : the menu for the inventory of the player.
"""
player: Player player: Player
def update_player(self, player: Player) -> None: def update_player(self, player: Player) -> None:
"""
Updates the player.
"""
self.player = player self.player = player
@property @property
def values(self) -> list: def values(self) -> list:
"""
Returns the values of the menu.
"""
return self.player.inventory return self.player.inventory
class StoreMenu(Menu): class StoreMenu(Menu):
"""
A special instance of a menu : the menu for the inventory of a merchant.
"""
merchant: Merchant = None merchant: Merchant = None
def update_merchant(self, merchant: Merchant) -> None: def update_merchant(self, merchant: Merchant) -> None:
"""
Updates the merchant.
"""
self.merchant = merchant self.merchant = merchant
@property @property
def values(self) -> list: def values(self) -> list:
"""
Returns the values of the menu.
"""
return self.merchant.inventory if self.merchant else [] return self.merchant.inventory if self.merchant else []

View File

@ -13,9 +13,10 @@ from .translations import gettext as _
class Settings: class Settings:
""" """
This class stores the settings of the game. This class stores the settings of the game.
Settings can be get by using for example settings.TEXTURE_PACK directly. Settings can be obtained by using for example settings.TEXTURE_PACK
The comment can be get by using settings.get_comment('TEXTURE_PACK'). directly.
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key' The comment can be obtained by using settings.get_comment('TEXTURE_PACK').
We can set the setting by simply using settings.TEXTURE_PACK = 'new_key'
""" """
def __init__(self): def __init__(self):
self.KEY_UP_PRIMARY = ['z', 'Main key to move up'] self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
@ -51,7 +52,7 @@ class Settings:
def get_comment(self, item: str) -> str: def get_comment(self, item: str) -> str:
""" """
Retrieve the comment of a setting. Retrieves the comment relative to a setting.
""" """
if item in self.settings_keys: if item in self.settings_keys:
return _(object.__getattribute__(self, item)[1]) return _(object.__getattribute__(self, item)[1])
@ -62,13 +63,13 @@ class Settings:
@property @property
def settings_keys(self) -> Generator[str, Any, None]: def settings_keys(self) -> Generator[str, Any, None]:
""" """
Get the list of all parameters. Gets the list of all parameters.
""" """
return (key for key in self.__dict__) return (key for key in self.__dict__)
def loads_from_string(self, json_str: str) -> None: def loads_from_string(self, json_str: str) -> None:
""" """
Dump settings Loads settings.
""" """
d = json.loads(json_str) d = json.loads(json_str)
for key in d: for key in d:
@ -76,7 +77,7 @@ class Settings:
def dumps_to_string(self) -> str: def dumps_to_string(self) -> str:
""" """
Dump settings Dumps settings.
""" """
d = dict() d = dict()
for key in self.settings_keys: for key in self.settings_keys:

View File

@ -8,7 +8,7 @@ from types import TracebackType
class TermManager: # pragma: no cover class TermManager: # pragma: no cover
""" """
The TermManager object initializes the terminal, returns a screen object and The TermManager object initializes the terminal, returns a screen object and
de-initializes the terminal after use de-initializes the terminal after use.
""" """
def __init__(self): def __init__(self):
self.screen = curses.initscr() self.screen = curses.initscr()

View File

@ -6,6 +6,7 @@ import unittest
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item, \ from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item, \
Explosion Explosion
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Trumpet
from squirrelbattle.entities.player import Player from squirrelbattle.entities.player import Player
from squirrelbattle.interfaces import Entity, Map from squirrelbattle.interfaces import Entity, Map
from squirrelbattle.resources import ResourceManager from squirrelbattle.resources import ResourceManager
@ -14,7 +15,7 @@ from squirrelbattle.resources import ResourceManager
class TestEntities(unittest.TestCase): class TestEntities(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
""" """
Load example map that can be used in tests. Loads example map that can be used in tests.
""" """
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt")) self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.player = Player() self.player = Player()
@ -23,7 +24,7 @@ class TestEntities(unittest.TestCase):
def test_basic_entities(self) -> None: def test_basic_entities(self) -> None:
""" """
Test some random stuff with basic entities. Tests some random stuff with basic entities.
""" """
entity = Entity() entity = Entity()
entity.move(42, 64) entity.move(42, 64)
@ -38,7 +39,7 @@ class TestEntities(unittest.TestCase):
def test_fighting_entities(self) -> None: def test_fighting_entities(self) -> None:
""" """
Test some random stuff with fighting entities. Tests some random stuff with fighting entities.
""" """
entity = Tiger() entity = Tiger()
self.map.add_entity(entity) self.map.add_entity(entity)
@ -57,17 +58,17 @@ class TestEntities(unittest.TestCase):
self.map.add_entity(entity) self.map.add_entity(entity)
entity.move(15, 44) entity.move(15, 44)
# Move randomly # Move randomly
self.map.tick() self.map.tick(self.player)
self.assertFalse(entity.y == 15 and entity.x == 44) self.assertFalse(entity.y == 15 and entity.x == 44)
# Move to the player # Move to the player
entity.move(3, 6) entity.move(3, 6)
self.map.tick() self.map.tick(self.player)
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
# Rabbit should fight # Rabbit should fight
old_health = self.player.health old_health = self.player.health
self.map.tick() self.map.tick(self.player)
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health) self.assertEqual(old_health - entity.strength, self.player.health)
self.assertEqual(self.map.logs.messages[-1], self.assertEqual(self.map.logs.messages[-1],
@ -89,9 +90,50 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.dead) self.assertTrue(entity.dead)
self.assertGreaterEqual(self.player.current_xp, 3) self.assertGreaterEqual(self.player.current_xp, 3)
# Test the familiars
fam = Trumpet()
entity = Rabbit()
self.map.add_entity(entity)
self.map.add_entity(fam)
self.player.move(1, 6)
entity.move(2, 6)
fam.move(2, 7)
# Test fighting
entity.health = 2
entity.paths = []
entity.recalculate_paths()
fam.target = entity
self.map.tick(self.player)
self.assertTrue(entity.dead)
# Test finding a new target
entity2 = Rabbit()
self.map.add_entity(entity2)
entity2.move(2, 6)
self.map.tick(self.player)
self.assertTrue(fam.target == entity2)
self.map.remove_entity(entity2)
# Test following the player and finding the player as target
self.player.move(5, 5)
fam.move(4, 5)
fam.target = None
self.player.move_down()
self.map.tick(self.player)
self.assertTrue(fam.target == self.player)
self.assertEqual(fam.y, 5)
self.assertEqual(fam.x, 5)
# Test random move
fam.move(13, 20)
fam.target = self.player
self.map.tick(self.player)
self.assertTrue(fam.x != 20 or fam.y != 13)
def test_items(self) -> None: def test_items(self) -> None:
""" """
Test some random stuff with items. Tests some random stuff with items.
""" """
item = Item() item = Item()
self.map.add_entity(item) self.map.add_entity(item)
@ -112,7 +154,7 @@ class TestEntities(unittest.TestCase):
def test_bombs(self) -> None: def test_bombs(self) -> None:
""" """
Test some random stuff with bombs. Tests some random stuff with bombs.
""" """
item = Bomb() item = Bomb()
hedgehog = Hedgehog() hedgehog = Hedgehog()
@ -156,7 +198,7 @@ class TestEntities(unittest.TestCase):
def test_hearts(self) -> None: def test_hearts(self) -> None:
""" """
Test some random stuff with hearts. Tests some random stuff with hearts.
""" """
item = Heart() item = Heart()
self.map.add_entity(item) self.map.add_entity(item)
@ -171,7 +213,7 @@ class TestEntities(unittest.TestCase):
def test_body_snatch_potion(self) -> None: def test_body_snatch_potion(self) -> None:
""" """
Test some random stuff with body snatch potions. Tests some random stuff with body snatch potions.
""" """
item = BodySnatchPotion() item = BodySnatchPotion()
self.map.add_entity(item) self.map.add_entity(item)
@ -189,7 +231,7 @@ class TestEntities(unittest.TestCase):
def test_players(self) -> None: def test_players(self) -> None:
""" """
Test some random stuff with players. Tests some random stuff with players.
""" """
player = Player() player = Player()
self.map.add_entity(player) self.map.add_entity(player)

View File

@ -22,7 +22,7 @@ from ..translations import gettext as _, Translator
class TestGame(unittest.TestCase): class TestGame(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
""" """
Setup game. Sets the game up.
""" """
self.game = Game() self.game = Game()
self.game.new_game() self.game.new_game()
@ -36,7 +36,7 @@ class TestGame(unittest.TestCase):
def test_load_game(self) -> None: def test_load_game(self) -> None:
""" """
Save a game and reload it. Saves a game and reloads it.
""" """
bomb = Bomb() bomb = Bomb()
self.game.map.add_entity(bomb) self.game.map.add_entity(bomb)
@ -46,6 +46,11 @@ class TestGame(unittest.TestCase):
bomb.hold(self.game.player) bomb.hold(self.game.player)
sword.hold(self.game.player) sword.hold(self.game.player)
for entity in self.game.map.entities:
# trumpets change order when they are loaded, this breaks the test.
if entity.name == 'trumpet':
self.game.map.remove_entity(entity)
# Ensure that merchants can be saved # Ensure that merchants can be saved
merchant = Merchant() merchant = Merchant()
merchant.move(3, 6) merchant.move(3, 6)
@ -90,7 +95,7 @@ class TestGame(unittest.TestCase):
def test_bootstrap_fail(self) -> None: def test_bootstrap_fail(self) -> None:
""" """
Ensure that the test can't play the game, Ensures that the test can't play the game,
because there is no associated shell. because there is no associated shell.
Yeah, that's only for coverage. Yeah, that's only for coverage.
""" """
@ -99,7 +104,7 @@ class TestGame(unittest.TestCase):
def test_key_translation(self) -> None: def test_key_translation(self) -> None:
""" """
Test key bindings. Tests key bindings.
""" """
self.game.settings = Settings() self.game.settings = Settings()
@ -158,7 +163,7 @@ class TestGame(unittest.TestCase):
def test_key_press(self) -> None: def test_key_press(self) -> None:
""" """
Press a key and see what is done. Presses a key and asserts what is done is correct.
""" """
self.assertEqual(self.game.state, GameMode.MAINMENU) self.assertEqual(self.game.state, GameMode.MAINMENU)
self.assertEqual(self.game.main_menu.validate(), self.assertEqual(self.game.main_menu.validate(),
@ -249,7 +254,7 @@ class TestGame(unittest.TestCase):
def test_mouse_click(self) -> None: def test_mouse_click(self) -> None:
""" """
Simulate mouse clicks. Simulates mouse clicks.
""" """
self.game.state = GameMode.MAINMENU self.game.state = GameMode.MAINMENU
@ -279,7 +284,7 @@ class TestGame(unittest.TestCase):
def test_new_game(self) -> None: def test_new_game(self) -> None:
""" """
Ensure that the start button starts a new game. Ensures that the start button starts a new game.
""" """
old_map = self.game.map old_map = self.game.map
old_player = self.game.player old_player = self.game.player
@ -302,7 +307,7 @@ class TestGame(unittest.TestCase):
def test_settings_menu(self) -> None: def test_settings_menu(self) -> None:
""" """
Ensure that the settings menu is working properly. Ensures that the settings menu is working properly.
""" """
self.game.settings = Settings() self.game.settings = Settings()
@ -388,7 +393,7 @@ class TestGame(unittest.TestCase):
def test_dead_screen(self) -> None: def test_dead_screen(self) -> None:
""" """
Kill player and render dead screen. Kills the player and renders the dead message on the fake screen.
""" """
self.game.state = GameMode.PLAY self.game.state = GameMode.PLAY
# Kill player # Kill player
@ -404,14 +409,14 @@ class TestGame(unittest.TestCase):
def test_not_implemented(self) -> None: def test_not_implemented(self) -> None:
""" """
Check that some functions are not implemented, only for coverage. Checks that some functions are not implemented, only for coverage.
""" """
self.assertRaises(NotImplementedError, Display.display, None) self.assertRaises(NotImplementedError, Display.display, None)
self.assertRaises(NotImplementedError, Display.update, None, self.game) self.assertRaises(NotImplementedError, Display.update, None, self.game)
def test_messages(self) -> None: def test_messages(self) -> None:
""" """
Display error messages. Displays error messages.
""" """
self.game.message = "I am an error" self.game.message = "I am an error"
self.game.display_actions(DisplayActions.UPDATE) self.game.display_actions(DisplayActions.UPDATE)
@ -421,7 +426,7 @@ class TestGame(unittest.TestCase):
def test_inventory_menu(self) -> None: def test_inventory_menu(self) -> None:
""" """
Open the inventory menu and interact with items. Opens the inventory menu and interacts with items.
""" """
self.game.state = GameMode.PLAY self.game.state = GameMode.PLAY
# Open and close the inventory # Open and close the inventory
@ -482,7 +487,7 @@ class TestGame(unittest.TestCase):
def test_talk_to_sunflowers(self) -> None: def test_talk_to_sunflowers(self) -> None:
""" """
Interact with sunflowers Interacts with sunflowers.
""" """
self.game.state = GameMode.PLAY self.game.state = GameMode.PLAY
@ -533,7 +538,7 @@ class TestGame(unittest.TestCase):
def test_talk_to_merchant(self) -> None: def test_talk_to_merchant(self) -> None:
""" """
Interact with merchants Interacts with merchants.
""" """
self.game.state = GameMode.PLAY self.game.state = GameMode.PLAY
@ -643,3 +648,19 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.player.y, 3) self.assertEqual(self.game.player.y, 3)
self.assertEqual(self.game.player.x, 40) self.assertEqual(self.game.player.x, 40)
self.game.display_actions(DisplayActions.UPDATE) self.game.display_actions(DisplayActions.UPDATE)
def test_credits(self) -> None:
"""
Load credits menu.
"""
self.game.state = GameMode.MAINMENU
self.game.display_actions(DisplayActions.MOUSE, 41, 41)
self.assertEqual(self.game.state, GameMode.CREDITS)
self.game.display_actions(DisplayActions.MOUSE, 21, 21)
self.game.display_actions(DisplayActions.REFRESH)
self.game.state = GameMode.CREDITS
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)

View File

@ -11,7 +11,7 @@ from squirrelbattle.resources import ResourceManager
class TestInterfaces(unittest.TestCase): class TestInterfaces(unittest.TestCase):
def test_map(self) -> None: def test_map(self) -> None:
""" """
Create a map and check that it is well parsed. Creates a map and checks that it is well parsed.
""" """
m = Map.load_from_string("0 0\n.#\n#.\n") m = Map.load_from_string("0 0\n.#\n#.\n")
self.assertEqual(m.width, 2) self.assertEqual(m.width, 2)
@ -20,7 +20,7 @@ class TestInterfaces(unittest.TestCase):
def test_load_map(self) -> None: def test_load_map(self) -> None:
""" """
Try to load a map from a file. Tries to load a map from a file.
""" """
m = Map.load(ResourceManager.get_asset_path("example_map.txt")) m = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.assertEqual(m.width, 52) self.assertEqual(m.width, 52)
@ -28,7 +28,7 @@ class TestInterfaces(unittest.TestCase):
def test_tiles(self) -> None: def test_tiles(self) -> None:
""" """
Test some things about tiles. Tests some things about tiles.
""" """
self.assertFalse(Tile.FLOOR.is_wall()) self.assertFalse(Tile.FLOOR.is_wall())
self.assertTrue(Tile.WALL.is_wall()) self.assertTrue(Tile.WALL.is_wall())

View File

@ -24,3 +24,6 @@ class FakePad:
def getmaxyx(self) -> Tuple[int, int]: def getmaxyx(self) -> Tuple[int, int]:
return 42, 42 return 42, 42
def inch(self, y: int, x: int) -> str:
return "i"

View File

@ -13,7 +13,7 @@ class TestSettings(unittest.TestCase):
def test_settings(self) -> None: def test_settings(self) -> None:
""" """
Ensure that settings are well loaded. Ensures that settings are well loaded.
""" """
settings = Settings() settings = Settings()
self.assertEqual(settings.KEY_UP_PRIMARY, 'z') self.assertEqual(settings.KEY_UP_PRIMARY, 'z')

View File

@ -11,7 +11,7 @@ class TestTranslations(unittest.TestCase):
def test_main_menu_translation(self) -> None: def test_main_menu_translation(self) -> None:
""" """
Ensure that the main menu is translated. Ensures that the main menu is translated.
""" """
self.assertEqual(_("New game"), "Nouvelle partie") self.assertEqual(_("New game"), "Nouvelle partie")
self.assertEqual(_("Resume"), "Continuer") self.assertEqual(_("Resume"), "Continuer")
@ -22,7 +22,7 @@ class TestTranslations(unittest.TestCase):
def test_settings_menu_translation(self) -> None: def test_settings_menu_translation(self) -> None:
""" """
Ensure that the settings menu is translated. Ensures that the settings menu is translated.
""" """
self.assertEqual(_("Main key to move up"), self.assertEqual(_("Main key to move up"),
"Touche principale pour aller vers le haut") "Touche principale pour aller vers le haut")

View File

@ -13,7 +13,7 @@ class Translator:
""" """
This module uses gettext to translate strings. This module uses gettext to translate strings.
Translator.setlocale defines the language of the strings, Translator.setlocale defines the language of the strings,
then gettext() translates the message. then gettext() translates the messages.
""" """
SUPPORTED_LOCALES: List[str] = ["de", "en", "es", "fr"] SUPPORTED_LOCALES: List[str] = ["de", "en", "es", "fr"]
locale: str = "en" locale: str = "en"
@ -22,7 +22,7 @@ class Translator:
@classmethod @classmethod
def refresh_translations(cls) -> None: def refresh_translations(cls) -> None:
""" """
Load compiled translations. Loads compiled translations.
""" """
for language in cls.SUPPORTED_LOCALES: for language in cls.SUPPORTED_LOCALES:
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES" rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
@ -37,7 +37,7 @@ class Translator:
@classmethod @classmethod
def setlocale(cls, lang: str) -> None: def setlocale(cls, lang: str) -> None:
""" """
Define the language used to translate the game. Defines the language used to translate the game.
The language must be supported, otherwise nothing is done. The language must be supported, otherwise nothing is done.
""" """
lang = lang[:2] lang = lang[:2]
@ -51,7 +51,7 @@ class Translator:
@classmethod @classmethod
def makemessages(cls) -> None: # pragma: no cover def makemessages(cls) -> None: # pragma: no cover
""" """
Analyse all strings in the project and extract them. Analyses all strings in the project and extracts them.
""" """
for language in cls.SUPPORTED_LOCALES: for language in cls.SUPPORTED_LOCALES:
if language == "en": if language == "en":
@ -83,7 +83,7 @@ class Translator:
@classmethod @classmethod
def compilemessages(cls) -> None: def compilemessages(cls) -> None:
""" """
Compile translation messages from source files. Compiles translation messages from source files.
""" """
for language in cls.SUPPORTED_LOCALES: for language in cls.SUPPORTED_LOCALES:
if language == "en": if language == "en":
@ -99,7 +99,7 @@ class Translator:
def gettext(message: str) -> str: def gettext(message: str) -> str:
""" """
Translate a message. Translates a message.
""" """
return Translator.get_translator().gettext(message) return Translator.get_translator().gettext(message)