Merge branch 'master' into map_generation

# Conflicts:
#	dungeonbattle/game.py
#	squirrelbattle/mapgeneration/__init__.py
#	squirrelbattle/mapgeneration/randomwalk.py
This commit is contained in:
Yohann D'ANELLO
2020-12-11 02:14:47 +01:00
94 changed files with 5326 additions and 1243 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

@ -0,0 +1,11 @@
██████ █████ █ ██ ██▓ ██▀███ ██▀███ ▓█████ ██▓ ▄▄▄▄ ▄▄▄ ▄▄▄█████▓▄▄▄█████▓ ██▓ ▓█████
▒██ ▒ ▒██▓ ██▒ ██ ▓██▒▓██▒▓██ ▒ ██▒▓██ ▒ ██▒▓█ ▀ ▓██▒ ▓█████▄ ▒████▄ ▓ ██▒ ▓▒▓ ██▒ ▓▒▓██▒ ▓█ ▀
░ ▓██▄ ▒██▒ ██░▓██ ▒██░▒██▒▓██ ░▄█ ▒▓██ ░▄█ ▒▒███ ▒██░ ▒██▒ ▄██▒██ ▀█▄ ▒ ▓██░ ▒░▒ ▓██░ ▒░▒██░ ▒███
▒ ██▒░██ █▀ ░▓▓█ ░██░░██░▒██▀▀█▄ ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ▒██░█▀ ░██▄▄▄▄██░ ▓██▓ ░ ░ ▓██▓ ░ ▒██░ ▒▓█ ▄
▒██████▒▒░▒███▒█▄ ▒▒█████▓ ░██░░██▓ ▒██▒░██▓ ▒██▒░▒████▒░██████▒ ░▓█ ▀█▓ ▓█ ▓██▒ ▒██▒ ░ ▒██▒ ░ ░██████▒░▒████▒
▒ ▒▓▒ ▒ ░░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ▒▓ ░▒▓░░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░ ░▒▓███▀▒ ▒▒ ▓▒█░ ▒ ░░ ▒ ░░ ░ ▒░▓ ░░░ ▒░ ░
░ ░▒ ░ ░ ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░▒ ░ ▒░ ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒░▒ ░ ▒ ▒▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░
░ ░ ░ ░ ░ ░░░ ░ ░ ▒ ░ ░░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░

View File

@ -0,0 +1,18 @@
1 6
####### #############
#.....# #...........#
#.....# #####...........#
#.....# #...............#
#.##### #.###...........#
#.# #.# #...........#
#.# #.# #############
#.# #.#
#.#### #.#
#....# #.#
####.###################.#
#.....................# #################
#.....................# #...............#
#.....................#######...............#
#...........................................#
#.....................#######...............#
####################### #################

View File

@ -0,0 +1,41 @@
1 17
########### #########
#.........# #.......#
#.........# ############.......#
#.........###############..........#.......##############
#.........#........................#....................#
#.........#.............#..........#.......#............#
########.########.............#..................#............#
#.........# #.............####.#######.......#............#
#.........# #.............##.........######################
#.........# #####.##########.........# ###########
#.........# #......# #.........# #.........#
########.##########......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# ################.######
#...........##......# #.........# #.................############
#...........##......# ########.########.......#.........#..........#
#...........##......# #...............#.......#.........#..........#
#...........######### #...............#.......#.........#..........#
#...........# #...............#.......#....................#
#####.####### #.......................#.........#..........#
#.........# #...............###################..........#
#.........############ #...............# #..........#
#.........#..........# #...............# ############
#....................#####.###########.#############
########.#########...................# #.............#
#........# #..........#........# #.............#########
#........# ######.##########........# #.............#.......#
#........# #..........# #........# #.....................#
#........# #..........# #........# #.............#.......#
#........# #..........# #........# #.............#.......#
#........# #..........# #........# #.............#.......#
#........# #..........#########.##### #.............#.......#
#........# #..........#.........# ##########.############.#######
#........# #..........#.........# #..............# #..........#
########## #..........#.........# #..............# #..........#
############.........# #..............# #..........#
#.........# #..............# #..........#
########### #..............# #..........#
################ ############

View File

@ -0,0 +1,24 @@
# 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
class Bootstrap:
"""
The bootstrap object is used to bootstrap the game so that it starts
properly.
(It was initially created to avoid circular imports between the Game and
Display classes)
"""
@staticmethod
def run_game():
with TermManager() as term_manager: # pragma: no cover
game = Game()
game.new_game()
display = DisplayManager(term_manager.screen, game)
game.display_actions = display.handle_display_action
game.run(term_manager.screen)

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

@ -0,0 +1,160 @@
# 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
from squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.tests.screen import FakePad
class Display:
x: int
y: int
width: int
height: int
pad: Any
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
self.screen = screen
self.pack = pack or TexturePack.get_pack("ascii")
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
def color_pair(self, number: int) -> int:
return curses.color_pair(number) if self.screen else 0
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
self.x = x
self.y = y
self.width = width
self.height = height
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
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@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

