Merge branch 'master' into doc

This commit is contained in:
Eichhornchen 2021-01-06 14:39:23 +01:00
commit 77d501c389
27 changed files with 650 additions and 237 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

@ -25,9 +25,16 @@ class Display:
self.pack = pack or TexturePack.get_pack("ascii")
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()
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)
width = max(0, width)
lines = msg.split("\n")
@ -37,8 +44,8 @@ class Display:
def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int:
"""
Translate a tuple (R, G, B) into a curses color index.
If we have already a color index, then nothing is processed.
Translates a tuple (R, G, B) into a curses color index.
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
and we return this index.
The values of R, G and B must be between 0 and 1000, and not
@ -67,9 +74,9 @@ class Display:
low: bool = False, right: bool = False, top: bool = False,
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
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.
The foreground and background colors can be given as curses constants
(curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to
@ -129,6 +136,9 @@ class Display:
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
"""
Resizes a pad.
"""
self.x = x
self.y = y
self.width = width
@ -139,6 +149,9 @@ class Display:
self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
"""
Refreshes a pad
"""
if len(args) == 4:
self.resize(*args, resize_pad)
self.display()
@ -147,10 +160,10 @@ class Display:
window_y: int, window_x: int,
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,
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.
"""
top_y, top_x = max(0, top_y), max(0, top_x)
@ -180,7 +193,7 @@ class Display:
def handle_click(self, y: int, x: int, game: Game) -> None:
"""
A mouse click was performed on the coordinates (y, x) of the pad.
Maybe it can do something.
Maybe it should do something.
"""
pass
@ -194,7 +207,9 @@ class Display:
class VerticalSplit(Display):
"""
A class to split the screen in two vertically with a pretty line.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, 1)
@ -215,7 +230,9 @@ class VerticalSplit(Display):
class HorizontalSplit(Display):
"""
A class to split the screen in two horizontally with a pretty line.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(1, self.cols)
@ -236,6 +253,9 @@ class HorizontalSplit(Display):
class Box(Display):
"""
A class for pretty boxes to print menus and other content.
"""
title: str = ""
def update_title(self, title: str) -> None:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,9 @@ from typing import Any
class TexturePack:
"""
A class to handle displaying several textures.
"""
_packs = dict()
name: str
@ -29,6 +32,7 @@ class TexturePack:
SWORD: str
TEDDY_BEAR: str
TIGER: str
TRUMPET: str
WALL: str
ASCII_PACK: "TexturePack"
@ -74,6 +78,7 @@ TexturePack.ASCII_PACK = TexturePack(
SWORD='\u2020',
TEDDY_BEAR='8',
TIGER='n',
TRUMPET='/',
WALL='#',
)
@ -100,5 +105,6 @@ TexturePack.SQUIRREL_PACK = TexturePack(
SWORD='🗡️ ',
TEDDY_BEAR='🧸',
TIGER='🐅',
TRUMPET='🎺',
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 .player import Player
from .monsters import Monster
from .items import Item
from random import choice
from random import choice, shuffle
class Merchant(InventoryHolder, FriendlyEntity):
"""
The class for merchants in the dungeon
The class of merchants in the dungeon.
"""
def keys(self) -> list:
"""
Returns a friendly entitie's specific attributes
Returns a friendly entitie's specific attributes.
"""
return super().keys() + ["inventory", "hazel"]
@ -20,7 +21,6 @@ class Merchant(InventoryHolder, FriendlyEntity):
super().__init__(name=name, *args, **kwargs)
self.inventory = self.translate_inventory(inventory or [])
self.hazel = hazel
if not self.inventory:
for i in range(5):
self.inventory.append(choice(Item.get_all_items())())
@ -28,20 +28,20 @@ class Merchant(InventoryHolder, FriendlyEntity):
def talk_to(self, player: Player) -> str:
"""
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")
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
class Sunflower(FriendlyEntity):
"""
A friendly sunflower
A friendly sunflower.
"""
def __init__(self, maxhealth: int = 15,
*args, **kwargs) -> None:
@ -49,4 +49,80 @@ class Sunflower(FriendlyEntity):
@property
def dialogue_option(self) -> list:
"""
Lists all that a sunflower can say to the player.
"""
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):
"""
A class for items
A class for items.
"""
held: bool
held_by: Optional[InventoryHolder]
@ -27,7 +27,7 @@ class Item(Entity):
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:
self.held_by.inventory.remove(self)
@ -48,7 +48,7 @@ class Item(Entity):
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_by = player
@ -57,7 +57,7 @@ class Item(Entity):
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["held"] = self.held
@ -65,13 +65,16 @@ class Item(Entity):
@staticmethod
def get_all_items() -> list:
"""
Returns the list of all item classes.
"""
return [BodySnatchPotion, Bomb, Heart, Sword]
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool:
"""
Does all necessary actions when an object is to be sold.
Is overwritten by some classes that cannot exist in the player's
inventory
inventory.
"""
if buyer.hazel >= self.price:
self.hold(buyer)
@ -85,7 +88,7 @@ class Item(Entity):
class Heart(Item):
"""
A heart item to return health to the player
A heart item to return health to the player.
"""
healing: int
@ -96,14 +99,15 @@ class Heart(Item):
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.map.remove_entity(self)
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["healing"] = self.healing
@ -129,7 +133,7 @@ class Bomb(Item):
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:
self.owner = self.held_by
@ -138,7 +142,7 @@ class Bomb(Item):
def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
Special exploding action of the bomb.
"""
if self.exploding:
if self.tick > 0:
@ -164,7 +168,7 @@ class Bomb(Item):
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["exploding"] = self.exploding
@ -181,13 +185,13 @@ class Explosion(Item):
def act(self, m: Map) -> None:
"""
The explosion instant dies.
The bomb disappears after exploding.
"""
m.remove_entity(self)
def hold(self, player: InventoryHolder) -> None:
"""
The player can't hold any explosion.
The player can't hold an explosion.
"""
pass

View File

@ -10,8 +10,8 @@ from ..interfaces import FightingEntity, Map
class Monster(FightingEntity):
"""
The class for all monsters in the dungeon.
A monster must override this class, and the parameters are given
in the __init__ function.
All specific monster classes overwrite this class,
and the parameters are given in the __init__ function.
An example of the specification of a monster that has a strength of 4
and 20 max HP:
@ -21,7 +21,7 @@ class Monster(FightingEntity):
super().__init__(name="my_monster", strength=strength,
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):
super().__init__(*args, **kwargs)
@ -29,7 +29,7 @@ class Monster(FightingEntity):
def act(self, m: Map) -> None:
"""
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
for entity in m.entities:
@ -38,12 +38,12 @@ class Monster(FightingEntity):
target = entity
break
# A Dijkstra algorithm has ran that targets the player.
# With that way, monsters can simply follow the path.
# If they can't move and they are already close to the player,
# They hit.
# Monsters move according to a Dijkstra algorithm
# that targets the player.
# If they can not move and are already close to the player,
# they hit.
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)]:
moved = self.check_move(next_y, next_x, True)
if moved:
@ -52,8 +52,8 @@ class Monster(FightingEntity):
self.map.logs.add_message(self.hit(target))
break
else:
# Move in a random direction
# If the direction is not available, try another one
# 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)
@ -61,10 +61,17 @@ class Monster(FightingEntity):
if move():
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):
"""
A tiger monster
A tiger monster.
"""
def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None:
@ -74,7 +81,7 @@ class Tiger(Monster):
class Hedgehog(Monster):
"""
A really mean hedgehog monster
A really mean hedgehog monster.
"""
def __init__(self, name: str = "hedgehog", strength: int = 3,
maxhealth: int = 10, *args, **kwargs) -> None:
@ -84,7 +91,7 @@ class Hedgehog(Monster):
class Rabbit(Monster):
"""
A rabbit monster
A rabbit monster.
"""
def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, *args, **kwargs) -> None:
@ -94,7 +101,7 @@ class Rabbit(Monster):
class TeddyBear(Monster):
"""
A cute teddybear monster
A cute teddybear monster.
"""
def __init__(self, name: str = "teddy_bear", strength: int = 0,
maxhealth: int = 50, *args, **kwargs) -> None:

