Merge remote-tracking branch 'origin/master' into village

# Conflicts:
#	squirrelbattle/display/texturepack.py
#	squirrelbattle/interfaces.py
This commit is contained in:
Yohann D'ANELLO
2020-12-01 17:07:40 +01:00
52 changed files with 1763 additions and 206 deletions

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.game import Game
from squirrelbattle.display.display_manager import DisplayManager
from squirrelbattle.term_manager import TermManager

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import Any, Optional, Union
@ -19,6 +22,24 @@ class Display:
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
return curses.newpad(height, width) if self.screen else FakePad()
def truncate(self, msg: str, height: int, width: int) -> str:
height = max(0, height)
width = max(0, width)
lines = msg.split("\n")
lines = lines[:height]
lines = [line[:width] for line in lines]
return "\n".join(lines)
def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None:
"""
Display a message onto the pad.
If the message is too large, it is truncated vertically and horizontally
"""
height, width = pad.getmaxyx()
msg = self.truncate(msg, height - y, width - x - 1)
if msg.replace("\n", "") and x >= 0 and y >= 0:
return pad.addstr(y, x, msg, *options)
def init_pair(self, number: int, foreground: int, background: int) -> None:
return curses.init_pair(number, foreground, background) \
if self.screen else None
@ -32,14 +53,36 @@ class Display:
self.y = y
self.width = width
self.height = height
if hasattr(self, "pad") and resize_pad:
self.pad.resize(self.height, self.width)
if hasattr(self, "pad") and resize_pad and \
self.height >= 0 and self.width >= 0:
self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
if len(args) == 4:
self.resize(*args, resize_pad)
self.display()
def refresh_pad(self, pad: Any, top_y: int, top_x: int,
window_y: int, window_x: int,
last_y: int, last_x: int) -> None:
"""
Refresh 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
is drawn and no error is raised.
"""
top_y, top_x = max(0, top_y), max(0, top_x)
window_y, window_x = max(0, window_y), max(0, window_x)
screen_max_y, screen_max_x = self.screen.getmaxyx() if self.screen \
else (42, 42)
last_y, last_x = min(screen_max_y - 1, last_y), \
min(screen_max_x - 1, last_x)
if last_y >= window_y and last_x >= window_x:
# Refresh the pad only if coordinates are valid
pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x)
def display(self) -> None:
raise NotImplementedError
@ -50,3 +93,68 @@ class Display:
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42
class VerticalSplit(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, 1)
@property
def width(self) -> int:
return 1
@width.setter
def width(self, val: Any) -> None:
pass
def display(self) -> None:
for i in range(self.height):
self.addstr(self.pad, i, 0, "")
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x)
class HorizontalSplit(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(1, self.cols)
@property
def height(self) -> int:
return 1
@height.setter
def height(self, val: Any) -> None:
pass
def display(self) -> None:
for i in range(self.width):
self.addstr(self.pad, 0, i, "")
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y,
self.x + self.width - 1)
class Box(Display):
def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.fg_border_color = fg_border_color or curses.COLOR_WHITE
pair_number = 4 + self.fg_border_color
self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK)
self.pair = self.color_pair(pair_number)
def display(self) -> None:
self.addstr(self.pad, 0, 0, "" + "" * (self.width - 2) + "",
self.pair)
for i in range(1, self.height - 1):
self.addstr(self.pad, i, 0, "", self.pair)
self.addstr(self.pad, i, self.width - 1, "", self.pair)
self.addstr(self.pad, self.height - 1, 0,
"" + "" * (self.width - 2) + "", self.pair)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1)

View File