@ -0,0 +1,107 @@
# 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 MainMenuDisplay, \
InventoryDisplay, SettingsMenuDisplay
from squirrelbattle.display.logsdisplay import LogsDisplay
from squirrelbattle.display.texturepack import TexturePack
from typing import Any
from squirrelbattle.game import Game, GameMode
from squirrelbattle.enums import DisplayActions
class DisplayManager:
def __init__(self, screen: Any, g: Game):
self.game = g
self.screen = screen
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay = MapDisplay(screen, pack)
self.statsdisplay = StatsDisplay(screen, pack)
self.logsdisplay = LogsDisplay(screen, pack)
self.inventorydisplay = InventoryDisplay(screen, pack)
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(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.messagedisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None:
if action == DisplayActions.REFRESH:
self.refresh()
elif action == DisplayActions.UPDATE:
self.update_game_components()
def update_game_components(self) -> None:
for d in self.displays:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player)
self.inventorydisplay.update_menu(self.game.inventory_menu)
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 \
or self.game.state == GameMode.INVENTORY:
# The map pad has already the good size
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(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.INVENTORY:
self.inventorydisplay.refresh(self.rows // 10,
self.cols // 2,
8 * self.rows // 10,
2 * self.cols // 5)
elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
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:
"""
If the window got resized, ensure that the screen size got updated.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows,
self.cols): # pragma: nocover
curses.resizeterm(y, x)
return True
return False
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42

View File

@ -0,0 +1,25 @@
# 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
class LogsDisplay(Display):
def __init__(self, *args) -> None:
super().__init__(*args)
self.pad = self.newpad(self.rows, self.cols)
def update_logs(self, logs: Logs) -> None:
self.logs = logs
def display(self) -> None:
messages = self.logs.messages[-self.height:]
messages = messages[::-1]
self.pad.erase()
for i in range(min(self.height, len(messages))):
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

@ -0,0 +1,71 @@
# 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
class MapDisplay(Display):
def __init__(self, *args):
super().__init__(*args)
def update_map(self, m: Map) -> None:
self.map = m
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
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.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for e in self.map.entities:
self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
# Display Path map for deubg purposes
# from squirrelbattle.entities.player import Player
# players = [ p for p in self.map.entities if isinstance(p,Player) ]
# player = players[0] if len(players) > 0 else None
# if player:
# for x in range(self.map.width):
# for y in range(self.map.height):
# if (y,x) in player.paths:
# deltay, deltax = (y - player.paths[(y, x)][0],
# x - player.paths[(y, x)][1])
# if (deltay, deltax) == (-1, 0):
# character = '↓'
# elif (deltay, deltax) == (1, 0):
# character = '↑'
# elif (deltay, deltax) == (0, -1):
# character = '→'
# else:
# character = '←'
# self.addstr(self.pad, y, self.pack.tile_width * x,
# character, self.color_pair(1))
def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
pminrow, pmincol = y - deltay, x - deltax
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
deltay, deltax = self.height - deltay, self.width - deltax
smaxrow = self.map.height - (y + deltay) + self.height - 1
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.erase()
self.update_pad()
self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow,
smaxcol)

View File

@ -0,0 +1,120 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import List
from squirrelbattle.menus import Menu, MainMenu
from .display import Display, Box
from ..resources import ResourceManager
from ..translations import gettext as _
class MenuDisplay(Display):
position: int
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.menubox = Box(*args, **kwargs)
def update_menu(self, menu: Menu) -> None:
self.menu = menu
# Menu values are printed in pad
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
def update_pad(self) -> None:
for i in range(self.trueheight):
self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line
self.addstr(self.pad, self.menu.position, 0, ">")
def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
else self.trueheight - self.height + 2 \
if self.height - 2 >= self.trueheight - self.menu.position else 0
# Menu box
self.menubox.refresh(self.y, self.x, self.height, self.width)
self.pad.erase()
self.update_pad()
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
@property
def preferred_height(self) -> int:
return self.trueheight + 2
@property
def values(self) -> List[str]:
return [str(a) for a in self.menu.values]
class SettingsMenuDisplay(MenuDisplay):
@property
def values(self) -> List[str]:
return [_(a[1][1]) + (" : "
+ ("?" if self.menu.waiting_for_key
and a == self.menu.validate() else a[1][0]
.replace("\n", "\\n"))
if a[1][0] else "") for a in self.menu.values]
class MainMenuDisplay(Display):
def __init__(self, menu: MainMenu, *args):
super().__init__(*args)
self.menu = menu
with open(ResourceManager.get_asset_path("ascii_art.txt"), "r") as file:
self.title = file.read().split("\n")
self.pad = self.newpad(max(self.rows, len(self.title) + 30),
max(len(self.title[0]) + 5, self.cols))
self.menudisplay = MenuDisplay(self.screen, self.pack)
self.menudisplay.update_menu(self.menu)
def display(self) -> None:
for i in range(len(self.title)):
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(
menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth)
class InventoryDisplay(MenuDisplay):
def update_pad(self) -> None:
message = _("== INVENTORY ==")
self.addstr(self.pad, 0, (self.width - len(message)) // 2, message,
curses.A_BOLD | curses.A_ITALIC)
for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()]
selection = f"[{rep}]" if i == self.menu.position else f" {rep} "
self.addstr(self.pad, 2 + i, 0, selection
+ " " + item.translated_name.capitalize())
@property
def truewidth(self) -> int:
return max(1, self.height if hasattr(self, "height") else 10)
@property
def trueheight(self) -> int:
return 2 + super().trueheight

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

@ -0,0 +1,58 @@
# 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
class StatsDisplay(Display):
player: Player
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
def update_player(self, p: Player) -> None:
self.player = p
def update_pad(self) -> None:
string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
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)
self.addstr(self.pad, 3, 0, string3)
inventory_str = _("Inventory:") + " "
# Stack items by type instead of displaying each item
item_types = [item.name for item in self.player.inventory]
item_types.sort(key=item_types.count, reverse=True)
printed_items = []
for item in item_types:
if item in printed_items:
continue
count = item_types.count(item)
inventory_str += self.pack[item.upper()]
if count > 1:
inventory_str += f"x{count} "
printed_items.append(item)
self.addstr(self.pad, 8, 0, inventory_str)
if self.player.dead:
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.erase()
self.update_pad()
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.width + self.x - 1)

View File

@ -0,0 +1,80 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import Any
class TexturePack:
_packs = dict()
name: str
tile_width: int
tile_fg_color: int
tile_bg_color: int
entity_fg_color: int
entity_bg_color: int
EMPTY: str
WALL: str
FLOOR: str
PLAYER: str
ASCII_PACK: "TexturePack"
SQUIRREL_PACK: "TexturePack"
def __init__(self, name: str, **kwargs):
self.name = name
self.__dict__.update(**kwargs)
TexturePack._packs[name] = self
def __getitem__(self, item: str) -> Any:
return self.__dict__[item]
@classmethod
def get_pack(cls, name: str) -> "TexturePack":
return cls._packs[name.lower()]
@classmethod
def get_next_pack_name(cls, name: str) -> str:
return "squirrel" if name == "ascii" else "ascii"
TexturePack.ASCII_PACK = TexturePack(
name="ascii",
tile_width=1,
tile_fg_color=curses.COLOR_WHITE,
tile_bg_color=curses.COLOR_BLACK,
entity_fg_color=curses.COLOR_WHITE,
entity_bg_color=curses.COLOR_BLACK,
EMPTY=' ',
WALL='#',
FLOOR='.',
PLAYER='@',
HEDGEHOG='*',
HEART='',
BOMB='o',
RABBIT='Y',
TIGER='n',
TEDDY_BEAR='8',
BODY_SNATCH_POTION='S',
)
TexturePack.SQUIRREL_PACK = TexturePack(
name="squirrel",
tile_width=2,
tile_fg_color=curses.COLOR_WHITE,
tile_bg_color=curses.COLOR_BLACK,
entity_fg_color=curses.COLOR_WHITE,
entity_bg_color=curses.COLOR_WHITE,
EMPTY=' ',
WALL='🧱',
FLOOR='██',
PLAYER='🐿️ ',
HEDGEHOG='🦔',
HEART='💜',
BOMB='💣',
RABBIT='🐇',
TIGER='🐅',
TEDDY_BEAR='🧸',
BODY_SNATCH_POTION='🔀',
)

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

@ -0,0 +1,178 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice, randint
from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map
from ..translations import gettext as _
class Item(Entity):
"""
A class for items
"""
held: bool
held_by: Optional[Player]
def __init__(self, held: bool = False, held_by: Optional[Player] = None,
*args, **kwargs):
super().__init__(*args, **kwargs)
self.held = held
self.held_by = held_by
def drop(self) -> None:
"""
The item is dropped from the inventory onto the floor
"""
if self.held:
self.held_by.inventory.remove(self)
self.map.add_entity(self)
self.move(self.held_by.y, self.held_by.x)
self.held = False
self.held_by = None
def use(self) -> None:
"""
Indicates what should be done when the item is used.
"""
def equip(self) -> None:
"""
Indicates what should be done when the item is equipped.
"""
def hold(self, player: "Player") -> None:
"""
The item is taken from the floor and put into the inventory
"""
self.held = True
self.held_by = player
self.map.remove_entity(self)
player.inventory.append(self)
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
d["held"] = self.held
return d
class Heart(Item):
"""
A heart item to return health to the player
"""
healing: int
def __init__(self, name: str = "heart", healing: int = 5, *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
self.healing = healing
def hold(self, player: "Player") -> None:
"""
When holding a heart, heal the player and don't put item in inventory.
"""
player.health = min(player.maxhealth, player.health + self.healing)
self.map.remove_entity(self)
def save_state(self) -> dict:
"""
Saves the state of the header into a dictionary
"""
d = super().save_state()
d["healing"] = self.healing
return d
class Bomb(Item):
"""
A bomb item intended to deal damage to enemies at long range
"""
damage: int = 5
exploding: bool
owner: Optional["Player"]
tick: int
def __init__(self, name: str = "bomb", damage: int = 5,
exploding: bool = False, *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
self.damage = damage
self.exploding = exploding
self.tick = 4
self.owner = None
def use(self) -> None:
"""
When the bomb is used, throw it and explodes it.
"""
if self.held:
self.owner = self.held_by
super().drop()
self.exploding = True
def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
"""
if self.exploding:
if self.tick > 0:
# The bomb will explode in <tick> moves
self.tick -= 1
else:
# The bomb is exploding.
# Each entity that is close to the bomb takes damages.
# The player earn XP if the entity was killed.
log_message = _("Bomb is exploding.")
for e in m.entities.copy():
if abs(e.x - self.x) + abs(e.y - self.y) <= 3 and \
isinstance(e, FightingEntity):
log_message += " " + e.take_damage(self, self.damage)
if e.dead:
self.owner.add_xp(randint(3, 7))
m.logs.add_message(log_message)
m.entities.remove(self)
def save_state(self) -> dict:
"""
Saves the state of the bomb into a dictionary
"""
d = super().save_state()
d["exploding"] = self.exploding
d["damage"] = self.damage
return d
class BodySnatchPotion(Item):
"""
The body-snatch potion allows to exchange all characteristics with a random
other entity.
"""
def __init__(self, name: str = "body_snatch_potion", *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
def use(self) -> None:
"""
Find a valid random entity, then exchange characteristics.
"""
valid_entities = self.held_by.map.find_entities(FightingEntity)
valid_entities.remove(self.held_by)
entity = choice(valid_entities)
entity_state = entity.save_state()
player_state = self.held_by.save_state()
self.held_by.__dict__.update(entity_state)
entity.__dict__.update(player_state)
self.held_by.map.currenty, self.held_by.map.currentx = self.held_by.y,\
self.held_by.x
self.held_by.map.logs.add_message(
_("{player} exchanged its body with {entity}.").format(
player=self.held_by.translated_name.capitalize(),
entity=entity.translated_name))
self.held_by.recalculate_paths()
self.held_by.inventory.remove(self)

View File

@ -0,0 +1,102 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
from .player import Player
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.
An example of the specification of a monster that has a strength of 4
and 20 max HP:
class MyMonster(Monster):
def __init__(self, strength: int = 4, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="my_monster", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
With that way, attributes can be overwritten when the entity got created.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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.
"""
target = None
for entity in m.entities:
if self.distance_squared(entity) <= 25 and \
isinstance(entity, Player):
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.
if target and (self.y, self.x) in target.paths:
# Move to target player by choosing the best avaliable path
for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
break
if self.distance_squared(target) <= 1:
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 = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
for move in moves:
if move():
break
class Tiger(Monster):
"""
A tiger monster
"""
def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class Hedgehog(Monster):
"""
A really mean hedgehog monster
"""
def __init__(self, name: str = "hedgehog", strength: int = 3,
maxhealth: int = 10, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class Rabbit(Monster):
"""
A rabbit monster
"""
def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class TeddyBear(Monster):
"""
A cute teddybear monster
"""
def __init__(self, name: str = "teddy_bear", strength: int = 0,
maxhealth: int = 50, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -0,0 +1,153 @@
# 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
class Player(FightingEntity):
"""
The class of the player
"""
current_xp: int = 0
max_xp: int = 10
inventory: list
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,
dexterity: int = 1, constitution: int = 1, level: int = 1,
current_xp: int = 0, max_xp: int = 10, inventory: list = None,
*args, **kwargs) \
-> None:
super().__init__(name=name, maxhealth=maxhealth, strength=strength,
intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution,
level=level, *args, **kwargs)
self.current_xp = current_xp
self.max_xp = max_xp
self.inventory = inventory if inventory else list()
for i in range(len(self.inventory)):
if isinstance(self.inventory[i], dict):
entity_classes = self.get_all_entity_classes_in_a_dict()
item_class = entity_classes[self.inventory[i]["type"]]
self.inventory[i] = item_class(**self.inventory[i])
self.paths = dict()
def move(self, y: int, x: int) -> None:
"""
Moves the view of the map (the point on which the camera is centered)
according to the moves of the player.
"""
super().move(y, x)
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
"""
while self.current_xp > self.max_xp:
self.level += 1
self.current_xp -= self.max_xp
self.max_xp = self.level * 10
self.health = self.maxhealth
self.strength = self.strength + 1
# TODO Remove it, that's only fun
self.map.spawn_random_entities(randint(3 * self.level,
10 * self.level))
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
"""
self.current_xp += xp
self.level_up()
# noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool:
"""
If the player tries to move but a fighting entity is there,
the player fights this entity.
If the entity dies, the player is rewarded with some XP
"""
# Don't move if we are dead
if self.dead:
return False
for entity in self.map.entities:
if entity.y == y and entity.x == x:
if entity.is_fighting_entity():
self.map.logs.add_message(self.hit(entity))
if entity.dead:
self.add_xp(randint(3, 7))
return True
elif entity.is_item():
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
"""
d = super().save_state()
d["current_xp"] = self.current_xp
d["max_xp"] = self.max_xp
d["inventory"] = [item.save_state() for item in self.inventory]
return d

75
squirrelbattle/enums.py Normal file
View File

@ -0,0 +1,75 @@
# 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
from squirrelbattle.settings import Settings
# This file contains a few useful enumeration classes used elsewhere in the code
class DisplayActions(Enum):
"""
Display actions options for the callable displayaction Game uses
It just calls the same action on the display object displayaction refers to.
"""
REFRESH = auto()
UPDATE = auto()
class GameMode(Enum):
"""
Game mode options
"""
MAINMENU = auto()
PLAY = auto()
SETTINGS = auto()
INVENTORY = auto()
class KeyValues(Enum):
"""
Key values options used in the game
"""
UP = auto()
DOWN = auto()
LEFT = auto()
RIGHT = auto()
ENTER = auto()
INVENTORY = auto()
USE = auto()
EQUIP = auto()
DROP = auto()
SPACE = auto()
@staticmethod
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
"""
Translate the raw string key into an enum value that we can use.
"""
if key in (settings.KEY_DOWN_SECONDARY,
settings.KEY_DOWN_PRIMARY):
return KeyValues.DOWN
elif key in (settings.KEY_LEFT_PRIMARY,
settings.KEY_LEFT_SECONDARY):
return KeyValues.LEFT
elif key in (settings.KEY_RIGHT_PRIMARY,
settings.KEY_RIGHT_SECONDARY):
return KeyValues.RIGHT
elif key in (settings.KEY_UP_PRIMARY,
settings.KEY_UP_SECONDARY):
return KeyValues.UP
elif key == settings.KEY_ENTER:
return KeyValues.ENTER
elif key == settings.KEY_INVENTORY:
return KeyValues.INVENTORY
elif key == settings.KEY_USE:
return KeyValues.USE
elif key == settings.KEY_EQUIP:
return KeyValues.EQUIP
elif key == settings.KEY_DROP:
return KeyValues.DROP
elif key == ' ':
return KeyValues.SPACE
return None

216
squirrelbattle/game.py Normal file
View File

@ -0,0 +1,216 @@
# 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
import os
import sys
from .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions
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
class Game:
"""
The game object controls all actions in the game.
"""
map: Map
player: Player
# display_actions is a display interface set by the bootstrapper
display_actions: Callable[[DisplayActions], None]
def __init__(self) -> None:
"""
Init the game.
"""
self.state = GameMode.MAINMENU
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.inventory_menu = menus.InventoryMenu()
self.logs = Logs()
self.message = None
def new_game(self) -> None:
"""
Create a new game on the screen.
"""
# TODO generate a new map procedurally
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.map.logs = self.logs
self.logs.clear()
self.player = Player()
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
self.map.spawn_random_entities(randint(3, 10))
self.inventory_menu.update_player(self.player)
def run(self, screen: Any) -> None:
"""
Main infinite loop.
We wait for the player's action, then we do what that should be done
when the given key gets pressed.
"""
while True: # pragma no cover
screen.erase()
screen.refresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None:
"""
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.INVENTORY:
self.handle_key_pressed_inventory(key)
elif self.state == GameMode.MAINMENU:
self.handle_key_pressed_main_menu(key)
elif self.state == GameMode.SETTINGS:
self.settings_menu.handle_key_pressed(key, raw_key, self)
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
"""
In play mode, arrows or zqsd move the main character.
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY
elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU
def handle_key_pressed_inventory(self, key: KeyValues) -> None:
"""
In the inventory menu, we can interact with items or close the menu.
"""
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
self.state = GameMode.PLAY
elif key == KeyValues.UP:
self.inventory_menu.go_up()
elif key == KeyValues.DOWN:
self.inventory_menu.go_down()
if self.inventory_menu.values and not self.player.dead:
if key == KeyValues.USE:
self.inventory_menu.validate().use()
elif key == KeyValues.EQUIP:
self.inventory_menu.validate().equip()
elif key == KeyValues.DROP:
self.inventory_menu.validate().drop()
# Ensure that the cursor has a good position
self.inventory_menu.position = min(self.inventory_menu.position,
len(self.inventory_menu.values)
- 1)
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
"""
In the main menu, we can navigate through options.
"""
if key == KeyValues.DOWN:
self.main_menu.go_down()
if key == KeyValues.UP:
self.main_menu.go_up()
if key == KeyValues.ENTER:
option = self.main_menu.validate()
if option == menus.MainMenuValues.START:
self.new_game()
self.display_actions(DisplayActions.UPDATE)
self.state = GameMode.PLAY
if option == menus.MainMenuValues.RESUME:
self.state = GameMode.PLAY
elif option == menus.MainMenuValues.SAVE:
self.save_game()
elif option == menus.MainMenuValues.LOAD:
self.load_game()
elif option == menus.MainMenuValues.SETTINGS:
self.state = GameMode.SETTINGS
elif option == menus.MainMenuValues.EXIT:
sys.exit(0)
def save_state(self) -> dict:
"""
Saves the game to a dictionary
"""
return self.map.save_state()
def load_state(self, d: dict) -> None:
"""
Loads the game from a dictionary
"""
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:
"""
Loads the game from a file
"""
file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path):
with open(file_path, "r") as f:
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:
"""
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

@ -0,0 +1,438 @@
# 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
from .display.texturepack import TexturePack
from .translations import gettext as _
class Logs:
"""
The logs object stores the messages to display. It is encapsulating a list
of such messages, to allow multiple pointers to keep track of it even if
the list was to be reassigned.
"""
def __init__(self) -> None:
self.messages = []
def add_message(self, msg: str) -> None:
self.messages.append(msg)
def add_messages(self, msg: List[str]) -> None:
self.messages += msg
def clear(self) -> None:
self.messages = []
class Map:
"""
Object that represents a Map with its width, height
and tiles, that have their custom properties.
"""
width: int
height: int
start_y: int
start_x: int
tiles: List[List["Tile"]]
entities: List["Entity"]
logs: Logs
# coordinates of the point that should be
# on the topleft corner of the screen
currentx: int
currenty: int
def __init__(self, width: int, height: int, tiles: list,
start_y: int, start_x: int):
self.width = width
self.height = height
self.start_y = start_y
self.start_x = start_x
self.tiles = tiles
self.entities = []
self.logs = Logs()
def add_entity(self, entity: "Entity") -> None:
"""
Register a new entity in the map.
"""
self.entities.append(entity)
entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
"""
self.entities.remove(entity)
def find_entities(self, entity_class: type) -> list:
return [entity for entity in self.entities
if isinstance(entity, entity_class)]
def is_free(self, y: int, x: int) -> bool:
"""
Indicates that the case at the coordinates (y, x) is empty.
"""
return 0 <= y < self.height and 0 <= x < self.width and \
self.tiles[y][x].can_walk() and \
not any(entity.x == x and entity.y == y for entity in self.entities)
@staticmethod
def load(filename: str) -> "Map":
"""
Read a file that contains the content of a map, and build a Map object.
"""
with open(filename, "r") as f:
file = f.read()
return Map.load_from_string(file)
@staticmethod
def load_from_string(content: str) -> "Map":
"""
Load a map represented by its characters and build a Map object.
"""
lines = content.split("\n")
first_line = lines[0]
start_y, start_x = map(int, first_line.split(" "))
lines = [line for line in lines[1:] if line]
height = len(lines)
width = len(lines[0])
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return Map(width, height, tiles, start_y, start_x)
@staticmethod
def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
"""
Transforms a string into the list of corresponding tiles
"""
lines = content.split("\n")
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return tiles
def draw_string(self, pack: TexturePack) -> str:
"""
Draw 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)
for line in self.tiles)
def spawn_random_entities(self, count: int) -> None:
"""
Put randomly {count} hedgehogs on the map, where it is available.
"""
for ignored in range(count):
y, x = 0, 0
while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
tile = self.tiles[y][x]
if tile.can_walk():
break
entity = choice(Entity.get_all_entity_classes())()
entity.move(y, x)
self.add_entity(entity)
def tick(self) -> None:
"""
Trigger all entity events.
"""
for entity in self.entities:
entity.act(self)
def save_state(self) -> dict:
"""
Saves the map's attributes to a dictionary
"""
d = dict()
d["width"] = self.width
d["height"] = self.height
d["start_y"] = self.start_y
d["start_x"] = self.start_x
d["currentx"] = self.currentx
d["currenty"] = self.currenty
d["entities"] = []
for enti in self.entities:
d["entities"].append(enti.save_state())
d["map"] = self.draw_string(TexturePack.ASCII_PACK)
return d
def load_state(self, d: dict) -> None:
"""
Loads the map's attributes from a dictionary
"""
self.width = d["width"]
self.height = d["height"]
self.start_y = d["start_y"]
self.start_x = d["start_x"]
self.currentx = d["currentx"]
self.currenty = d["currenty"]
self.tiles = self.load_dungeon_from_string(d["map"])
self.entities = []
dictclasses = Entity.get_all_entity_classes_in_a_dict()
for entisave in d["entities"]:
self.add_entity(dictclasses[entisave["type"]](**entisave))
class Tile(Enum):
"""
The internal representation of the tiles of the map
"""
EMPTY = auto()
WALL = auto()
FLOOR = auto()
@staticmethod
def from_ascii_char(ch: str) -> "Tile":
"""
Maps an ascii character to its equivalent in the texture pack
"""
for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch:
return tile
raise ValueError(ch)
def char(self, pack: TexturePack) -> str:
"""
Translates a Tile to the corresponding character according
to the texture pack
"""
return getattr(pack, self.name)
def is_wall(self) -> bool:
"""
Is this Tile a wall?
"""
return self == Tile.WALL
def can_walk(self) -> bool:
"""
Check 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
"""
y: int
x: int
name: str
map: Map
# noinspection PyShadowingBuiltins
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
map: Optional[Map] = None, *ignored, **ignored2):
self.y = y
self.x = x
self.name = name
self.map = map
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool:
"""
Checks if moving to (y,x) is authorized
"""
free = self.map.is_free(y, x)
if free and move_if_possible:
self.move(y, x)
return free
def move(self, y: int, x: int) -> bool:
"""
Moves an entity to (y,x) coordinates
"""
self.y = y
self.x = x
return True
def move_up(self, force: bool = False) -> bool:
"""
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
"""
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
"""
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
"""
return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True)
def act(self, m: Map) -> None:
"""
Define the action of the entity that is ran 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.
"""
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
def distance(self, other: "Entity") -> float:
"""
Get the cartesian distance to another entity.
"""
return sqrt(self.distance_squared(other))
def is_fighting_entity(self) -> bool:
"""
Is this entity a fighting entity?
"""
return isinstance(self, FightingEntity)
def is_item(self) -> bool:
"""
Is this entity an item?
"""
from squirrelbattle.entities.items import Item
return isinstance(self, Item)
@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 BodySnatchPotion, Bomb, Heart
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear
return [BodySnatchPotion, Bomb, Heart, Hedgehog,
Rabbit, TeddyBear, Tiger]
@staticmethod
def get_all_entity_classes_in_a_dict() -> dict:
"""
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.items import BodySnatchPotion, Bomb, Heart
return {
"Tiger": Tiger,
"Bomb": Bomb,
"Heart": Heart,
"BodySnatchPotion": BodySnatchPotion,
"Hedgehog": Hedgehog,
"Rabbit": Rabbit,
"TeddyBear": TeddyBear,
"Player": Player,
}
def save_state(self) -> dict:
"""
Saves the coordinates of the entity
"""
d = dict()
d["x"] = self.x
d["y"] = self.y
d["type"] = self.__class__.__name__
return d
class FightingEntity(Entity):
"""
A FightingEntity is an entity that can fight, and thus has a health,
level and stats
"""
maxhealth: int
health: int
strength: int
intelligence: int
charisma: int
dexterity: int
constitution: int
level: int
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
strength: int = 0, intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0, level: int = 0,
*args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.maxhealth = maxhealth
self.health = maxhealth if health is None else health
self.strength = strength
self.intelligence = intelligence
self.charisma = charisma
self.dexterity = dexterity
self.constitution = constitution
self.level = level
@property
def dead(self) -> bool:
return self.health <= 0
def hit(self, opponent: "FightingEntity") -> str:
"""
Deals damage to the opponent, based on the stats
"""
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:
"""
Take damage from the attacker, based on the stats
"""
self.health -= amount
if self.health <= 0:
self.die()
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:
"""
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 entities 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
"""
d = super().save_state()
for name in self.keys():
d[name] = getattr(self, name)
return d

View File

@ -0,0 +1,201 @@
# German translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
#
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+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/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== BESTAND =="
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Bestand:"
#: squirrelbattle/display/statsdisplay.py:50
msgid "YOU ARE DEAD"
msgstr "SIE WURDEN GESTORBEN"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
msgid "Bomb is exploding."
msgstr "Die Bombe explodiert."
#: squirrelbattle/entities/items.py:172
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} täuscht seinem Körper mit {entity} aus."
#: squirrelbattle/game.py:177
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:185
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:205
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."
#: squirrelbattle/interfaces.py:400
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} schlägt {opponent}."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
#: squirrelbattle/interfaces.py:414
#, python-brace-format
msgid "{name} dies."
msgstr "{name} stirbt."
#: squirrelbattle/menus.py:72
msgid "Back"
msgstr "Zurück"
#: squirrelbattle/tests/game_test.py:300 squirrelbattle/tests/game_test.py:303
#: squirrelbattle/tests/game_test.py:306
#: 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 "Key used to open the inventory"
msgstr "Bestandtaste"
#: squirrelbattle/tests/translations_test.py:47
msgid "Key used to use an item in the inventory"
msgstr "Taste um eines Objekts im Bestand zu verwenden"
#: squirrelbattle/tests/translations_test.py:49
msgid "Key used to equip an item in the inventory"
msgstr "Taste um eines Objekts im Bestand auszurüsten"
#: squirrelbattle/tests/translations_test.py:51
msgid "Key used to drop an item in the inventory"
msgstr "Taste um eines Objekts im Bestand zu werfen"
#: squirrelbattle/tests/translations_test.py:53
msgid "Texture pack"
msgstr "Textur-Packung"
#: squirrelbattle/tests/translations_test.py:54
msgid "Language"
msgstr "Sprache"
#: squirrelbattle/tests/translations_test.py:57
msgid "player"
msgstr "Spieler"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "Tiger"
#: squirrelbattle/tests/translations_test.py:60
msgid "hedgehog"
msgstr "Igel"
#: squirrelbattle/tests/translations_test.py:61
msgid "rabbit"
msgstr "Kanninchen"
#: squirrelbattle/tests/translations_test.py:62
msgid "teddy bear"
msgstr "Teddybär"
#: squirrelbattle/tests/translations_test.py:64
msgid "body snatch potion"
msgstr "Leichenfleddererzaubertrank"
#: squirrelbattle/tests/translations_test.py:65
msgid "bomb"
msgstr "Bombe"
#: squirrelbattle/tests/translations_test.py:66
msgid "heart"
msgstr "Herz"

View File

@ -0,0 +1,206 @@
# Spanish translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
# Translation by ifugaao
#
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ifugao\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"
# Suggested in Weblate: == INVENTORIO ==
#: squirrelbattle/display/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== INVENTORIO =="
# Suggested in Weblate: Inventorio :
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Inventorio :"
# Suggested in Weblate: ERES MUERTO
#: squirrelbattle/display/statsdisplay.py:50
msgid "YOU ARE DEAD"
msgstr "ERES MUERTO"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
msgid "Bomb is exploding."
msgstr "La bomba está explotando."
#: squirrelbattle/entities/items.py:172
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} intercambió su cuerpo con {entity}."
#: squirrelbattle/game.py:177
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
"Algunas claves faltan en su archivo de guarda.\n"
"Su guarda parece a ser corruptido. Fue eliminado."
#: squirrelbattle/game.py:185
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
"No jugador encontrado sobre la carta !\n"
"¿ Quizas murió ?"
#: squirrelbattle/game.py:205
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
"El JSON archivo no es correcto.\n"
"Su guarda parece corrupta. Fue eliminada."
#: squirrelbattle/interfaces.py:400
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} golpea a {opponent}."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} recibe {amount} daño."
#: squirrelbattle/interfaces.py:414
#, python-brace-format
msgid "{name} dies."
msgstr "{name} se muere."
#: squirrelbattle/menus.py:72
msgid "Back"
msgstr "Volver"
#: squirrelbattle/tests/game_test.py:300,
#: squirrelbattle/tests/game_test.py:303,
#: squirrelbattle/tests/game_test.py:306,
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nuevo partido"
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Resumir"
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Cargar"
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Guardar"
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Parametros"
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Salir"
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Primera tecla para subir"
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Segunda tecla para subir"
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Primera tecla para bajar"
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Segunda tecla para bajar"
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Primera tecla para moverse a la izquierda"
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Segunda tecla para moverse a la izquierda"
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Primera tecla para moverse a la derecha"
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Segunda tecla para moverse a la derecha"
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Tecla para validar un menú"
#: squirrelbattle/tests/translations_test.py:45
msgid "Key used to open the inventory"
msgstr "Tecla para abrir el inventorio"
#: squirrelbattle/tests/translations_test.py:47
msgid "Key used to use an item in the inventory"
msgstr "Tecla para utilizar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:49
msgid "Key used to equip an item in the inventory"
msgstr "Tecla para equipar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:51
msgid "Key used to drop an item in the inventory"
msgstr "Tecla para dejar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:53
msgid "Texture pack"
msgstr "Paquete de texturas"
#: squirrelbattle/tests/translations_test.py:54
msgid "Language"
msgstr "Languaje"
#: squirrelbattle/tests/translations_test.py:57
msgid "player"
msgstr "jugador"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:60
msgid "hedgehog"
msgstr "erizo"
#: squirrelbattle/tests/translations_test.py:61
msgid "rabbit"
msgstr "conejo"
#: squirrelbattle/tests/translations_test.py:62
msgid "teddy bear"
msgstr "osito de peluche"
#: squirrelbattle/tests/translations_test.py:64
msgid "body snatch potion"
msgstr "poción de intercambio"
#: squirrelbattle/tests/translations_test.py:65
msgid "bomb"
msgstr "bomba"
#: squirrelbattle/tests/translations_test.py:66
msgid "heart"
msgstr "corazón"