View File

@ -1,21 +1,17 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from functools import reduce
from queue import PriorityQueue
from random import randint
from typing import Dict, Tuple
from ..interfaces import FightingEntity, InventoryHolder
class Player(InventoryHolder, FightingEntity):
"""
The class of the player
The class of the player.
"""
current_xp: int = 0
max_xp: int = 10
paths: Dict[Tuple[int, int], Tuple[int, int]]
def __init__(self, name: str = "player", maxhealth: int = 20,
strength: int = 5, intelligence: int = 1, charisma: int = 1,
@ -45,7 +41,7 @@ class Player(InventoryHolder, FightingEntity):
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:
self.level += 1
@ -59,8 +55,8 @@ class Player(InventoryHolder, FightingEntity):
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
Adds some experience to the player.
If the required amount is reached, the player levels up.
"""
self.current_xp += xp
self.level_up()
@ -87,56 +83,6 @@ class Player(InventoryHolder, FightingEntity):
entity.hold(self)
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:
"""
Saves the state of the entity into a dictionary

View File

@ -21,20 +21,20 @@ class DisplayActions(Enum):
class GameMode(Enum):
"""
Game mode options
Game mode options.
"""
MAINMENU = auto()
PLAY = auto()
SETTINGS = auto()
INVENTORY = auto()
STORE = auto()
CREDITS = auto()
class KeyValues(Enum):
"""
Key values options used in the game
Key values options used in the game.
"""
MOUSE = auto()
UP = auto()
DOWN = auto()
LEFT = auto()
@ -51,7 +51,7 @@ class KeyValues(Enum):
@staticmethod
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,
settings.KEY_DOWN_PRIMARY):