@ -1,5 +1,10 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit
from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay
from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \
MainMenuDisplay
@ -22,9 +27,12 @@ class DisplayManager:
screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.logsdisplay = LogsDisplay(screen, pack)
self.messagedisplay = MessageDisplay(screen=screen, pack=None)
self.hbar = HorizontalSplit(screen, pack)
self.vbar = VerticalSplit(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay]
self.logsdisplay, self.messagedisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None:
@ -40,20 +48,35 @@ class DisplayManager:
self.statsdisplay.update_player(self.game.player)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message)
def refresh(self) -> None:
if self.game.state == GameMode.PLAY:
# The map pad has already the good size
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols,
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5,
self.mapdisplay.pack.tile_width
* (self.cols * 4 // 5
// self.mapdisplay.pack.tile_width),
resize_pad=False)
self.statsdisplay.refresh(self.rows * 4 // 5, 0,
self.rows // 10, self.cols)
self.logsdisplay.refresh(self.rows * 9 // 10, 0,
self.rows // 10, self.cols)
self.statsdisplay.refresh(0, self.cols * 4 // 5 + 1,
self.rows, self.cols // 5 - 1)
self.logsdisplay.refresh(self.rows * 4 // 5 + 1, 0,
self.rows // 5 - 1, self.cols * 4 // 5)
self.hbar.refresh(self.rows * 4 // 5, 0, 1, self.cols * 4 // 5)
self.vbar.refresh(0, self.cols * 4 // 5, self.rows, 1)
if self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
if self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1)
if self.game.message:
height, width = 0, 0
for line in self.game.message.split("\n"):
height += 1
width = max(width, len(line))
y, x = (self.rows - height) // 2, (self.cols - width) // 2
self.messagedisplay.refresh(y, x, height, width)
self.resize_window()
def resize_window(self) -> bool:

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.display.display import Display
from squirrelbattle.interfaces import Logs
@ -12,12 +15,11 @@ class LogsDisplay(Display):
self.logs = logs
def display(self) -> None:
print(type(self.logs.messages), flush=True)
messages = self.logs.messages[-self.height:]
messages = messages[::-1]
self.pad.clear()
self.pad.erase()
for i in range(min(self.height, len(messages))):
self.pad.addstr(self.height - i - 1, self.x,
messages[i][:self.width])
self.pad.refresh(0, 0, self.y, self.x, self.y + self.height,
self.x + self.width)
self.addstr(self.pad, self.height - i - 1, self.x,
messages[i][:self.width])
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1)

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.interfaces import Map
from .display import Display
@ -15,11 +17,11 @@ class MapDisplay(Display):
def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.pad.addstr(0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for e in self.map.entities:
self.pad.addstr(e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
@ -31,9 +33,18 @@ class MapDisplay(Display):
smaxrow = min(smaxrow, self.height - 1)
smaxcol = self.pack.tile_width * self.map.width - \
(x + deltax) + self.width - 1
# Wrap perfectly the map according to the width of the tiles
pmincol = self.pack.tile_width * (pmincol // self.pack.tile_width)
smincol = self.pack.tile_width * (smincol // self.pack.tile_width)
smaxcol = self.pack.tile_width \
* (smaxcol // self.pack.tile_width + 1) - 1
smaxcol = min(smaxcol, self.width - 1)
pminrow = max(0, min(self.map.height, pminrow))
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
self.pad.clear()
self.pad.erase()
self.update_pad()
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)
self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow,
smaxcol)

View File

@ -1,8 +1,12 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import List
from squirrelbattle.menus import Menu, MainMenu
from .display import Display
from .display import Display, Box
from ..resources import ResourceManager
from ..translations import gettext as _
class MenuDisplay(Display):
@ -11,25 +15,23 @@ class MenuDisplay(Display):
"""
position: int
def __init__(self, *args):
super().__init__(*args)
self.menubox = self.newpad(self.rows, self.cols)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.menubox = Box(*args, **kwargs)
def update_menu(self, menu: Menu) -> None:
self.menu = menu
self.trueheight = len(self.values)
self.truewidth = max([len(a) for a in self.values])
# Menu values are printed in pad
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i])
self.addstr(self.pad, i, 0, " " + self.values[i])
def update_pad(self) -> None:
for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i])
self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line
self.pad.addstr(self.menu.position, 0, ">")
self.addstr(self.pad, self.menu.position, 0, ">")
def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
@ -37,20 +39,21 @@ class MenuDisplay(Display):
if self.height - 2 >= self.trueheight - self.menu.position else 0
# Menu box
self.menubox.addstr(0, 0, "" + "" * (self.width - 2) + "")
for i in range(1, self.height - 1):
self.menubox.addstr(i, 0, "" + " " * (self.width - 2) + "")
self.menubox.addstr(self.height - 1, 0,
"" + "" * (self.width - 2) + "")
self.menubox.refresh(0, 0, self.y, self.x,
self.height + self.y,
self.width + self.x)
self.menubox.refresh(self.y, self.x, self.height, self.width)
self.pad.erase()
self.update_pad()
self.pad.refresh(cornery, 0, self.y + 1, self.x + 2,
self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2,
self.height - 2 + self.y,
self.width - 2 + self.x)
@property
def truewidth(self) -> int:
return max([len(str(a)) for a in self.values])
@property
def trueheight(self) -> int:
return len(self.values)
@property
def preferred_width(self) -> int:
return self.truewidth + 6
@ -70,9 +73,10 @@ class SettingsMenuDisplay(MenuDisplay):
"""
@property
def values(self) -> List[str]:
return [a[1][1] + (" : "
return [_(a[1][1]) + (" : "
+ ("?" if self.menu.waiting_for_key
and a == self.menu.validate() else a[1][0])
and a == self.menu.validate() else a[1][0]
.replace("\n", "\\n"))
if a[1][0] else "") for a in self.menu.values]
@ -95,9 +99,11 @@ class MainMenuDisplay(Display):
def display(self) -> None:
for i in range(len(self.title)):
self.pad.addstr(4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i])
self.pad.refresh(0, 0, self.y, self.x, self.height, self.width)
self.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i])
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)
menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
self.menudisplay.refresh(

View File

@ -0,0 +1,31 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.display import Box, Display
class MessageDisplay(Display):
"""
Display a message in a popup.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.box = Box(fg_border_color=curses.COLOR_RED, *args, **kwargs)
self.message = ""
self.pad = self.newpad(1, 1)
def update_message(self, msg: str) -> None:
self.message = msg
def display(self) -> None:
self.box.refresh(self.y - 1, self.x - 2,
self.height + 2, self.width + 4)
self.box.display()
self.pad.erase()
self.addstr(self.pad, 0, 0, self.message, curses.A_BOLD)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)

View File

@ -1,9 +1,12 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from ..entities.player import Player
from ..translations import gettext as _
from .display import Display
from squirrelbattle.entities.player import Player
class StatsDisplay(Display):
player: Player
@ -17,36 +20,28 @@ class StatsDisplay(Display):
self.player = p
def update_pad(self) -> None:
string = ""
for _ in range(self.width - 1):
string = string + "-"
self.pad.addstr(0, 0, string)
string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\
string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
for _ in range(self.width - len(string2) - 1):
string2 = string2 + " "
self.pad.addstr(1, 0, string2)
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
self.addstr(self.pad, 0, 0, string2)
string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
for _ in range(self.width - len(string3) - 1):
string3 = string3 + " "
self.pad.addstr(2, 0, string3)
self.addstr(self.pad, 3, 0, string3)
inventory_str = "Inventaire : " + "".join(
inventory_str = _("Inventory:") + " " + "".join(
self.pack[item.name.upper()] for item in self.player.inventory)
self.pad.addstr(3, 0, inventory_str)
self.addstr(self.pad, 8, 0, inventory_str)
if self.player.dead:
self.pad.addstr(4, 0, "VOUS ÊTES MORT",
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
self.addstr(self.pad, 10, 0, _("YOU ARE DEAD"),
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
def display(self) -> None:
self.pad.clear()
self.pad.erase()
self.update_pad()
self.pad.refresh(0, 0, self.y, self.x,
4 + self.y, self.width + self.x)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.width + self.x - 1)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import Any
@ -51,7 +54,7 @@ TexturePack.ASCII_PACK = TexturePack(
HEART='',
BOMB='o',
RABBIT='Y',
BEAVER='_',
TIGER='n',
TEDDY_BEAR='8',
MERCHANT='M',
SUNFLOWER='I',
@ -67,12 +70,12 @@ TexturePack.SQUIRREL_PACK = TexturePack(
EMPTY=' ',
WALL='🧱',
FLOOR='██',
PLAYER='🐿️️',
PLAYER='🐿️ ',
HEDGEHOG='🦔',
HEART='💜',
BOMB='💣',
RABBIT='🐇',
BEAVER='🦫',
TIGER='🐅',
TEDDY_BEAR='🧸',
MERCHANT='🦜',
SUNFLOWER='🌻',

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional
from .player import Player

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice
from .player import Player
@ -52,13 +55,13 @@ class Monster(FightingEntity):
break
class Beaver(Monster):
class Tiger(Monster):
"""
A beaver monster
A tiger monster
"""
def __init__(self, strength: int = 2, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="beaver", strength=strength,
super().__init__(name="tiger", strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import randint
from typing import Dict, Tuple

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto
from typing import Optional

View File

@ -1,3 +1,7 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from json import JSONDecodeError
from random import randint
from typing import Any, Optional
import json
@ -10,6 +14,7 @@ from .interfaces import Map, Logs
from .resources import ResourceManager
from .settings import Settings
from . import menus
from .translations import gettext as _, Translator
from typing import Callable
@ -27,13 +32,15 @@ class Game:
Init the game.
"""
self.state = GameMode.MAINMENU
self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings = Settings()
self.settings.load_settings()
self.settings.write_settings()
Translator.setlocale(self.settings.LOCALE)
self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings_menu.update_values(self.settings)
self.logs = Logs()
self.message = None
def new_game(self) -> None:
"""
@ -55,7 +62,7 @@ class Game:
when the given key gets pressed.
"""
while True: # pragma no cover
screen.clear()
screen.erase()
screen.refresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
@ -68,6 +75,11 @@ class Game:
Indicates what should be done when the given key is pressed,
according to the current game state.
"""
if self.message:
self.message = None
self.display_actions(DisplayActions.REFRESH)
return
if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU:
@ -130,9 +142,24 @@ class Game:
"""
Loads the game from a dictionary
"""
self.map.load_state(d)
# noinspection PyTypeChecker
self.player = self.map.find_entities(Player)[0]
try:
self.map.load_state(d)
except KeyError:
self.message = _("Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted.")
os.unlink(ResourceManager.get_config_path("save.json"))
self.display_actions(DisplayActions.UPDATE)
return
players = self.map.find_entities(Player)
if not players:
self.message = _("No player was found on this map!\n"
"Maybe you died?")
self.player.health = 0
self.display_actions(DisplayActions.UPDATE)
return
self.player = players[0]
self.display_actions(DisplayActions.UPDATE)
def load_game(self) -> None:
@ -142,7 +169,15 @@ class Game:
file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path):
with open(file_path, "r") as f:
self.load_state(json.loads(f.read()))
try:
state = json.loads(f.read())
self.load_state(state)
except JSONDecodeError:
self.message = _("The JSON file is not correct.\n"
"Your save seems corrupted. "
"It got deleted.")
os.unlink(file_path)
self.display_actions(DisplayActions.UPDATE)
def save_game(self) -> None:
"""

View File

@ -1,11 +1,13 @@
#!/usr/bin/env python
from random import randint
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List, Optional, Any
from squirrelbattle.display.texturepack import TexturePack
from .display.texturepack import TexturePack
from .translations import gettext as _
class Logs:
@ -127,7 +129,7 @@ class Map:
"""
Put randomly {count} hedgehogs on the map, where it is available.
"""
for _ in range(count):
for ignored in range(count):
y, x = 0, 0
while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
@ -319,16 +321,20 @@ class Entity:
"""
return isinstance(self, FriendlyEntity)
@property
def translated_name(self) -> str:
return _(self.name.replace("_", " "))
@staticmethod
def get_all_entity_classes():
"""
Returns all entities subclasses
"""
from squirrelbattle.entities.items import Heart, Bomb
from squirrelbattle.entities.monsters import Beaver, Hedgehog, \
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Merchant,Sunflower
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,Sunflower]
return [Tiger, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,Sunflower]
@staticmethod
def get_all_entity_classes_in_a_dict() -> dict:
@ -336,12 +342,12 @@ class Entity:
Returns all entities subclasses in a dictionary
"""
from squirrelbattle.entities.player import Player
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear
from squirrelbattle.entities.items import Bomb, Heart
from squirrelbattle.entities.friendly import Merchant,Sunflower
return {
"Beaver": Beaver,
"Tiger": Tiger,
"Bomb": Bomb,
"Heart": Heart,
"Hedgehog": Hedgehog,
@ -399,8 +405,10 @@ class FightingEntity(Entity):
"""
Deals damage to the opponent, based on the stats
"""
return f"{self.name} hits {opponent.name}. "\
+ opponent.take_damage(self, self.strength)
return _("{name} hits {opponent}.")\
.format(name=_(self.translated_name.capitalize()),
opponent=_(opponent.translated_name)) + " " + \
opponent.take_damage(self, self.strength)
def take_damage(self, attacker: "Entity", amount: int) -> str:
"""
@ -409,8 +417,11 @@ class FightingEntity(Entity):
self.health -= amount
if self.health <= 0:
self.die()
return f"{self.name} takes {amount} damage."\
+ (f" {self.name} dies." if self.health <= 0 else "")
return _("{name} takes {amount} damage.")\
.format(name=self.translated_name.capitalize(), amount=str(amount))\
+ (" " + _("{name} dies.")
.format(name=self.translated_name.capitalize())
if self.health <= 0 else "")
def die(self) -> None:
"""
@ -451,5 +462,5 @@ class FriendlyEntity(Entity):
def talk_to(self, player : Any) -> str:
a = randint(0,len(self.dialogue_option)-1)
return "The sunflower said : "+self.dialogue_option[a]

View File

@ -0,0 +1,166 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-11-28 16:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Neu Spiel"
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Weitergehen"
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Laden"
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Speichern"
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Einstellungen"
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Verlassen"
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Haupttaste zum Obengehen"
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Sekundärtaste zum Obengehen"
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Haupttaste zum Untergehen"
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Sekundärtaste zum Untergehen"
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Haupttaste zum Linksgehen"
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Sekundärtaste zum Linksgehen"
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Haupttaste zum Rechtsgehen"
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Sekundärtaste zum Rechtsgehen"
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Menütaste"
#: squirrelbattle/tests/translations_test.py:45
msgid "Texture pack"
msgstr "Textur-Packung"
#: squirrelbattle/tests/translations_test.py:46
msgid "Language"
msgstr "Sprache"
#: squirrelbattle/tests/translations_test.py:49
msgid "player"
msgstr "Spieler"
#: squirrelbattle/tests/translations_test.py:51
msgid "tiger"
msgstr "Tiger"
#: squirrelbattle/tests/translations_test.py:52
msgid "hedgehog"
msgstr "Igel"
#: squirrelbattle/tests/translations_test.py:53
msgid "rabbit"
msgstr "Kanninchen"
#: squirrelbattle/tests/translations_test.py:54
msgid "teddy bear"
msgstr "Teddybär"
#: squirrelbattle/tests/translations_test.py:56
msgid "bomb"
msgstr "Bombe"
#: squirrelbattle/tests/translations_test.py:57
msgid "heart"
msgstr "Herz"
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Bestand:"
#: squirrelbattle/display/statsdisplay.py:39
msgid "YOU ARE DEAD"
msgstr "SIE WURDEN GESTORBEN"
#: squirrelbattle/interfaces.py:398
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} schlägt {opponent}."
#: squirrelbattle/interfaces.py:410
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} dies."
msgstr "{name} stirbt."
#: squirrelbattle/menus.py:71
msgid "Back"
msgstr "Zurück"
#: squirrelbattle/game.py:148
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
"In Ihrer Speicherdatei fehlen einige Schlüssel.\n"
"Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht."
#: squirrelbattle/game.py:156
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
"Auf dieser Karte wurde kein Spieler gefunden!\n"
"Vielleicht sind Sie gestorben?"
#: squirrelbattle/game.py:176
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
"Die JSON-Datei ist nicht korrekt.\n"
"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht."

View File

@ -0,0 +1,195 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-11-28 16:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:39
msgid "YOU ARE DEAD"
msgstr ""
#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr ""
#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr ""
#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14
#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr ""
#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr ""
#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr ""
#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr ""
#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr ""
#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr ""
#: squirrelbattle/menus.py:71
msgid "Back"
msgstr ""
#: squirrelbattle/game.py:147 squirrelbattle/game.py:148
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
#: squirrelbattle/game.py:155 squirrelbattle/game.py:156
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
#: squirrelbattle/game.py:175 squirrelbattle/game.py:176
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21
#: squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr ""
#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23
#: squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr ""
#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr ""
#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr ""
#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr ""
#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr ""
#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr ""
#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr ""
#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:41
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr ""
#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:43
#: squirrelbattle/tests/translations_test.py:45
msgid "Texture pack"
msgstr ""
#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40
#: squirrelbattle/tests/translations_test.py:44
#: squirrelbattle/tests/translations_test.py:46
msgid "Language"
msgstr ""
#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} dies."
msgstr ""
#: squirrelbattle/tests/translations_test.py:47
#: squirrelbattle/tests/translations_test.py:49
msgid "player"
msgstr ""
#: squirrelbattle/tests/translations_test.py:49
#: squirrelbattle/tests/translations_test.py:51
msgid "tiger"
msgstr ""
#: squirrelbattle/tests/translations_test.py:50
#: squirrelbattle/tests/translations_test.py:52
msgid "hedgehog"
msgstr ""
#: squirrelbattle/tests/translations_test.py:51
#: squirrelbattle/tests/translations_test.py:53
msgid "rabbit"
msgstr ""
#: squirrelbattle/tests/translations_test.py:52
#: squirrelbattle/tests/translations_test.py:54
msgid "teddy bear"
msgstr ""
#: squirrelbattle/tests/translations_test.py:54
#: squirrelbattle/tests/translations_test.py:56
msgid "bomb"
msgstr ""
#: squirrelbattle/tests/translations_test.py:55
#: squirrelbattle/tests/translations_test.py:57
msgid "heart"
msgstr ""

View File

@ -0,0 +1,201 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-11-28 16:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Inventaire :"
#: squirrelbattle/display/statsdisplay.py:39
msgid "YOU ARE DEAD"
msgstr "VOUS ÊTES MORT"
#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} frappe {opponent}."
#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14
#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nouvelle partie"
#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Continuer"
#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Sauvegarder"
#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Charger"
#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Paramètres"
#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Quitter"
#: squirrelbattle/menus.py:71
msgid "Back"
msgstr "Retour"
#: squirrelbattle/game.py:147 squirrelbattle/game.py:148
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
"Certaines clés de votre ficher de sauvegarde sont manquantes.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/game.py:155 squirrelbattle/game.py:156
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
"Aucun joueur n'a été trouvé sur la carte !\n"
"Peut-être êtes-vous mort ?"
#: squirrelbattle/game.py:175 squirrelbattle/game.py:176
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
"Le fichier JSON de sauvegarde est incorrect.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21
#: squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Touche principale pour aller vers le haut"
#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23
#: squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Touche secondaire pour aller vers le haut"
#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Touche principale pour aller vers le bas"
#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Touche secondaire pour aller vers le bas"
#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Touche principale pour aller vers la gauche"
#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Touche secondaire pour aller vers la gauche"
#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Touche principale pour aller vers la droite"
#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Touche secondaire pour aller vers la droite"
#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:41
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Touche pour valider un menu"
#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:43
#: squirrelbattle/tests/translations_test.py:45
msgid "Texture pack"
msgstr "Pack de textures"
#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40
#: squirrelbattle/tests/translations_test.py:44
#: squirrelbattle/tests/translations_test.py:46
msgid "Language"
msgstr "Langue"
#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} dies."
msgstr "{name} meurt."
#: squirrelbattle/tests/translations_test.py:47
#: squirrelbattle/tests/translations_test.py:49
msgid "player"
msgstr "joueur"
#: squirrelbattle/tests/translations_test.py:49
#: squirrelbattle/tests/translations_test.py:51
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:50
#: squirrelbattle/tests/translations_test.py:52
msgid "hedgehog"
msgstr "hérisson"
#: squirrelbattle/tests/translations_test.py:51
#: squirrelbattle/tests/translations_test.py:53
msgid "rabbit"
msgstr "lapin"
#: squirrelbattle/tests/translations_test.py:52
#: squirrelbattle/tests/translations_test.py:54
msgid "teddy bear"
msgstr "nounours"
#: squirrelbattle/tests/translations_test.py:54
#: squirrelbattle/tests/translations_test.py:56
msgid "bomb"
msgstr "bombe"
#: squirrelbattle/tests/translations_test.py:55
#: squirrelbattle/tests/translations_test.py:57
msgid "heart"
msgstr "cœur"

View File

@ -1,9 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum
from typing import Any, Optional
from .display.texturepack import TexturePack
from .enums import GameMode, KeyValues, DisplayActions
from .settings import Settings
from .translations import gettext as _, Translator
class Menu:
@ -38,15 +42,15 @@ class MainMenuValues(Enum):
"""
Values of the main menu
"""
START = 'Nouvelle partie'
RESUME = 'Continuer'
SAVE = 'Sauvegarder'
LOAD = 'Charger'
SETTINGS = 'Paramètres'
EXIT = 'Quitter'
START = "New game"
RESUME = "Resume"
SAVE = "Save"
LOAD = "Load"
SETTINGS = "Settings"
EXIT = "Exit"
def __str__(self):
return self.value
return _(self.value)
class MainMenu(Menu):
@ -64,7 +68,7 @@ class SettingsMenu(Menu):
def update_values(self, settings: Settings) -> None:
self.values = list(settings.__dict__.items())
self.values.append(("RETURN", ["", "Retour"]))
self.values.append(("RETURN", ["", _("Back")]))
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None:
@ -92,6 +96,12 @@ class SettingsMenu(Menu):
game.settings.TEXTURE_PACK)
game.settings.write_settings()
self.update_values(game.settings)
elif option == "LOCALE":
game.settings.LOCALE = 'fr' if game.settings.LOCALE == 'en'\
else 'de' if game.settings.LOCALE == 'fr' else 'en'
Translator.setlocale(game.settings.LOCALE)
game.settings.write_settings()
self.update_values(game.settings)
else:
self.waiting_for_key = True
self.update_values(game.settings)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path

View File

@ -1,8 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import locale
import os
from typing import Any, Generator
from .resources import ResourceManager
from .translations import gettext as _
class Settings:
@ -13,25 +18,17 @@ class Settings:
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
"""
def __init__(self):
self.KEY_UP_PRIMARY = \
['z', 'Touche principale pour aller vers le haut']
self.KEY_UP_SECONDARY = \
['KEY_UP', 'Touche secondaire pour aller vers le haut']
self.KEY_DOWN_PRIMARY = \
['s', 'Touche principale pour aller vers le bas']
self.KEY_DOWN_SECONDARY = \
['KEY_DOWN', 'Touche secondaire pour aller vers le bas']
self.KEY_LEFT_PRIMARY = \
['q', 'Touche principale pour aller vers la gauche']
self.KEY_LEFT_SECONDARY = \
['KEY_LEFT', 'Touche secondaire pour aller vers la gauche']
self.KEY_RIGHT_PRIMARY = \
['d', 'Touche principale pour aller vers la droite']
self.KEY_RIGHT_SECONDARY = \
['KEY_RIGHT', 'Touche secondaire pour aller vers la droite']
self.KEY_ENTER = \
['\n', 'Touche pour valider un menu']
self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé']
self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
self.KEY_UP_SECONDARY = ['KEY_UP', 'Secondary key to move up']
self.KEY_DOWN_PRIMARY = ['s', 'Main key to move down']
self.KEY_DOWN_SECONDARY = ['KEY_DOWN', 'Secondary key to move down']
self.KEY_LEFT_PRIMARY = ['q', 'Main key to move left']
self.KEY_LEFT_SECONDARY = ['KEY_LEFT', 'Secondary key to move left']
self.KEY_RIGHT_PRIMARY = ['d', 'Main key to move right']
self.KEY_RIGHT_SECONDARY = ['KEY_RIGHT', 'Secondary key to move right']
self.KEY_ENTER = ['\n', 'Key to validate a menu']
self.TEXTURE_PACK = ['ascii', 'Texture pack']
self.LOCALE = [locale.getlocale()[0][:2], 'Language']
def __getattribute__(self, item: str) -> Any:
superattribute = super().__getattribute__(item)
@ -50,10 +47,10 @@ class Settings:
Retrieve the comment of a setting.
"""
if item in self.settings_keys:
return object.__getattribute__(self, item)[1]
return _(object.__getattribute__(self, item)[1])
for key in self.settings_keys:
if getattr(self, key) == item:
return object.__getattribute__(self, key)[1]
return _(object.__getattribute__(self, key)[1])
@property
def settings_keys(self) -> Generator[str, Any, None]:

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from types import TracebackType

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,7 +1,10 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest
from squirrelbattle.entities.items import Bomb, Heart, Item
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
from squirrelbattle.entities.player import Player
from squirrelbattle.interfaces import Entity, Map
from squirrelbattle.resources import ResourceManager
@ -36,17 +39,17 @@ class TestEntities(unittest.TestCase):
"""
Test some random stuff with fighting entities.
"""
entity = Beaver()
entity = Tiger()
self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 20)
self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 2)
for _ in range(9):
self.assertEqual(entity.hit(entity),
"beaver hits beaver. beaver takes 2 damage.")
"Tiger hits tiger. Tiger takes 2 damage.")
self.assertFalse(entity.dead)
self.assertEqual(entity.hit(entity), "beaver hits beaver. "
+ "beaver takes 2 damage. beaver dies.")
self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
+ "Tiger takes 2 damage. Tiger dies.")
self.assertTrue(entity.dead)
entity = Rabbit()
@ -67,8 +70,8 @@ class TestEntities(unittest.TestCase):
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],
f"{entity.name} hits {self.player.name}. \
{self.player.name} takes {entity.strength} damage.")
f"{entity.name.capitalize()} hits {self.player.name}. \
{self.player.name.capitalize()} takes {entity.strength} damage.")
# Fight the rabbit
old_health = entity.health