View File

@ -0,0 +1,202 @@
# French translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+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: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== INVENTAIRE =="
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Inventaire :"
#: squirrelbattle/display/statsdisplay.py:50
msgid "YOU ARE DEAD"
msgstr "VOUS ÊTES MORT"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
msgid "Bomb is exploding."
msgstr "La bombe explose."
#: squirrelbattle/entities/items.py:172
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} a échangé son corps avec {entity}."
#: squirrelbattle/game.py:177
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:185
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:205
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/interfaces.py:400
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} frappe {opponent}."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
#: squirrelbattle/interfaces.py:414
#, python-brace-format
msgid "{name} dies."
msgstr "{name} meurt."
#: squirrelbattle/menus.py:72
msgid "Back"
msgstr "Retour"
#: squirrelbattle/tests/game_test.py:300 squirrelbattle/tests/game_test.py:303
#: squirrelbattle/tests/game_test.py:306
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nouvelle partie"
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Continuer"
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Charger"
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Sauvegarder"
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Paramètres"
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Quitter"
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Touche principale pour aller vers le haut"
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Touche secondaire pour aller vers le haut"
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Touche principale pour aller vers le bas"
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Touche secondaire pour aller vers le bas"
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Touche principale pour aller vers la gauche"
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Touche secondaire pour aller vers la gauche"
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Touche principale pour aller vers la droite"
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Touche secondaire pour aller vers la droite"
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Touche pour valider un menu"
#: squirrelbattle/tests/translations_test.py:45
msgid "Key used to open the inventory"
msgstr "Touche utilisée pour ouvrir l'inventaire"
#: squirrelbattle/tests/translations_test.py:47
msgid "Key used to use an item in the inventory"
msgstr "Touche pour utiliser un objet de l'inventaire"
#: squirrelbattle/tests/translations_test.py:49
msgid "Key used to equip an item in the inventory"
msgstr "Touche pour équiper un objet de l'inventaire"
#: squirrelbattle/tests/translations_test.py:51
msgid "Key used to drop an item in the inventory"
msgstr "Touche pour jeter un objet de l'inventaire"
#: squirrelbattle/tests/translations_test.py:53
msgid "Texture pack"
msgstr "Pack de textures"
#: squirrelbattle/tests/translations_test.py:54
msgid "Language"
msgstr "Langue"
#: squirrelbattle/tests/translations_test.py:57
msgid "player"
msgstr "joueur"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:60
msgid "hedgehog"
msgstr "hérisson"
#: squirrelbattle/tests/translations_test.py:61
msgid "rabbit"
msgstr "lapin"
#: squirrelbattle/tests/translations_test.py:62
msgid "teddy bear"
msgstr "nounours"
#: squirrelbattle/tests/translations_test.py:64
msgid "body snatch potion"
msgstr "potion d'arrachage de corps"
#: squirrelbattle/tests/translations_test.py:65
msgid "bomb"
msgstr "bombe"
#: squirrelbattle/tests/translations_test.py:66
msgid "heart"
msgstr "cœur"