View File

@ -30,7 +30,7 @@ class Game:
def __init__(self) -> None:
"""
Init the game.
Initiates the game.
"""
self.state = GameMode.MAINMENU
self.waiting_for_friendly_key = False
@ -49,7 +49,7 @@ class Game:
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
self.map = Map.load(ResourceManager.get_asset_path("example_map_2.txt"))
@ -64,8 +64,8 @@ class Game:
def run(self, screen: Any) -> None: # pragma no cover
"""
Main infinite loop.
We wait for the player's action, then we do what that should be done
when the given key gets pressed.
We wait for the player's action, then we do what should be done
when a key gets pressed.
"""
screen.refresh()
while True:
@ -84,7 +84,7 @@ class Game:
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> 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.
"""
if self.message:
@ -106,6 +106,8 @@ class Game:
self.settings_menu.handle_key_pressed(key, raw_key, self)
elif self.state == GameMode.STORE:
self.handle_key_pressed_store(key)
elif self.state == GameMode.CREDITS:
self.state = GameMode.MAINMENU
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
@ -114,16 +116,16 @@ class Game:
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY
elif key == KeyValues.SPACE:
@ -132,12 +134,13 @@ class Game:
# Wait for the direction of the friendly entity
self.waiting_for_friendly_key = True
elif key == KeyValues.WAIT:
self.map.tick()
self.map.tick(self.player)
def handle_friendly_entity_chat(self, key: KeyValues) -> None:
"""
If the player is talking to a friendly entity, we get the direction
where the entity is, then we interact with it.
If the player tries to talk to a friendly entity, the game waits for
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:
return
@ -226,7 +229,7 @@ class Game:
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:
self.main_menu.go_down()
@ -251,13 +254,13 @@ class Game:
def save_state(self) -> dict:
"""
Saves the game to a dictionary
Saves the game to a dictionary.
"""
return self.map.save_state()
def load_state(self, d: dict) -> None:
"""
Loads the game from a dictionary
Loads the game from a dictionary.
"""
try:
self.map.load_state(d)
@ -281,7 +284,7 @@ class Game:
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")
if os.path.isfile(file_path):
@ -298,7 +301,7 @@ class Game:
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:
f.write(json.dumps(self.save_state()))

View File