View File

@ -1,13 +1,19 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import unittest
from squirrelbattle.bootstrap import Bootstrap
from squirrelbattle.display.display import Display
from squirrelbattle.display.display_manager import DisplayManager
from squirrelbattle.entities.player import Player
from squirrelbattle.game import Game, KeyValues, GameMode
from squirrelbattle.menus import MainMenuValues
from squirrelbattle.settings import Settings
from ..bootstrap import Bootstrap
from ..display.display import Display
from ..display.display_manager import DisplayManager
from ..entities.player import Player
from ..enums import DisplayActions
from ..game import Game, KeyValues, GameMode
from ..menus import MainMenuValues
from ..resources import ResourceManager
from ..settings import Settings
from ..translations import gettext as _, Translator
class TestGame(unittest.TestCase):
@ -38,6 +44,27 @@ class TestGame(unittest.TestCase):
new_state = self.game.save_state()
self.assertEqual(old_state, new_state)
# Error on loading save
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write("I am not a JSON file")
self.assertIsNone(self.game.message)
self.game.load_game()
self.assertIsNotNone(self.game.message)
self.game.message = None
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write("{}")
self.assertIsNone(self.game.message)
self.game.load_game()
self.assertIsNotNone(self.game.message)
self.game.message = None
# Load game with a dead player
self.game.map.remove_entity(self.game.player)
self.game.save_game()
self.game.load_game()
self.assertIsNotNone(self.game.message)
def test_bootstrap_fail(self) -> None:
"""
Ensure that the test can't play the game,
@ -247,12 +274,23 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
# Change language
Translator.compilemessages()
Translator.refresh_translations()
self.game.settings.LOCALE = "en"
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "fr")
self.assertEqual(_("New game"), "Nouvelle partie")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "de")
self.assertEqual(_("New game"), "Neu Spiel")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "en")
self.assertEqual(_("New game"), "New game")
# Navigate to "back" button
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)
@ -289,3 +327,13 @@ class TestGame(unittest.TestCase):
Check that some functions are not implemented, only for coverage.
"""
self.assertRaises(NotImplementedError, Display.display, None)
def test_messages(self) -> None:
"""
Display error messages.
"""
self.game.message = "I am an error"
self.game.display_actions(DisplayActions.UPDATE)
self.game.display_actions(DisplayActions.REFRESH)
self.game.handle_key_pressed(None, "random key")
self.assertIsNone(self.game.message)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest
from squirrelbattle.display.texturepack import TexturePack