View File

View File

@ -0,0 +1,98 @@
from enum import auto, Enum
from random import choice, random, randint
from dungeonbattle.interfaces import Map, Tile
DEFAULT_PARAMS = {"split_chance" : .15,
"turn_chance" : .5,
"death_chance" : .1,
"max_walkers" : 15,
"width" : 100,
"height" : 100,
"fill" : .4}
class Directions(Enum):
up = auto()
down = auto()
left = auto()
right = auto()
class Walker:
def __init__(self, x, y):
self.x = x
self.y = y
self.dir = choice(list(Directions))
def random_turn(self):
self.dir = choice(list(Directions))
def next_pos(self):
if self.dir == Directions.up:
return self.x, self.y + 1
elif self.dir == Directions.down:
return self.x, self.y - 1
elif self.dir == Directions.right:
return self.x + 1, self.y
elif self.dir == Directions.left:
return self.x - 1, self.y
def move_in_bounds(self, width, height):
nx, ny = self.next_pos()
if 0 < nx < width and 0 < ny < height:
self.x, self.y = nx, ny
def split(self):
child = Walker(self.x, self.y)
child.dir = self.dir
return child
class Generator:
def __init__(self, params = DEFAULT_PARAMS):
self.params = params
def run(self):
width, height = self.params["width"], self.params["height"]
walkers = [Walker(width//2, height//2)]
grid = [[Tile.EMPTY for _ in range(width)] for _ in range(height)]
count = 0
while count < self.params["fill"] * width*height:
# because we can't add or remove walkers while looping over the pop
# we need lists to keep track of what will be the walkers for the
# next iteration of the main loop
next_walker_pop = []
for walker in walkers:
if grid[walker.y][walker.x] == Tile.EMPTY:
count += 1
grid[walker.y][walker.x] = Tile.FLOOR
if random() < self.params["turn_chance"]:
walker.random_turn()
walker.move_in_bounds(width, height)
if random() > self.params["death_chance"]:
next_walker_pop.append(walker)
# we make sure to never kill all walkers
if next_walker_pop == []:
next_walker_pop.append(choice(walkers))
# we use a second loop for spliting so we're not bothered by cases
# like a walker not spliting because we hit the population cap even
# though the next one would have died and freed a place
# not a big if it happened though
for walker in walkers:
if len(next_walker_pop) < self.params["max_walkers"]:
if random() < self.params["split_chance"]:
next_walker_pop.append(walker.split())
walkers = next_walker_pop
start_x, start_y = randint(0, width-1), randint(0, height-1)
while grid[start_y][start_x] != Tile.FLOOR:
start_x, start_y = randint(0, width-1), randint(0, height-1)
return Map(width, height, grid, start_x, start_y)

130
squirrelbattle/menus.py Normal file
View File

@ -0,0 +1,130 @@
# 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 .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions
from .settings import Settings
from .translations import gettext as _, Translator
class Menu:
"""
A Menu object is the logical representation of a menu in the game
"""
values: list
def __init__(self):
self.position = 0
def go_up(self) -> None:
"""
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
"""
self.position = min(len(self.values) - 1, self.position + 1)
def validate(self) -> Any:
"""
Selects the value that is pointed by the menu pointer
"""
return self.values[self.position]
class MainMenuValues(Enum):
"""
Values of the main menu
"""
START = "New game"
RESUME = "Resume"
SAVE = "Save"
LOAD = "Load"
SETTINGS = "Settings"
EXIT = "Exit"
def __str__(self):
return _(self.value)
class MainMenu(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
"""
waiting_for_key: bool = False
def update_values(self, settings: Settings) -> None:
self.values = list(settings.__dict__.items())
self.values.append(("RETURN", ["", _("Back")]))
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
"""
if not self.waiting_for_key:
# Navigate normally through the menu.
if key == KeyValues.SPACE or \
key == KeyValues.ENTER and \
self.position == len(self.values) - 1:
# Go back
game.display_actions(DisplayActions.UPDATE)
game.state = GameMode.MAINMENU
if key == KeyValues.DOWN:
self.go_down()
if key == KeyValues.UP:
self.go_up()
if key == KeyValues.ENTER and self.position < len(self.values) - 1:
# Change a setting
option = self.values[self.position][0]
if option == "TEXTURE_PACK":
game.settings.TEXTURE_PACK = \
TexturePack.get_next_pack_name(
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 'es' \
if game.settings.LOCALE == 'de' 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)
else:
option = self.values[self.position][0]
# Don't use an already mapped key
if any(getattr(game.settings, opt) == raw_key
for opt in game.settings.settings_keys if opt != option):
return
setattr(game.settings, option, raw_key)
game.settings.write_settings()
self.waiting_for_key = False
self.update_values(game.settings)
class InventoryMenu(Menu):
player: Player
def update_player(self, player: Player) -> None:
self.player = player
@property
def values(self) -> list:
return self.player.inventory

View File

@ -0,0 +1,24 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
class ResourceManager:
"""
The ResourceManager loads resources at their right place,
and stores files in config directory.
"""
BASE_DIR = Path(__file__).resolve().parent / 'assets'
# FIXME This might not work on not-UNIX based systems.
CONFIG_DIR = Path.home() / '.config' / 'squirrel-battle'
@classmethod
def get_asset_path(cls, filename: str) -> str:
return str(cls.BASE_DIR / filename)
@classmethod
def get_config_path(cls, filename: str) -> str:
cls.CONFIG_DIR.mkdir(parents=True) if not cls.CONFIG_DIR.is_dir() \
else None
return str(cls.CONFIG_DIR / filename)

View File

@ -0,0 +1,97 @@
# 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:
"""
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'
"""
def __init__(self):
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.KEY_INVENTORY = ['i', 'Key used to open the inventory']
self.KEY_USE = ['u', 'Key used to use an item in the inventory']
self.KEY_EQUIP = ['e', 'Key used to equip an item in the inventory']
self.KEY_DROP = ['r', 'Key used to drop an item in the inventory']
self.TEXTURE_PACK = ['ascii', 'Texture pack']
self.LOCALE = [locale.getlocale()[0][:2], 'Language']
def __getattribute__(self, item: str) -> Any:
superattribute = super().__getattribute__(item)
if item.isupper() and item in self.settings_keys:
return superattribute[0]
return superattribute
def __setattr__(self, name: str, value: Any) -> None:
if name in self.settings_keys:
object.__getattribute__(self, name)[0] = value
return
return super().__setattr__(name, value)
def get_comment(self, item: str) -> str:
"""
Retrieve the comment of a setting.
"""
if item in self.settings_keys:
return _(object.__getattribute__(self, item)[1])
for key in self.settings_keys:
if getattr(self, key) == item:
return _(object.__getattribute__(self, key)[1])
@property
def settings_keys(self) -> Generator[str, Any, None]:
"""
Get the list of all parameters.
"""
return (key for key in self.__dict__)
def loads_from_string(self, json_str: str) -> None:
"""
Dump settings
"""
d = json.loads(json_str)
for key in d:
setattr(self, key, d[key])
def dumps_to_string(self) -> str:
"""
Dump settings
"""
d = dict()
for key in self.settings_keys:
d[key] = getattr(self, key)
return json.dumps(d, indent=4)
def load_settings(self) -> None:
"""
Loads the settings from a file
"""
file_path = ResourceManager.get_config_path("settings.json")
if os.path.isfile(file_path):
with open(file_path, "r") as f:
self.loads_from_string(f.read())
def write_settings(self) -> None:
"""
Dumps the settings into a file
"""
with open(ResourceManager.get_config_path("settings.json"), "w") as f:
f.write(self.dumps_to_string())

View File

@ -0,0 +1,36 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
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
"""
def __init__(self):
self.screen = curses.initscr()
# convert escapes sequences to curses abstraction
self.screen.keypad(True)
# stop printing typed keys to the terminal
curses.noecho()
# send keys through without having to press <enter>
curses.cbreak()
# make cursor invisible
curses.curs_set(False)
# Enable colors
curses.start_color()
def __enter__(self):
return self
def __exit__(self, exc_type: type, exc_value: Exception,
exc_traceback: TracebackType) -> None:
# restore the terminal to its original state
self.screen.keypad(False)
curses.echo()
curses.nocbreak()
curses.curs_set(True)
curses.endwin()

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

@ -0,0 +1,206 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item
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
class TestEntities(unittest.TestCase):
def setUp(self) -> None:
"""
Load example map that can be used in tests.
"""
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.player = Player()
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
def test_basic_entities(self) -> None:
"""
Test some random stuff with basic entities.
"""
entity = Entity()
entity.move(42, 64)
self.assertEqual(entity.y, 42)
self.assertEqual(entity.x, 64)
self.assertIsNone(entity.act(self.map))
other_entity = Entity()
other_entity.move(45, 68)
self.assertEqual(entity.distance_squared(other_entity), 25)
self.assertEqual(entity.distance(other_entity), 5)
def test_fighting_entities(self) -> None:
"""
Test some random stuff with fighting entities.
"""
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),
"Tiger hits tiger. Tiger takes 2 damage.")
self.assertFalse(entity.dead)
self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
+ "Tiger takes 2 damage. Tiger dies.")
self.assertTrue(entity.dead)
entity = Rabbit()
self.map.add_entity(entity)
entity.move(15, 44)
# Move randomly
self.map.tick()
self.assertFalse(entity.y == 15 and entity.x == 44)
# Move to the player
entity.move(3, 6)
self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6)
# Rabbit should fight
old_health = self.player.health
self.map.tick()
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.capitalize()} hits {self.player.name}. \
{self.player.name.capitalize()} takes {entity.strength} damage.")
# Fight the rabbit
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertFalse(entity.dead)
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertFalse(entity.dead)
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertTrue(entity.dead)
self.assertGreaterEqual(self.player.current_xp, 3)
def test_items(self) -> None:
"""
Test some random stuff with items.
"""
item = Item()
self.map.add_entity(item)
self.assertFalse(item.held)
item.hold(self.player)
self.assertTrue(item.held)
item.drop()
self.assertEqual(item.y, 1)
self.assertEqual(item.x, 6)
# Pick up item
self.player.move_left()
self.player.move_right()
self.assertTrue(item.held)
self.assertEqual(item.held_by, self.player)
self.assertIn(item, self.player.inventory)
self.assertNotIn(item, self.map.entities)
def test_bombs(self) -> None:
"""
Test some random stuff with bombs.
"""
item = Bomb()
hedgehog = Hedgehog()
teddy_bear = TeddyBear()
self.map.add_entity(item)
self.map.add_entity(hedgehog)
self.map.add_entity(teddy_bear)
hedgehog.health = 2
teddy_bear.health = 2
hedgehog.move(41, 42)
teddy_bear.move(42, 41)
item.act(self.map)
self.assertFalse(hedgehog.dead)
self.assertFalse(teddy_bear.dead)
self.player.move(42, 42)
item.hold(self.player)
item.use()
self.assertEqual(item.y, 42)
self.assertEqual(item.x, 42)
# Wait for the explosion
for ignored in range(5):
item.act(self.map)
self.assertTrue(hedgehog.dead)
self.assertTrue(teddy_bear.dead)
bomb_state = item.save_state()
self.assertEqual(bomb_state["damage"], item.damage)
def test_hearts(self) -> None:
"""
Test some random stuff with hearts.
"""
item = Heart()
self.map.add_entity(item)
item.move(2, 6)
self.player.health -= 2 * item.healing
self.player.move_down()
self.assertNotIn(item, self.map.entities)
self.assertEqual(self.player.health,
self.player.maxhealth - item.healing)
heart_state = item.save_state()
self.assertEqual(heart_state["healing"], item.healing)
def test_body_snatch_potion(self) -> None:
"""
Test some random stuff with body snatch potions.
"""
item = BodySnatchPotion()
self.map.add_entity(item)
item.hold(self.player)
tiger = Tiger(y=42, x=42)
self.map.add_entity(tiger)
# The player becomes a tiger, and the tiger becomes a squirrel
item.use()
self.assertEqual(self.player.name, "tiger")
self.assertEqual(tiger.name, "player")
self.assertEqual(self.player.y, 42)
self.assertEqual(self.player.x, 42)
def test_players(self) -> None:
"""
Test some random stuff with players.
"""
player = Player()
self.map.add_entity(player)
player.move(1, 6)
self.assertEqual(player.strength, 5)
self.assertEqual(player.health, player.maxhealth)
self.assertEqual(player.maxhealth, 20)
# Test movements and ensure that collisions are working
self.assertFalse(player.move_up())
self.assertTrue(player.move_left())
self.assertFalse(player.move_left())
for _ in range(8):
self.assertTrue(player.move_down())
self.assertFalse(player.move_down())
self.assertTrue(player.move_right())
self.assertTrue(player.move_right())
self.assertTrue(player.move_right())
self.assertFalse(player.move_right())
self.assertTrue(player.move_down())
self.assertTrue(player.move_down())
player.add_xp(70)
self.assertEqual(player.current_xp, 10)
self.assertEqual(player.max_xp, 40)
self.assertEqual(player.level, 4)
player_state = player.save_state()
self.assertEqual(player_state["current_xp"], 10)

View File

@ -0,0 +1,419 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import unittest
from ..bootstrap import Bootstrap
from ..display.display import Display
from ..display.display_manager import DisplayManager
from ..entities.items import Bomb
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):
def setUp(self) -> None:
"""
Setup game.
"""
self.game = Game()
self.game.new_game()
self.game.logs.add_message("Hello World !")
display = DisplayManager(None, self.game)
self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None:
"""
Save a game and reload it.
"""
bomb = Bomb()
self.game.map.add_entity(bomb)
bomb.hold(self.game.player)
old_state = self.game.save_state()
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.ENTER) # Save game
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.ENTER) # Load game
new_state = self.game.save_state()
self.assertEqual(old_state, new_state)
# Ensure that the bomb is loaded
self.assertTrue(self.game.player.inventory)
# 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,
because there is no associated shell.
Yeah, that's only for coverage.
"""
self.assertRaises(Exception, Bootstrap.run_game)
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
def test_key_translation(self) -> None:
"""
Test key bindings.
"""
self.game.settings = Settings()
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_PRIMARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_SECONDARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_PRIMARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_SECONDARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_PRIMARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_SECONDARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_PRIMARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_ENTER, self.game.settings),
KeyValues.ENTER)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_INVENTORY, self.game.settings),
KeyValues.INVENTORY)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_USE, self.game.settings),
KeyValues.USE)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_EQUIP, self.game.settings),
KeyValues.EQUIP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DROP, self.game.settings),
KeyValues.DROP)
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
KeyValues.SPACE)
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
None)
def test_key_press(self) -> None:
"""
Press a key and see what is done.
"""
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.RESUME)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.EXIT)
self.assertRaises(SystemExit, self.game.handle_key_pressed,
KeyValues.ENTER)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.RESUME)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.PLAY)
# Kill entities
for entity in self.game.map.entities.copy():
if not isinstance(entity, Player):
self.game.map.remove_entity(entity)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.DOWN)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y + 1)
self.assertEqual(new_x, x)
y, x = new_y, new_x
self.game.handle_key_pressed(KeyValues.RIGHT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x + 1)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.UP)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y - 1)
self.assertEqual(new_x, x)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.LEFT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x - 1)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_new_game(self) -> None:
"""
Ensure that the start button starts a new game.
"""
old_map = self.game.map
old_player = self.game.player
self.game.handle_key_pressed(KeyValues.ENTER) # Start new game
new_map = self.game.map
new_player = self.game.player
# Ensure that
self.assertNotEqual(old_map, new_map)
self.assertNotEqual(old_player, new_player)
self.game.handle_key_pressed(KeyValues.SPACE)
old_map = new_map
old_player = new_player
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER) # Resume game
new_map = self.game.map
new_player = self.game.player
self.assertEqual(old_map, new_map)
self.assertEqual(old_player, new_player)
def test_settings_menu(self) -> None:
"""
Ensure that the settings menu is working properly.
"""
self.game.settings = Settings()
# Open settings menu
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.SETTINGS)
# Define the "move up" key to 'w'
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'w')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
# Navigate to "move left"
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.UP)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
# Define the "move up" key to 'a'
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
# Can't used a mapped key
self.game.handle_key_pressed(None, 's')
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'a')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack"
for ignored in range(9):
self.game.handle_key_pressed(KeyValues.DOWN)
# Change texture pack
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "squirrel")
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, "es")
self.assertEqual(_("New game"), "Nuevo partido")
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.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_logs(self) -> None:
"""
Tests the use of logs
"""
self.assertEqual(self.game.logs.messages, ["Hello World !"])
self.game.logs.add_messages(["Hello", "World"])
self.assertEqual(self.game.logs.messages, ["Hello World !",
"Hello", "World"])
self.game.logs.clear()
self.assertEqual(self.game.logs.messages, [])
def test_dead_screen(self) -> None:
"""
Kill player and render dead screen.
"""
self.game.state = GameMode.PLAY
# Kill player
self.game.player.take_damage(self.game.player,
self.game.player.health + 2)
y, x = self.game.player.y, self.game.player.x
for key in [KeyValues.UP, KeyValues.DOWN,
KeyValues.LEFT, KeyValues.RIGHT]:
self.game.handle_key_pressed(key)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x)
def test_not_implemented(self) -> None:
"""
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)
def test_inventory_menu(self) -> None:
"""
Open the inventory menu and interact with items.
"""
self.game.state = GameMode.PLAY
# Open and close the inventory
self.game.handle_key_pressed(KeyValues.INVENTORY)
self.assertEqual(self.game.state, GameMode.INVENTORY)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY)
# Add five bombs in the inventory
for ignored in range(5):
bomb = Bomb()
bomb.map = self.game.map
bomb.map.add_entity(bomb)
bomb.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.INVENTORY)
self.assertEqual(self.game.state, GameMode.INVENTORY)
# Navigate in the menu
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.inventory_menu.position, 3)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.UP)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.inventory_menu.position, 4)
# Equip key does nothing
self.game.handle_key_pressed(KeyValues.EQUIP)
# Drop an item
bomb = self.game.player.inventory[-1]
self.assertEqual(self.game.inventory_menu.validate(), bomb)
self.assertTrue(bomb.held)
self.assertEqual(bomb.held_by, self.game.player)
self.game.handle_key_pressed(KeyValues.DROP)
self.assertFalse(bomb.held)
self.assertIsNone(bomb.held_by)
self.assertIsNone(bomb.owner)
self.assertFalse(bomb.exploding)
self.assertEqual(bomb.y, self.game.player.y)
self.assertEqual(bomb.x, self.game.player.x)
# Use the bomb
bomb = self.game.player.inventory[-1]
self.assertEqual(self.game.inventory_menu.validate(), bomb)
self.assertTrue(bomb.held)
self.assertEqual(bomb.held_by, self.game.player)
self.game.handle_key_pressed(KeyValues.USE)
self.assertFalse(bomb.held)
self.assertIsNone(bomb.held_by)
self.assertEqual(bomb.owner, self.game.player)
self.assertTrue(bomb.exploding)
self.assertEqual(bomb.y, self.game.player.y)
self.assertEqual(bomb.x, self.game.player.x)

View File

@ -0,0 +1,39 @@
# 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
from squirrelbattle.interfaces import Map, Tile
from squirrelbattle.resources import ResourceManager
class TestInterfaces(unittest.TestCase):
def test_map(self) -> None:
"""
Create a map and check that it is well parsed.
"""
m = Map.load_from_string("0 0\n.#\n#.\n")
self.assertEqual(m.width, 2)
self.assertEqual(m.height, 2)
self.assertEqual(m.draw_string(TexturePack.ASCII_PACK), ".#\n#.")
def test_load_map(self) -> None:
"""
Try to load a map from a file.
"""
m = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.assertEqual(m.width, 52)
self.assertEqual(m.height, 17)
def test_tiles(self) -> None:
"""
Test some things about tiles.
"""
self.assertFalse(Tile.FLOOR.is_wall())
self.assertTrue(Tile.WALL.is_wall())
self.assertFalse(Tile.EMPTY.is_wall())
self.assertTrue(Tile.FLOOR.can_walk())
self.assertFalse(Tile.WALL.can_walk())
self.assertFalse(Tile.EMPTY.can_walk())
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')

View File

@ -0,0 +1,26 @@
# 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
but does nothing with them.
"""
def addstr(self, y: int, x: int, message: str, color: int = 0) -> None:
pass
def refresh(self, pminrow: int, pmincol: int, sminrow: int,
smincol: int, smaxrow: int, smaxcol: int) -> None:
pass
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

