Merge branch 'master' into 'ladders'

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

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")
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")
@ -36,8 +43,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
@ -66,9 +73,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
@ -126,6 +133,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
@ -136,6 +146,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()
@ -144,10 +157,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)
@ -177,7 +190,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
@ -191,7 +204,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)
@ -212,7 +227,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)
@ -233,6 +250,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"
@ -75,6 +79,7 @@ TexturePack.ASCII_PACK = TexturePack(
SWORD='\u2020',
TEDDY_BEAR='8',
TIGER='n',
TRUMPET='/',
WALL='#',
)
@ -102,5 +107,6 @@ TexturePack.SQUIRREL_PACK = TexturePack(
SWORD='🗡️ ',
TEDDY_BEAR='🧸',
TIGER='🐅',
TRUMPET='🎺',
WALL='🧱',
)