@ -4,7 +4,9 @@
from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List, Optional, Any
from typing import List, Optional, Any, Dict, Tuple
from queue import PriorityQueue
from functools import reduce
from .display.texturepack import TexturePack
from .translations import gettext as _
@ -12,7 +14,7 @@ from .translations import gettext as _
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
the list was to be reassigned.
"""
@ -32,7 +34,7 @@ class Logs:
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.
"""
width: int
@ -59,14 +61,17 @@ class Map:
def add_entity(self, entity: "Entity") -> None:
"""
Register a new entity in the map.
Registers a new entity in the map.
"""
self.entities.append(entity)
if entity.is_familiar():
self.entities.insert(1, entity)
else:
self.entities.append(entity)
entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
Unregisters an entity from the map.
"""
if entity in self.entities:
self.entities.remove(entity)
@ -86,7 +91,7 @@ class Map:
def entity_is_present(self, y: int, x: int) -> bool:
"""
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 \
any(entity.x == x and entity.y == y and entity.is_friendly()
@ -95,7 +100,8 @@ class Map:
@staticmethod
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:
file = f.read()
@ -104,7 +110,7 @@ class Map:
@staticmethod
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")
first_line = lines[0]
@ -120,7 +126,7 @@ class Map:
@staticmethod
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")
tiles = [[Tile.from_ascii_char(c)
@ -129,7 +135,7 @@ class Map:
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.
"""
return "\n".join("".join(tile.char(pack) for tile in line)
@ -137,7 +143,7 @@ class Map:
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):
y, x = 0, 0
@ -150,16 +156,19 @@ class Map:
entity.move(y, x)
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:
entity.act(self)
if entity.is_familiar():
entity.act(p, self)
else:
entity.act(self)
def save_state(self) -> dict:
"""
Saves the map's attributes to a dictionary
Saves the map's attributes to a dictionary.
"""
d = dict()
d["width"] = self.width
@ -176,7 +185,7 @@ class Map:
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.height = d["height"]
@ -193,7 +202,7 @@ class Map:
class Tile(Enum):
"""
The internal representation of the tiles of the map
The internal representation of the tiles of the map.
"""
EMPTY = auto()
WALL = auto()
@ -202,7 +211,7 @@ class Tile(Enum):
@staticmethod
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:
if tile.char(TexturePack.ASCII_PACK) == ch:
@ -212,7 +221,7 @@ class Tile(Enum):
def char(self, pack: TexturePack) -> str:
"""
Translates a Tile to the corresponding character according
to the texture pack
to the texture pack.
"""
return getattr(pack, self.name)
@ -224,19 +233,20 @@ class Tile(Enum):
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
class Entity:
"""
An Entity object represents any entity present on the map
An Entity object represents any entity present on the map.
"""
y: int
x: int
name: str
map: Map
paths: Dict[Tuple[int, int], Tuple[int, int]]
# noinspection PyShadowingBuiltins
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
@ -245,11 +255,12 @@ class Entity:
self.x = x
self.name = name
self.map = map
self.paths = None
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool:
"""
Checks if moving to (y,x) is authorized
Checks if moving to (y,x) is authorized.
"""
free = self.map.is_free(y, x)
if free and move_if_possible:
@ -258,7 +269,7 @@ class Entity:
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.x = x
@ -266,49 +277,100 @@ class Entity:
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 \
self.check_move(self.y - 1, self.x, True)
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 \
self.check_move(self.y + 1, self.x, True)
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 \
self.check_move(self.y, self.x - 1, True)
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 \
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:
"""
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.
"""
pass
def distance_squared(self, other: "Entity") -> int:
"""
Get the square of the distance to another entity.
Useful to check distances since square root takes time.
Gives the square of the distance to another entity.
Useful to check distances since taking the square root takes time.
"""
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
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))
@ -331,6 +393,13 @@ class Entity:
"""
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:
"""
Is this entity a merchant?
@ -340,29 +409,34 @@ class Entity:
@property
def translated_name(self) -> str:
"""
Translates the name of entities.
"""
return _(self.name.replace("_", " "))
@staticmethod
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.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Merchant, Sunflower
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet
return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
Sunflower, Tiger, Merchant]
Sunflower, Tiger, Merchant, Trumpet]
@staticmethod
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.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear
from squirrelbattle.entities.friendly import Merchant, Sunflower
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
Heart, Sword
return {
@ -377,11 +451,12 @@ class Entity:
"Merchant": Merchant,
"Sunflower": Sunflower,
"Sword": Sword,
"Trumpet": Trumpet,
}
def save_state(self) -> dict:
"""
Saves the coordinates of the entity
Saves the coordinates of the entity.
"""
d = dict()
d["x"] = self.x
@ -393,7 +468,7 @@ class Entity:
class FightingEntity(Entity):
"""
A FightingEntity is an entity that can fight, and thus has a health,
level and stats
level and stats.
"""
maxhealth: int
health: int
@ -420,11 +495,15 @@ class FightingEntity(Entity):
@property
def dead(self) -> bool:
"""
Is this entity dead ?
"""
return self.health <= 0
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}.")\
.format(name=_(self.translated_name.capitalize()),
@ -433,7 +512,8 @@ class FightingEntity(Entity):
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
if self.health <= 0:
@ -446,20 +526,20 @@ class FightingEntity(Entity):
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)
def keys(self) -> list:
"""
Returns a fighting entity's specific attributes
Returns a fighting entity's specific attributes.
"""
return ["name", "maxhealth", "health", "level", "strength",
"intelligence", "charisma", "dexterity", "constitution"]
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()
for name in self.keys():
@ -469,7 +549,7 @@ class FightingEntity(Entity):
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
@ -480,7 +560,7 @@ class FriendlyEntity(FightingEntity):
def keys(self) -> list:
"""
Returns a friendly entity's specific attributes
Returns a friendly entity's specific attributes.
"""
return ["maxhealth", "health"]
@ -491,7 +571,7 @@ class InventoryHolder(Entity):
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.
"""
for i in range(len(inventory)):
@ -501,7 +581,7 @@ class InventoryHolder(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.
"""
entity_classes = self.get_all_entity_classes_in_a_dict()
@ -511,7 +591,7 @@ class InventoryHolder(Entity):
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["hazel"] = self.hazel
@ -520,19 +600,19 @@ class InventoryHolder(Entity):
def add_to_inventory(self, obj: Any) -> None:
"""
Adds an object to inventory
Adds an object to the inventory.
"""
self.inventory.append(obj)
def remove_from_inventory(self, obj: Any) -> None:
"""
Removes an object from the inventory
Removes an object from the inventory.
"""
self.inventory.remove(obj)
def change_hazel_balance(self, hz: int) -> None:
"""
Change the number of hazel the entity has by hz. hz is negative
when the player loses money and positive when he gains money
Changes the number of hazel the entity has by hz. hz is negative
when the entity loses money and positive when it gains money.
"""
self.hazel += hz

View File

@ -14,7 +14,7 @@ from .translations import gettext as _, Translator
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
@ -23,26 +23,26 @@ class Menu:
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)
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)
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]
class MainMenuValues(Enum):
"""
Values of the main menu
Values of the main menu.
"""
START = "New game"
RESUME = "Resume"
@ -57,14 +57,14 @@ class MainMenuValues(Enum):
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]
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
@ -75,7 +75,7 @@ class SettingsMenu(Menu):
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
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:
# Navigate normally through the menu.
@ -121,22 +121,40 @@ class SettingsMenu(Menu):
class InventoryMenu(Menu):
"""
A special instance of a menu : the menu for the inventory of the player.
"""
player: Player
def update_player(self, player: Player) -> None:
"""
Updates the player.
"""
self.player = player
@property
def values(self) -> list:
"""
Returns the values of the menu.
"""
return self.player.inventory
class StoreMenu(Menu):
"""
A special instance of a menu : the menu for the inventory of a merchant.
"""
merchant: Merchant = None
def update_merchant(self, merchant: Merchant) -> None:
"""
Updates the merchant.
"""
self.merchant = merchant
@property
def values(self) -> list:
"""
Returns the values of the menu.
"""
return self.merchant.inventory if self.merchant else []

View File

@ -13,9 +13,10 @@ from .translations import gettext as _
class Settings:
"""
This class stores the settings of the game.
Settings can be get by using for example settings.TEXTURE_PACK directly.
The comment can be get by using settings.get_comment('TEXTURE_PACK').
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
Settings can be obtained by using for example settings.TEXTURE_PACK
directly.
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):
self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
@ -50,7 +51,7 @@ class Settings:
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:
return _(object.__getattribute__(self, item)[1])
@ -61,13 +62,13 @@ class Settings:
@property
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__)
def loads_from_string(self, json_str: str) -> None:
"""
Dump settings
Loads settings.
"""
d = json.loads(json_str)
for key in d:
@ -75,7 +76,7 @@ class Settings:
def dumps_to_string(self) -> str:
"""
Dump settings
Dumps settings.
"""
d = dict()
for key in self.settings_keys:

View File

@ -8,7 +8,7 @@ from types import TracebackType
class TermManager: # pragma: no cover
"""
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):
self.screen = curses.initscr()

View File

@ -6,6 +6,7 @@ import unittest
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item, \
Explosion
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Trumpet
from squirrelbattle.entities.player import Player
from squirrelbattle.interfaces import Entity, Map
from squirrelbattle.resources import ResourceManager
@ -14,7 +15,7 @@ from squirrelbattle.resources import ResourceManager
class TestEntities(unittest.TestCase):
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.player = Player()
@ -23,7 +24,7 @@ class TestEntities(unittest.TestCase):
def test_basic_entities(self) -> None:
"""
Test some random stuff with basic entities.
Tests some random stuff with basic entities.
"""
entity = Entity()
entity.move(42, 64)
@ -38,7 +39,7 @@ class TestEntities(unittest.TestCase):
def test_fighting_entities(self) -> None:
"""
Test some random stuff with fighting entities.
Tests some random stuff with fighting entities.
"""
entity = Tiger()
self.map.add_entity(entity)
@ -57,17 +58,17 @@ class TestEntities(unittest.TestCase):
self.map.add_entity(entity)
entity.move(15, 44)
# Move randomly
self.map.tick()
self.map.tick(self.player)
self.assertFalse(entity.y == 15 and entity.x == 44)
# Move to the player
entity.move(3, 6)
self.map.tick()
self.map.tick(self.player)
self.assertTrue(entity.y == 2 and entity.x == 6)
# Rabbit should fight
old_health = self.player.health
self.map.tick()
self.map.tick(self.player)
self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health)
self.assertEqual(self.map.logs.messages[-1],
@ -89,9 +90,50 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.dead)
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:
"""
Test some random stuff with items.
Tests some random stuff with items.
"""
item = Item()
self.map.add_entity(item)
@ -112,7 +154,7 @@ class TestEntities(unittest.TestCase):
def test_bombs(self) -> None:
"""
Test some random stuff with bombs.
Tests some random stuff with bombs.
"""
item = Bomb()
hedgehog = Hedgehog()
@ -156,7 +198,7 @@ class TestEntities(unittest.TestCase):
def test_hearts(self) -> None:
"""
Test some random stuff with hearts.
Tests some random stuff with hearts.
"""
item = Heart()
self.map.add_entity(item)
@ -171,7 +213,7 @@ class TestEntities(unittest.TestCase):
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()
self.map.add_entity(item)
@ -189,7 +231,7 @@ class TestEntities(unittest.TestCase):
def test_players(self) -> None:
"""
Test some random stuff with players.
Tests some random stuff with players.
"""
player = Player()
self.map.add_entity(player)

View File

@ -22,7 +22,7 @@ from ..translations import gettext as _, Translator
class TestGame(unittest.TestCase):
def setUp(self) -> None:
"""
Setup game.
Sets the game up.
"""
self.game = Game()
self.game.new_game()
@ -36,7 +36,7 @@ class TestGame(unittest.TestCase):
def test_load_game(self) -> None:
"""
Save a game and reload it.
Saves a game and reloads it.
"""
bomb = Bomb()
self.game.map.add_entity(bomb)
@ -46,6 +46,11 @@ class TestGame(unittest.TestCase):
bomb.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
merchant = Merchant()
merchant.move(3, 6)
@ -90,7 +95,7 @@ class TestGame(unittest.TestCase):
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.
Yeah, that's only for coverage.
"""
@ -99,7 +104,7 @@ class TestGame(unittest.TestCase):
def test_key_translation(self) -> None:
"""
Test key bindings.
Tests key bindings.
"""
self.game.settings = Settings()
@ -155,7 +160,7 @@ class TestGame(unittest.TestCase):
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.main_menu.validate(),
@ -246,7 +251,7 @@ class TestGame(unittest.TestCase):
def test_mouse_click(self) -> None:
"""
Simulate mouse clicks.
Simulates mouse clicks.
"""
self.game.state = GameMode.MAINMENU
@ -276,7 +281,7 @@ class TestGame(unittest.TestCase):
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_player = self.game.player
@ -299,7 +304,7 @@ class TestGame(unittest.TestCase):
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()
@ -385,7 +390,7 @@ class TestGame(unittest.TestCase):
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
# Kill player
@ -401,14 +406,14 @@ class TestGame(unittest.TestCase):
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.update, None, self.game)
def test_messages(self) -> None:
"""
Display error messages.
Displays error messages.
"""
self.game.message = "I am an error"
self.game.display_actions(DisplayActions.UPDATE)
@ -418,7 +423,7 @@ class TestGame(unittest.TestCase):
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
# Open and close the inventory
@ -479,7 +484,7 @@ class TestGame(unittest.TestCase):
def test_talk_to_sunflowers(self) -> None:
"""
Interact with sunflowers
Interacts with sunflowers.
"""
self.game.state = GameMode.PLAY
@ -530,7 +535,7 @@ class TestGame(unittest.TestCase):
def test_talk_to_merchant(self) -> None:
"""
Interact with merchants
Interacts with merchants.
"""
self.game.state = GameMode.PLAY
@ -609,3 +614,19 @@ class TestGame(unittest.TestCase):
# Exit the menu
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY)
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):
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")
self.assertEqual(m.width, 2)
@ -20,7 +20,7 @@ class TestInterfaces(unittest.TestCase):
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"))
self.assertEqual(m.width, 52)
@ -28,7 +28,7 @@ class TestInterfaces(unittest.TestCase):
def test_tiles(self) -> None:
"""
Test some things about tiles.
Tests some things about tiles.
"""
self.assertFalse(Tile.FLOOR.is_wall())
self.assertTrue(Tile.WALL.is_wall())