@ -0,0 +1,39 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest
from squirrelbattle.settings import Settings
from squirrelbattle.translations import Translator
class TestSettings(unittest.TestCase):
def setUp(self) -> None:
Translator.setlocale("en")
def test_settings(self) -> None:
"""
Ensure that settings are well loaded.
"""
settings = Settings()
self.assertEqual(settings.KEY_UP_PRIMARY, 'z')
self.assertEqual(settings.KEY_DOWN_PRIMARY, 's')
self.assertEqual(settings.KEY_LEFT_PRIMARY, 'q')
self.assertEqual(settings.KEY_RIGHT_PRIMARY, 'd')
self.assertEqual(settings.KEY_UP_SECONDARY, 'KEY_UP')
self.assertEqual(settings.KEY_DOWN_SECONDARY, 'KEY_DOWN')
self.assertEqual(settings.KEY_LEFT_SECONDARY, 'KEY_LEFT')
self.assertEqual(settings.KEY_RIGHT_SECONDARY, 'KEY_RIGHT')
self.assertEqual(settings.TEXTURE_PACK, 'ascii')
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
settings.get_comment('TEXTURE_PACK'))
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
'Texture pack')
settings.TEXTURE_PACK = 'squirrel'
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')
settings.write_settings()
settings.load_settings()
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')

View File

@ -0,0 +1,66 @@
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(_("Key used to open the inventory"),
"Touche utilisée pour ouvrir l'inventaire")
self.assertEqual(_("Key used to use an item in the inventory"),
"Touche pour utiliser un objet de l'inventaire")
self.assertEqual(_("Key used to equip an item in the inventory"),
"Touche pour équiper un objet de l'inventaire")
self.assertEqual(_("Key used to drop an item in the inventory"),
"Touche pour jeter un objet de l'inventaire")
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(_("body snatch potion"), "potion d'arrachage de corps")
self.assertEqual(_("bomb"), "bombe")
self.assertEqual(_("heart"), "cœur")

View File

@ -0,0 +1,107 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import gettext as gt
import os
import re
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", "es", "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:
if language == "en":
# Don't translate the main language
continue
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, ifugao",
"--msgid-bugs-address=squirrel-battle@crans.org",
"--sort-by-file",
"-o", file_name]
if file_name.is_file():
args.append("--join-existing")
with open(file_name, "r") as f:
content = f.read()
with open(file_name, "w") as f:
f.write(re.sub("#:.*\n", "", content))
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:
if language == "en":
continue
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()