View File

@ -1,3 +1,9 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Tuple
class FakePad:
"""
In order to run tests, we simulate a fake curses pad that accepts functions
@ -10,8 +16,11 @@ class FakePad:
smincol: int, smaxrow: int, smaxcol: int) -> None:
pass
def clear(self) -> None:
def erase(self) -> None:
pass
def resize(self, height: int, width: int) -> None:
pass
def getmaxyx(self) -> Tuple[int, int]:
return 42, 42

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest
from squirrelbattle.settings import Settings
@ -21,7 +24,7 @@ class TestSettings(unittest.TestCase):
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
settings.get_comment('TEXTURE_PACK'))
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
'Pack de textures utilisé')
'Texture pack')
settings.TEXTURE_PACK = 'squirrel'
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')

View File

@ -0,0 +1,57 @@
import unittest
from squirrelbattle.translations import gettext as _, Translator
class TestTranslations(unittest.TestCase):
def setUp(self) -> None:
Translator.compilemessages()
Translator.refresh_translations()
Translator.setlocale("fr")
def test_main_menu_translation(self) -> None:
"""
Ensure that the main menu is translated.
"""
self.assertEqual(_("New game"), "Nouvelle partie")
self.assertEqual(_("Resume"), "Continuer")
self.assertEqual(_("Load"), "Charger")
self.assertEqual(_("Save"), "Sauvegarder")
self.assertEqual(_("Settings"), "Paramètres")
self.assertEqual(_("Exit"), "Quitter")
def test_settings_menu_translation(self) -> None:
"""
Ensure that the settings menu is translated.
"""
self.assertEqual(_("Main key to move up"),
"Touche principale pour aller vers le haut")
self.assertEqual(_("Secondary key to move up"),
"Touche secondaire pour aller vers le haut")
self.assertEqual(_("Main key to move down"),
"Touche principale pour aller vers le bas")
self.assertEqual(_("Secondary key to move down"),
"Touche secondaire pour aller vers le bas")
self.assertEqual(_("Main key to move left"),
"Touche principale pour aller vers la gauche")
self.assertEqual(_("Secondary key to move left"),
"Touche secondaire pour aller vers la gauche")
self.assertEqual(_("Main key to move right"),
"Touche principale pour aller vers la droite")
self.assertEqual(_("Secondary key to move right"),
"Touche secondaire pour aller vers la droite")
self.assertEqual(_("Key to validate a menu"),
"Touche pour valider un menu")
self.assertEqual(_("Texture pack"), "Pack de textures")
self.assertEqual(_("Language"), "Langue")
def test_entities_translation(self) -> None:
self.assertEqual(_("player"), "joueur")
self.assertEqual(_("tiger"), "tigre")
self.assertEqual(_("hedgehog"), "hérisson")
self.assertEqual(_("rabbit"), "lapin")
self.assertEqual(_("teddy bear"), "nounours")
self.assertEqual(_("bomb"), "bombe")
self.assertEqual(_("heart"), "cœur")

View File

@ -0,0 +1,96 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import gettext as gt
import os
import subprocess
from pathlib import Path
from typing import Any, List
class Translator:
"""
This module uses gettext to translate strings.
Translator.setlocale defines the language of the strings,
then gettext() translates the message.
"""
SUPPORTED_LOCALES: List[str] = ["de", "en", "fr"]
locale: str = "en"
translators: dict = {}
@classmethod
def refresh_translations(cls) -> None:
"""
Load compiled translations.
"""
for language in cls.SUPPORTED_LOCALES:
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
rep.mkdir(parents=True) if not rep.is_dir() else None
if os.path.isfile(rep / "squirrelbattle.mo"):
cls.translators[language] = gt.translation(
"squirrelbattle",
localedir=Path(__file__).parent / "locale",
languages=[language],
)
@classmethod
def setlocale(cls, lang: str) -> None:
"""
Define the language used to translate the game.
The language must be supported, otherwise nothing is done.
"""
lang = lang[:2]
if lang in cls.SUPPORTED_LOCALES:
cls.locale = lang
@classmethod
def get_translator(cls) -> Any:
return cls.translators.get(cls.locale, gt.NullTranslations())
@classmethod
def makemessages(cls) -> None: # pragma: no cover
"""
Analyse all strings in the project and extract them.
"""
for language in cls.SUPPORTED_LOCALES:
file_name = Path(__file__).parent / "locale" / language \
/ "LC_MESSAGES" / "squirrelbattle.po"
args = ["find", "squirrelbattle", "-iname", "*.py"]
find = subprocess.Popen(args, cwd=Path(__file__).parent.parent,
stdout=subprocess.PIPE)
args = ["xargs", "xgettext", "--from-code", "utf-8",
"--add-comments",
"--package-name=squirrelbattle",
"--package-version=3.14.1",
"--copyright-holder=ÿnérant, eichhornchen, "
"nicomarg, charlse",
"--msgid-bugs-address=squirrel-battle@crans.org",
"-o", file_name]
if file_name.is_file():
args.append("--join-existing")
print(f"Make {language} messages...")
subprocess.Popen(args, stdin=find.stdout).wait()
@classmethod
def compilemessages(cls) -> None:
"""
Compile translation messages from source files.
"""
for language in cls.SUPPORTED_LOCALES:
args = ["msgfmt", "--check-format",
"-o", Path(__file__).parent / "locale" / language
/ "LC_MESSAGES" / "squirrelbattle.mo",
Path(__file__).parent / "locale" / language
/ "LC_MESSAGES" / "squirrelbattle.po"]
print(f"Compiling {language} messages...")
subprocess.Popen(args).wait()
def gettext(message: str) -> str:
"""
Translate a message.
"""
return Translator.get_translator().gettext(message)
Translator.refresh_translations()