View File

@ -24,3 +24,6 @@ class FakePad:
def getmaxyx(self) -> Tuple[int, int]:
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:
"""
Ensure that settings are well loaded.
Ensures that settings are well loaded.
"""
settings = Settings()
self.assertEqual(settings.KEY_UP_PRIMARY, 'z')

View File

@ -11,7 +11,7 @@ class TestTranslations(unittest.TestCase):
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(_("Resume"), "Continuer")
@ -22,7 +22,7 @@ class TestTranslations(unittest.TestCase):
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"),
"Touche principale pour aller vers le haut")

View File

@ -13,7 +13,7 @@ class Translator:
"""
This module uses gettext to translate 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"]
locale: str = "en"
@ -22,7 +22,7 @@ class Translator:
@classmethod
def refresh_translations(cls) -> None:
"""
Load compiled translations.
Loads compiled translations.
"""
for language in cls.SUPPORTED_LOCALES:
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
@ -37,7 +37,7 @@ class Translator:
@classmethod
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.
"""
lang = lang[:2]
@ -51,7 +51,7 @@ class Translator:
@classmethod
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:
if language == "en":
@ -83,7 +83,7 @@ class Translator:
@classmethod
def compilemessages(cls) -> None:
"""
Compile translation messages from source files.
Compiles translation messages from source files.
"""
for language in cls.SUPPORTED_LOCALES:
if language == "en":
@ -99,7 +99,7 @@ class Translator:
def gettext(message: str) -> str:
"""
Translate a message.
Translates a message.
"""
return Translator.get_translator().gettext(message)