Compare commits

..

4 Commits

Author SHA1 Message Date
Charles Peyrat ebca28930c Remove debug output. Oops. 2021-01-21 03:28:46 +01:00
Charles Peyrat bcfde6fa49 Move code around so loops placement isn't stopped by walls 2021-01-21 03:27:31 +01:00
Charles Peyrat 5981927dac Optimization for the loop placing algorithm 2021-01-21 03:26:54 +01:00
Charles Peyrat 38878687c4 Add new room type : rectangular rooms 2021-01-16 00:35:41 +01:00
21 changed files with 225 additions and 208 deletions

View File

@ -11,37 +11,31 @@
Squirrel Battle is an infinite rogue-like game with randomly generated levels, in which the player controls a squirrel in its quest down in a dungeon, using diverse items to defeat monsters, and trying not to die. Squirrel Battle is an infinite rogue-like game with randomly generated levels, in which the player controls a squirrel in its quest down in a dungeon, using diverse items to defeat monsters, and trying not to die.
## Installation ##Installation
#### Via PyPI : ####Via PyPI :
``` ``` pip install --user squirrel-battle
$ pip install --user squirrel-battle ``` to install
```
to install
``` ``` pip install --user --upgrade squirrel-battle
$ pip install --user --upgrade squirrel-battle ``` to upgrade
```
to upgrade
#### Via ArchLinux package : ####Via ArchLinux package :
Download one of these two packages on the AUR : Download one of these two packages on the AUR :
* python-squirrel-battle * python-squirrel-battle
* python-squirrel-battle-git * python-squirrel-battle-git
#### Via Debian package : ####Via Debian package :
Available on our git repository, has a dependency on fonts-noto-color-emoji (to be found in the official Debian repositories). Available on our git repository, has a dependency on fonts-noto-color-emoji (to be found in the official Debian repositories).
Run Run ```
``` dpkg -i python3-squirrelbattle_3.14.1_all.deb
$ dpkg -i python3-squirrelbattle_23.14_all.deb ``` after downloading
```
after downloading
In all cases, execute via command line : `squirrel-battle` In all cases, execute via command line : ```squirrel-battle```
## For first-time players ##For first-time players
The game is played in a terminal only, preferably one that supports color, markdown and emojis, but it can be played with only grey levels and relatively classic unicode characters. The game is played in a terminal only, preferably one that supports color, markdown and emojis, but it can be played with only grey levels and relatively classic unicode characters.

6
debian/changelog vendored
View File

@ -1,9 +1,3 @@
python3-squirrel-battle (23.14) beta; urgency=low
* Big update
-- Yohann D'ANELLO <squirrel-battle@crans.org> Sun, 10 Jan 2021 23:56:42 +0100
python3-squirrel-battle (3.14.1) beta; urgency=low python3-squirrel-battle (3.14.1) beta; urgency=low
* Some graphical improvements. * Some graphical improvements.

View File

@ -3,7 +3,7 @@ Déploiement du projet
.. _PyPI: https://pypi.org/project/squirrel-battle/ .. _PyPI: https://pypi.org/project/squirrel-battle/
.. _AUR: https://aur.archlinux.org/packages/python-squirrel-battle/ .. _AUR: https://aur.archlinux.org/packages/python-squirrel-battle/
.. _Debian: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_23.14_all.deb?job=build-deb .. _Debian: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14.1_all.deb?job=build-deb
.. _installation: install.html .. _installation: install.html
À chaque nouvelle version du projet, il est compilé et déployé dans PyPI_, dans À chaque nouvelle version du projet, il est compilé et déployé dans PyPI_, dans
@ -46,7 +46,7 @@ paquet ainsi que des détails à fournir à PyPI :
setup( setup(
name="squirrel-battle", name="squirrel-battle",
version="23.14", version="3.14.1",
author="ÿnérant, eichhornchen, nicomarg, charlse", author="ÿnérant, eichhornchen, nicomarg, charlse",
author_email="squirrel-battle@crans.org", author_email="squirrel-battle@crans.org",
description="Watch out for squirrel's knives!", description="Watch out for squirrel's knives!",
@ -172,7 +172,7 @@ du dépôt Git. Le fichier ``PKGBUILD`` dispose de cette structure :
pkgbase=squirrel-battle pkgbase=squirrel-battle
pkgname=python-squirrel-battle-git pkgname=python-squirrel-battle-git
pkgver=23.14 pkgver=3.14.1
pkgrel=1 pkgrel=1
pkgdesc="Watch out for squirrel's knives!" pkgdesc="Watch out for squirrel's knives!"
arch=('any') arch=('any')
@ -222,7 +222,7 @@ les releases, est plus ou moins similaire :
pkgbase=squirrel-battle pkgbase=squirrel-battle
pkgname=python-squirrel-battle pkgname=python-squirrel-battle
pkgver=23.14 pkgver=3.14.1
pkgrel=1 pkgrel=1
pkgdesc="Watch out for squirrel's knives!" pkgdesc="Watch out for squirrel's knives!"
arch=('any') arch=('any')
@ -232,7 +232,7 @@ les releases, est plus ou moins similaire :
makedepends=('gettext' 'python-setuptools') makedepends=('gettext' 'python-setuptools')
depends=('noto-fonts-emoji') depends=('noto-fonts-emoji')
checkdepends=('python-tox') checkdepends=('python-tox')
source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v23.14/$pkgbase-v$pkgver.tar.gz") source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v3.14.1/$pkgbase-v$pkgver.tar.gz")
sha256sums=("6090534d598c0b3a8f5acdb553c12908ba8107d62d08e17747d1dbb397bddef0") sha256sums=("6090534d598c0b3a8f5acdb553c12908ba8107d62d08e17747d1dbb397bddef0")
build() { build() {
@ -317,7 +317,7 @@ On peut ensuite construire le paquet :
dpkg-buildpackage dpkg-buildpackage
mkdir build && cp ../*.deb build/ mkdir build && cp ../*.deb build/
Le paquet sera installé dans ``build/python3-squirrel-battle_23.14_all.deb``. Le paquet sera installé dans ``build/python3-squirrel-battle_3.14.1_all.deb``.
Le paquet Debian_ est construit par l'intégration continue Gitlab et ajouté Le paquet Debian_ est construit par l'intégration continue Gitlab et ajouté
à chaque release. à chaque release.

View File

@ -61,7 +61,7 @@ Le jeu peut être ensuite lancé via la commande ``squirrel-battle``.
Sur Ubuntu/Debian Sur Ubuntu/Debian
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_23.14_all.deb?job=build-deb .. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14.1_all.deb?job=build-deb
Un paquet_ est généré par l'intégration continue de Gitlab à chaque commit. Un paquet_ est généré par l'intégration continue de Gitlab à chaque commit.
Ils sont également attachés à chaque nouvelle release. Ils sont également attachés à chaque nouvelle release.
@ -73,7 +73,7 @@ Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` :
.. code:: bash .. code:: bash
dpkg -i python3-squirrelbattle_23.14_all.deb dpkg -i python3-squirrelbattle_3.14.1_all.deb
Ce paquet inclut un patch pour afficher les émojis écureuil correctement. Ce paquet inclut un patch pour afficher les émojis écureuil correctement.

View File

@ -65,7 +65,7 @@ d'exécuter pour chaque langue ``<LANG>`` :
find squirrelbattle -iname '*.py' | xargs xgettext --from-code utf-8 find squirrelbattle -iname '*.py' | xargs xgettext --from-code utf-8
--add-comments --add-comments
--package-name=squirrelbattle --package-name=squirrelbattle
--package-version=23.14 --package-version=3.14.1
"--copyright-holder=ÿnérant, eichhornchen, nicomarg, charlse" "--copyright-holder=ÿnérant, eichhornchen, nicomarg, charlse"
--msgid-bugs-address=squirrel-battle@crans.org --msgid-bugs-address=squirrel-battle@crans.org
-o squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po -o squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po

View File

@ -22,7 +22,7 @@ for language in ["de", "es", "fr"]:
setup( setup(
name="squirrel-battle", name="squirrel-battle",
version="23.14", version="3.14.1",
author="ÿnérant, eichhornchen, nicomarg, charlse", author="ÿnérant, eichhornchen, nicomarg, charlse",
author_email="squirrel-battle@crans.org", author_email="squirrel-battle@crans.org",
description="Watch out for squirrel's knives!", description="Watch out for squirrel's knives!",

View File

@ -2,17 +2,17 @@
####### ############# ####### #############
#.H...# #...........# #.H...# #...........#
#.....# #####...........# #.....# #####...........#
#.....# #...&........H..# #.....# #............H..#
#.##### #.###...........# #.##### #.###...........#
#.# #.# #...........# #.# #.# #...........#
#.# #.# ############# #.# #.# #############
#.# #.# #.# #.#
#.#### #.# #.#### #.#
#....# #.# #....# #.#
####&###################&# ####.###################.#
#.....................# ################# #.....................# #################
#.....................# #...............# #.....................# #...............#
#.....................#######...............# #.....................#######...............#
#.....................&.....&...............# #...........................................#
#.....................#######...............# #.....................#######...............#
####################### ################# ####################### #################

View File

@ -176,8 +176,7 @@ class PlayerInventoryDisplay(MenuDisplay):
selection = f"[{rep}]" if i == self.menu.position \ selection = f"[{rep}]" if i == self.menu.position \
and self.selected else f" {rep} " and self.selected else f" {rep} "
self.addstr(self.pad, i + 1, 0, selection self.addstr(self.pad, i + 1, 0, selection
+ " " + ("[E]" if item.equipped else "") + " " + item.translated_name.capitalize()
+ item.translated_name.capitalize()
+ (f" ({item.description})" if item.description else "") + (f" ({item.description})" if item.description else "")
+ (": " + str(item.price) + " Hazels" + (": " + str(item.price) + " Hazels"
if self.store_mode else "")) if self.store_mode else ""))

View File

@ -82,7 +82,6 @@ TexturePack.ASCII_PACK = TexturePack(
BOW=')', BOW=')',
CHEST='', CHEST='',
CHESTPLATE='(', CHESTPLATE='(',
DOOR='&',
EAGLE='µ', EAGLE='µ',
EMPTY=' ', EMPTY=' ',
EXPLOSION='%', EXPLOSION='%',
@ -125,8 +124,6 @@ TexturePack.SQUIRREL_PACK = TexturePack(
BOW='🏹', BOW='🏹',
CHEST='🧰', CHEST='🧰',
CHESTPLATE='🦺', CHESTPLATE='🦺',
DOOR=('🚪', curses.COLOR_WHITE, (1000, 1000, 1000),
curses.COLOR_WHITE, (1000, 1000, 1000)),
EAGLE='🦅', EAGLE='🦅',
EMPTY=' ', EMPTY=' ',
EXPLOSION='💥', EXPLOSION='💥',

View File

@ -12,19 +12,16 @@ class Item(Entity):
""" """
A class for items. A class for items.
""" """
held: bool
held_by: Optional[InventoryHolder] held_by: Optional[InventoryHolder]
price: int price: int
def __init__(self, equipped: bool = False, def __init__(self, held: bool = False,
held_by: Optional[InventoryHolder] = None, held_by: Optional[InventoryHolder] = None,
hold_slot: str = "equipped_secondary",
price: int = 2, *args, **kwargs): price: int = 2, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.held = held
self.held_by = held_by self.held_by = held_by
self.equipped = equipped
self.hold_slot = hold_slot
if equipped:
self.equip()
self.price = price self.price = price
@property @property
@ -38,11 +35,11 @@ class Item(Entity):
""" """
The item is dropped from the inventory onto the floor. The item is dropped from the inventory onto the floor.
""" """
if self.held_by is not None: if self.held:
self.unequip()
self.held_by.remove_from_inventory(self) self.held_by.remove_from_inventory(self)
self.held_by.map.add_entity(self) self.held_by.map.add_entity(self)
self.move(self.held_by.y, self.held_by.x) self.move(self.held_by.y, self.held_by.x)
self.held = False
self.held_by = None self.held_by = None
def use(self) -> None: def use(self) -> None:
@ -55,41 +52,28 @@ class Item(Entity):
Indicates what should be done when the item is thrown. Indicates what should be done when the item is thrown.
""" """
def on_equip(self) -> None:
"""
Indicates a special behaviour when equipping
"""
def on_unequip(self) -> None:
"""
Indicates a special behaviour when unequipping
"""
def equip(self) -> None: def equip(self) -> None:
""" """
Indicates what should be done when the item is equipped. Indicates what should be done when the item is equipped.
""" """
# Other objects are only equipped as secondary. # Other objects are only equipped as secondary.
if not self.equipped: if self.held_by.equipped_secondary:
if getattr(self.held_by, self.hold_slot): self.held_by.equipped_secondary.unequip()
getattr(self.held_by, self.hold_slot).unequip() self.held_by.remove_from_inventory(self)
self.equipped = True self.held_by.equipped_secondary = self
setattr(self.held_by, self.hold_slot, self)
self.on_equip()
def unequip(self) -> None: def unequip(self) -> None:
""" """
Indicates what should be done when the item is unequipped. Indicates what should be done when the item is unequipped.
""" """
if self.equipped: self.held_by.remove_from_inventory(self)
setattr(self.held_by, self.hold_slot, None) self.held_by.add_to_inventory(self)
self.equipped = False
self.on_unequip()
def hold(self, holder: InventoryHolder) -> None: def hold(self, holder: InventoryHolder) -> None:
""" """
The item is taken from the floor and put into the inventory. The item is taken from the floor and put into the inventory.
""" """
self.held = True
self.held_by = holder self.held_by = holder
self.held_by.map.remove_entity(self) self.held_by.map.remove_entity(self)
holder.add_to_inventory(self) holder.add_to_inventory(self)
@ -99,7 +83,7 @@ class Item(Entity):
Saves the state of the item into a dictionary. Saves the state of the item into a dictionary.
""" """
d = super().save_state() d = super().save_state()
d["equipped"] = self.equipped d["held"] = self.held
return d return d
@staticmethod @staticmethod
@ -119,12 +103,10 @@ class Item(Entity):
inventory. inventory.
""" """
if for_free: if for_free:
self.unequip() if self.equipped else None
self.hold(buyer) self.hold(buyer)
seller.remove_from_inventory(self) seller.remove_from_inventory(self)
return True return True
elif buyer.hazel >= self.price: elif buyer.hazel >= self.price:
self.unequip() if self.equipped else None
self.hold(buyer) self.hold(buyer)
seller.remove_from_inventory(self) seller.remove_from_inventory(self)
buyer.change_hazel_balance(-self.price) buyer.change_hazel_balance(-self.price)
@ -187,7 +169,7 @@ class Bomb(Item):
""" """
When the bomb is used, it is thrown and then it explodes. When the bomb is used, it is thrown and then it explodes.
""" """
if self.held_by is not None: if self.held:
self.owner = self.held_by self.owner = self.held_by
super().drop() super().drop()
self.exploding = True self.exploding = True
@ -232,19 +214,14 @@ class Explosion(Item):
""" """
When a bomb explodes, the explosion is displayed. When a bomb explodes, the explosion is displayed.
""" """
living_ticks: int def __init__(self, *args, **kwargs):
def __init__(self, living_ticks: int = 2, *args, **kwargs):
super().__init__(name="explosion", *args, **kwargs) super().__init__(name="explosion", *args, **kwargs)
self.living_ticks = living_ticks
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
""" """
The bomb disappears after exploding. The bomb disappears after exploding.
""" """
self.living_ticks -= 1 m.remove_entity(self)
if self.living_ticks <= 0:
m.remove_entity(self)
def hold(self, player: InventoryHolder) -> None: def hold(self, player: InventoryHolder) -> None:
""" """
@ -259,7 +236,7 @@ class Weapon(Item):
damage: int damage: int
def __init__(self, damage: int = 3, *args, **kwargs): def __init__(self, damage: int = 3, *args, **kwargs):
super().__init__(hold_slot="equipped_main", *args, **kwargs) super().__init__(*args, **kwargs)
self.damage = damage self.damage = damage
@property @property
@ -274,17 +251,20 @@ class Weapon(Item):
d["damage"] = self.damage d["damage"] = self.damage
return d return d
def on_equip(self) -> None: def equip(self) -> None:
""" """
When a weapon is equipped, the player gains strength. When a weapon is equipped, the player gains strength.
""" """
self.held_by.remove_from_inventory(self)
self.held_by.equipped_main = self
self.held_by.strength += self.damage self.held_by.strength += self.damage
def on_unequip(self) -> None: def unequip(self) -> None:
""" """
Remove the strength earned by the weapon. Remove the strength earned by the weapon.
:return: :return:
""" """
super().unequip()
self.held_by.strength -= self.damage self.held_by.strength -= self.damage
@ -321,10 +301,12 @@ class Armor(Item):
return f"CON+{self.constitution}" if self.constitution \ return f"CON+{self.constitution}" if self.constitution \
else super().description else super().description
def on_equip(self) -> None: def equip(self) -> None:
super().equip()
self.held_by.constitution += self.constitution self.held_by.constitution += self.constitution
def on_unequip(self) -> None: def unequip(self) -> None:
super().unequip()
self.held_by.constitution -= self.constitution self.held_by.constitution -= self.constitution
def save_state(self) -> dict: def save_state(self) -> dict:
@ -350,7 +332,13 @@ class Helmet(Armor):
def __init__(self, name: str = "helmet", constitution: int = 2, def __init__(self, name: str = "helmet", constitution: int = 2,
price: int = 18, *args, **kwargs): price: int = 18, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price, super().__init__(name=name, constitution=constitution, price=price,
hold_slot="equipped_helmet", *args, **kwargs) *args, **kwargs)
def equip(self) -> None:
if self.held_by.equipped_helmet:
self.held_by.equipped_helmet.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_helmet = self
class Chestplate(Armor): class Chestplate(Armor):
@ -360,7 +348,13 @@ class Chestplate(Armor):
def __init__(self, name: str = "chestplate", constitution: int = 4, def __init__(self, name: str = "chestplate", constitution: int = 4,
price: int = 30, *args, **kwargs): price: int = 30, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price, super().__init__(name=name, constitution=constitution, price=price,
hold_slot="equipped_armor", *args, **kwargs) *args, **kwargs)
def equip(self) -> None:
if self.held_by.equipped_armor:
self.held_by.equipped_armor.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_armor = self
class BodySnatchPotion(Item): class BodySnatchPotion(Item):
@ -432,7 +426,8 @@ class Ring(Item):
("CRI", self.critical), ("XP", self.experience)] ("CRI", self.critical), ("XP", self.experience)]
return ", ".join(f"{key}+{value}" for key, value in fields if value) return ", ".join(f"{key}+{value}" for key, value in fields if value)
def on_equip(self) -> None: def equip(self) -> None:
super().equip()
self.held_by.maxhealth += self.maxhealth self.held_by.maxhealth += self.maxhealth
self.held_by.strength += self.strength self.held_by.strength += self.strength
self.held_by.intelligence += self.intelligence self.held_by.intelligence += self.intelligence
@ -442,7 +437,8 @@ class Ring(Item):
self.held_by.critical += self.critical self.held_by.critical += self.critical
self.held_by.xp_buff += self.experience self.held_by.xp_buff += self.experience
def on_unequip(self) -> None: def unequip(self) -> None:
super().unequip()
self.held_by.maxhealth -= self.maxhealth self.held_by.maxhealth -= self.maxhealth
self.held_by.strength -= self.strength self.held_by.strength -= self.strength
self.held_by.intelligence -= self.intelligence self.held_by.intelligence -= self.intelligence
@ -561,6 +557,13 @@ class LongRangeWeapon(Weapon):
self.held_by.map.logs.add_message(line) self.held_by.map.logs.add_message(line)
return (to_kill.y, to_kill.x) if to_kill else None return (to_kill.y, to_kill.x) if to_kill else None
def equip(self) -> None:
"""
Equip the weapon.
"""
self.held_by.remove_from_inventory(self)
self.held_by.equipped_main = self
@property @property
def stat(self) -> str: def stat(self) -> str:
""" """

View File

@ -6,7 +6,7 @@ from random import randint
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
from .items import Item from .items import Item
from ..interfaces import FightingEntity, InventoryHolder, Tile from ..interfaces import FightingEntity, InventoryHolder
from ..translations import gettext as _ from ..translations import gettext as _
@ -117,6 +117,21 @@ class Player(InventoryHolder, FightingEntity):
self.current_xp += int(xp * self.xp_buff) self.current_xp += int(xp * self.xp_buff)
self.level_up() self.level_up()
def remove_from_inventory(self, obj: Item) -> None:
"""
Remove the given item from the inventory, even if the item is equipped.
"""
if obj == self.equipped_main:
self.equipped_main = None
elif obj == self.equipped_armor:
self.equipped_armor = None
elif obj == self.equipped_secondary:
self.equipped_secondary = None
elif obj == self.equipped_helmet:
self.equipped_helmet = None
else:
return super().remove_from_inventory(obj)
# noinspection PyTypeChecker,PyUnresolvedReferences # noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \ def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool: -> bool:
@ -137,12 +152,6 @@ class Player(InventoryHolder, FightingEntity):
return True return True
elif entity.is_item(): elif entity.is_item():
entity.hold(self) entity.hold(self)
tile = self.map.tiles[y][x]
if tile == Tile.DOOR and move_if_possible:
# Open door
self.map.tiles[y][x] = Tile.FLOOR
self.map.compute_visibility(y, x, self.vision)
return super().check_move(y, x, move_if_possible)
return super().check_move(y, x, move_if_possible) return super().check_move(y, x, move_if_possible)
def save_state(self) -> dict: def save_state(self) -> dict:

View File

@ -192,16 +192,14 @@ class Game:
# We move up on the ladder of the beginning, # We move up on the ladder of the beginning,
# down at the end of the stage # down at the end of the stage
move_down = y != self.map.start_y or x != self.map.start_x move_down = y != self.map.start_y and x != self.map.start_x
old_map = self.map old_map = self.map
self.map_index += 1 if move_down else -1 self.map_index += 1 if move_down else -1
if self.map_index == -1: if self.map_index == -1:
self.map_index = 0 self.map_index = 0
return return
while self.map_index >= len(self.maps): while self.map_index >= len(self.maps):
m = broguelike.Generator().run() self.maps.append(broguelike.Generator().run())
m.logs = self.logs
self.maps.append(m)
new_map = self.map new_map = self.map
new_map.floor = self.map_index new_map.floor = self.map_index
old_map.remove_entity(self.player) old_map.remove_entity(self.player)
@ -307,8 +305,7 @@ class Game:
if key == KeyValues.USE: if key == KeyValues.USE:
self.inventory_menu.validate().use() self.inventory_menu.validate().use()
elif key == KeyValues.EQUIP: elif key == KeyValues.EQUIP:
item = self.inventory_menu.validate() self.inventory_menu.validate().equip()
item.unequip() if item.equipped else item.equip()
elif key == KeyValues.DROP: elif key == KeyValues.DROP:
self.inventory_menu.validate().drop() self.inventory_menu.validate().drop()
@ -420,7 +417,6 @@ class Game:
self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]] self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]]
for i, m in enumerate(self.maps): for i, m in enumerate(self.maps):
m.floor = i m.floor = i
m.logs = self.logs
except KeyError as error: except KeyError as error:
self.message = _("Some keys are missing in your save file.\n" self.message = _("Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted.")\ "Your save seems to be corrupt. It got deleted.")\

View File

@ -390,7 +390,6 @@ class Tile(Enum):
WALL = auto() WALL = auto()
FLOOR = auto() FLOOR = auto()
LADDER = auto() LADDER = auto()
DOOR = auto()
@staticmethod @staticmethod
def from_ascii_char(ch: str) -> "Tile": def from_ascii_char(ch: str) -> "Tile":
@ -431,7 +430,7 @@ class Tile(Enum):
""" """
Is this Tile a wall? Is this Tile a wall?
""" """
return self == Tile.WALL or self == Tile.DOOR return self == Tile.WALL
def is_ladder(self) -> bool: def is_ladder(self) -> bool:
""" """
@ -858,9 +857,7 @@ class InventoryHolder(Entity):
entity_classes = self.get_all_entity_classes_in_a_dict() entity_classes = self.get_all_entity_classes_in_a_dict()
item_class = entity_classes[item_dict["type"]] item_class = entity_classes[item_dict["type"]]
item = item_class(**item_dict) return item_class(**item_dict)
item.held_by = self
return item
def save_state(self) -> dict: def save_state(self) -> dict:
""" """
@ -876,7 +873,6 @@ class InventoryHolder(Entity):
Adds an object to the inventory. Adds an object to the inventory.
""" """
if obj not in self.inventory: if obj not in self.inventory:
obj.held_by = self
self.inventory.append(obj) self.inventory.append(obj)
def remove_from_inventory(self, obj: Any) -> None: def remove_from_inventory(self, obj: Any) -> None:

View File

@ -6,7 +6,7 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: squirrelbattle 23.14\n" "Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2021-01-10 21:30+0100\n" "POT-Creation-Date: 2021-01-10 21:30+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"

View File

@ -6,7 +6,7 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: squirrelbattle 23.14\n" "Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2021-01-10 21:30+0100\n" "POT-Creation-Date: 2021-01-10 21:30+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@ -23,19 +23,19 @@ msgstr "Inventorio :"
#: squirrelbattle/display/gamedisplay.py:167 #: squirrelbattle/display/gamedisplay.py:167
msgid "Equipped main:" msgid "Equipped main:"
msgstr "Equipado principal :" msgstr "Principal equipado:"
#: squirrelbattle/display/gamedisplay.py:171 #: squirrelbattle/display/gamedisplay.py:171
msgid "Equipped secondary:" msgid "Equipped secondary:"
msgstr "Equipado segundario :" msgstr "Equipado secundario:"
#: squirrelbattle/display/gamedisplay.py:176 #: squirrelbattle/display/gamedisplay.py:176
msgid "Equipped chestplate:" msgid "Equipped chestplate:"
msgstr "Pechera equipada :" msgstr "Pechera equipada:"
#: squirrelbattle/display/gamedisplay.py:180 #: squirrelbattle/display/gamedisplay.py:180
msgid "Equipped helmet:" msgid "Equipped helmet:"
msgstr "Casco equipado :" msgstr "Casco equipado:"
#: squirrelbattle/display/gamedisplay.py:187 #: squirrelbattle/display/gamedisplay.py:187
msgid "YOU ARE DEAD" msgid "YOU ARE DEAD"
@ -44,22 +44,22 @@ msgstr "ERES MUERTO"
#: squirrelbattle/display/gamedisplay.py:191 #: squirrelbattle/display/gamedisplay.py:191
#, python-brace-format #, python-brace-format
msgid "Use {key} to use the ladder" msgid "Use {key} to use the ladder"
msgstr "Presiona {key} para utilizar la escala" msgstr "Usa {key} para usar la escalera"
#: squirrelbattle/display/gamedisplay.py:210 #: squirrelbattle/display/gamedisplay.py:210
msgid "Move to the friendly entity to talk to it" msgid "Move to the friendly entity to talk to it"
msgstr "Moverse hasta la entitad amistosa para hablar con ella" msgstr "Muévete hacia la entidad amiga para hablar con ella."
#: squirrelbattle/display/gamedisplay.py:212 #: squirrelbattle/display/gamedisplay.py:212
#, python-brace-format #, python-brace-format
msgid "Use {key} then move to talk to the entity" msgid "Use {key} then move to talk to the entity"
msgstr "Presionar {key} pues moverse para hablar con la entitad" msgstr "Usa {key} y luego muévete para hablar con la entidad"
#: squirrelbattle/display/menudisplay.py:124 #: squirrelbattle/display/menudisplay.py:124
#: squirrelbattle/display/menudisplay.py:149 #: squirrelbattle/display/menudisplay.py:149
#: squirrelbattle/display/menudisplay.py:304 #: squirrelbattle/display/menudisplay.py:304
msgid "Credits" msgid "Credits"
msgstr "Créditos" msgstr "Creditos"
#: squirrelbattle/display/menudisplay.py:173 #: squirrelbattle/display/menudisplay.py:173
msgid "INVENTORY" msgid "INVENTORY"
@ -75,11 +75,11 @@ msgstr "COFRE"
#: squirrelbattle/display/menudisplay.py:308 #: squirrelbattle/display/menudisplay.py:308
msgid "Developers:" msgid "Developers:"
msgstr "Desarrolladores :" msgstr "Desarrollador:"
#: squirrelbattle/display/menudisplay.py:314 #: squirrelbattle/display/menudisplay.py:314
msgid "Translators:" msgid "Translators:"
msgstr "Traductores :" msgstr "Traductores:"
#: squirrelbattle/entities/friendly.py:38 #: squirrelbattle/entities/friendly.py:38
msgid "I don't sell any squirrel" msgid "I don't sell any squirrel"
@ -153,12 +153,12 @@ msgstr "El baile no fue efectivo ..."
#: squirrelbattle/game.py:214 #: squirrelbattle/game.py:214
#, python-brace-format #, python-brace-format
msgid "The player climbs down to the floor {floor}." msgid "The player climbs down to the floor {floor}."
msgstr "El jugador baja a la planta {floor}." msgstr "El jugador desciende alla planta {floor}."
#: squirrelbattle/game.py:227 #: squirrelbattle/game.py:227
#, python-brace-format #, python-brace-format
msgid "The player climbs up the floor {floor}." msgid "The player climbs up the floor {floor}."
msgstr "El jugador sube a la planta {floor}." msgstr "El jugador sube por la planta {floor}."
#: squirrelbattle/game.py:348 squirrelbattle/tests/game_test.py:631 #: squirrelbattle/game.py:348 squirrelbattle/tests/game_test.py:631
msgid "The buyer does not have enough money" msgid "The buyer does not have enough money"
@ -417,7 +417,7 @@ msgstr "anillo de daño crítico"
#: squirrelbattle/tests/translations_test.py:91 #: squirrelbattle/tests/translations_test.py:91
msgid "ring of more experience" msgid "ring of more experience"
msgstr "anillo de mejorada experiencia" msgstr "anillo de más experiencia"
#: squirrelbattle/tests/translations_test.py:93 #: squirrelbattle/tests/translations_test.py:93
msgid "monocle" msgid "monocle"

View File

@ -10,7 +10,7 @@ msgstr "{name} prend {amount} points de dégât."
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: squirrelbattle 23.14\n" "Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" "Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2021-01-10 21:30+0100\n" "POT-Creation-Date: 2021-01-10 21:30+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"

View File

@ -21,38 +21,38 @@ DEFAULT_PARAMS = {
"large_circular_room": .10, "large_circular_room": .10,
"circular_holes": .5, "circular_holes": .5,
"loop_tries": 40, "loop_tries": 40,
"loop_max": 5, "loop_max": 8,
"loop_threshold": 15, "loop_threshold": 15,
"spawn_per_region": [1, 2], "spawn_per_region": [1, 2],
"room_chances": { "room_chances" : {
"circular": 5, "circular" : 1,
"chunks": 1, "chunks" : 1,
}, "rectangle" : 1,
},
} }
def test_dist(level, y1, x1, y2, x2, threshold):
def dist(level: List[List[Tile]], y1: int, x1: int, y2: int, x2: int) -> int:
""" """
Compute the minimum walking distance between points (y1, x1) and (y2, x2) Returns whether the minimum walking distance between points (y1, x1) and
on a Tile grid (y2, x2) on the Tile grid level is greater than threshold
""" """
# simple breadth first search # simple breadth first search
copy = [[t for t in row] for row in level] copy = [[t for t in row] for row in level]
dist = -1 dist = -1
queue, next_queue = [[y1, x1]], [0] queue, next_queue = [[y1, x1]], [0]
while next_queue: while next_queue and dist < threshold:
next_queue = [] next_queue = []
dist += 1 dist += 1
while queue: while queue:
y, x = queue.pop() y, x = queue.pop()
copy[y][x] = Tile.EMPTY copy[y][x] = Tile.EMPTY
if y == y2 and x == x2: if y == y2 and x == x2:
return dist return False
for y, x in Map.neighbourhood(copy, y, x): for y, x in Map.neighbourhood(copy, y, x):
if copy[y][x].can_walk(): if copy[y][x].can_walk():
next_queue.append([y, x]) next_queue.append([y, x])
queue = next_queue queue = next_queue
return -1 return True
class Generator: class Generator:
@ -66,9 +66,9 @@ class Generator:
room: List[List[Tile]], door_y: int, door_x: int, room: List[List[Tile]], door_y: int, door_x: int,
dy: int, dx: int) -> bool: dy: int, dx: int) -> bool:
""" """
Using point (door_y, door_x) in the room as a reference and placing it Using point (door_y, door_x) in the room as a reference and placing it
over point (y, x) in the level, returns whether or not the room fits over point (y, x) in the level, returns whether or not the room fits
here here
""" """
lh, lw = len(level), len(level[0]) lh, lw = len(level), len(level[0])
rh, rw = len(room), len(room[0]) rh, rw = len(room), len(room[0])
@ -99,11 +99,12 @@ class Generator:
def place_room(level: List[List[Tile]], y: int, x: int, def place_room(level: List[List[Tile]], y: int, x: int,
room: List[List[Tile]], door_y: int, door_x: int) -> None: room: List[List[Tile]], door_y: int, door_x: int) -> None:
""" """
Mutates level in place to add the room. Placement is determined by Mutates level in place to add the room. Placement is determined by
making (door_y, door_x) in the room correspond with (y, x) in the level making (door_y, door_x) in the room correspond with (y, x) in the level
""" """
rh, rw = len(room), len(room[0]) rh, rw = len(room), len(room[0])
level[y][x] = Tile.DOOR # maybe place Tile.DOOR here instead ?
level[y][x] = Tile.FLOOR
for ry in range(rh): for ry in range(rh):
for rx in range(rw): for rx in range(rw):
if room[ry][rx] == Tile.FLOOR: if room[ry][rx] == Tile.FLOOR:
@ -112,11 +113,11 @@ class Generator:
@staticmethod @staticmethod
def add_loop(level: List[List[Tile]], y: int, x: int) -> bool: def add_loop(level: List[List[Tile]], y: int, x: int) -> bool:
""" """
Try to add a corridor between two far apart floor tiles, passing Try to add a corridor between two far apart floor tiles, passing
through point (y, x). through point (y, x).
""" """
h, w = len(level), len(level[0]) h, w = len(level), len(level[0])
if level[y][x] != Tile.EMPTY: if level[y][x] != Tile.EMPTY:
return False return False
@ -134,8 +135,8 @@ class Generator:
continue continue
def verify_sides() -> bool: def verify_sides() -> bool:
# switching up dy and dx here pivots the axis, so # switching up dy and dx here pivots the axis, so
# (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to # (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to
# (y, x), but not on the original axis # (y, x), but not on the original axis
for delta_x, delta_y in [[dy, dx], [-dy, -dx]]: for delta_x, delta_y in [[dy, dx], [-dy, -dx]]:
for i in range(1, y2 - y1 + x2 - x1): for i in range(1, y2 - y1 + x2 - x1):
@ -148,7 +149,7 @@ class Generator:
return True return True
# if adding the path would make the two tiles significantly closer # if adding the path would make the two tiles significantly closer
# and its sides don't touch already placed terrain, build it # and its sides don't touch already placed terrain, build it
if dist(level, y1, x1, y2, x2) < 20 and verify_sides(): if test_dist(level, y1, x1, y2, x2, 20) and verify_sides():
y, x = y1 + dy, x1 + dx y, x = y1 + dy, x1 + dx
while level[y][x] == Tile.EMPTY: while level[y][x] == Tile.EMPTY:
level[y][x] = Tile.FLOOR level[y][x] = Tile.FLOOR
@ -200,8 +201,8 @@ class Generator:
dy: int, dx: int, length: int) -> bool: dy: int, dx: int, length: int) -> bool:
""" """
Tries to build the exit from the room at given coordinates Tries to build the exit from the room at given coordinates
Depending on parameter length, it will either attempt to build a Depending on parameter length, it will either attempt to build a
simple door, or a long corridor. Return value is a boolean simple door, or a long corridor. Return value is a boolean
signifying whether or not the exit was successfully built signifying whether or not the exit was successfully built
""" """
rh, rw = len(room), len(room[0]) rh, rw = len(room), len(room[0])
@ -217,7 +218,8 @@ class Generator:
return False return False
# see if the path ahead is clear. needed in the case of non convex room # see if the path ahead is clear. needed in the case of non convex room
for i in range(length + 1): for i in range(length + 1):
if room[y + i * dy][x + i * dx] != Tile.EMPTY: if not(0 <= y + i * dy < rh and 0 <= x + i * dx < rw) \
or room[y + i * dy][x + i * dx] != Tile.EMPTY:
return False return False
for i in range(length): for i in range(length):
room[y + i * dy][x + i * dx] = Tile.FLOOR room[y + i * dy][x + i * dx] = Tile.FLOOR
@ -253,35 +255,33 @@ class Generator:
if room[y][x] == Tile.EMPTY and \ if room[y][x] == Tile.EMPTY and \
Generator.build_door(room, y, x, dy, dx, length): Generator.build_door(room, y, x, dy, dx, length):
break break
else: # pragma: no cover
return None, None, None, None
return y + length * dy, x + length * dx, dy, dx return y + length * dy, x + length * dx, dy, dx
def create_chunk_room(self, spawnable: bool = True) \ def create_chunk_room(self, spawnable: bool = True) \
-> Tuple[List[List[Tile]], int, int, int, int]: -> Tuple[List[List[Tile]], int, int, int, int]:
""" """
Create and return as a tile grid a room that is composed of multiple create and return as a tile grid a room that is composed of multiple
overlapping circles of the same radius overlapping circles of the same radius
Also return door info so we know how to place the room in the level also return door info so we know how to place the room in the level
""" """
height, width = 15, 15 height, width = 15, 15
nb_chunks, r = 6, 3 nb_chunks, r = 6, 3
h_sup, w_sup, h_off, w_off = self.corr_meta_info() h_sup, w_sup, h_off, w_off = self.corr_meta_info()
room = [[Tile.EMPTY] * (width + w_sup) room = [[Tile.EMPTY] * (width + w_sup) \
for _dummy in range(height + h_sup)] for _dummy in range(height + h_sup)]
def draw_chunk(room: List[List[Tile]], y: int, x: int) -> None: def draw_chunk(room, y, x):
for i in range(y - r, y + r + 1): for i in range(y - r, y + r + 1):
d = (y - i)**2 d = (y - i)**2
for j in range(x - r, x + r + 1): for j in range(x - r, x + r + 1):
if d + (x - j) ** 2 < r ** 2: if d + (x - j)**2 < r**2:
room[i][j] = Tile.FLOOR room[i][j] = Tile.FLOOR
draw_chunk(room, h_off + height // 2 + 1, w_off + width // 2 + 1) draw_chunk(room, h_off + height//2 + 1, w_off + width//2 + 1)
min_w, max_w = w_off + r + 1, width + w_off - r - 1 min_w, max_w = w_off + r + 1, width + w_off - r -1
min_h, max_h = h_off + r + 1, height + h_off - r - 1 min_h, max_h = h_off + r + 1, height + h_off - r - 1
for i in range(nb_chunks): for i in range(nb_chunks):
y, x = randint(min_h, max_h), randint(min_w, max_w) y, x = randint(min_h, max_h), randint(min_w, max_w)
@ -302,7 +302,7 @@ class Generator:
def create_circular_room(self, spawnable: bool = True) \ def create_circular_room(self, spawnable: bool = True) \
-> Tuple[List[List[Tile]], int, int, int, int]: -> Tuple[List[List[Tile]], int, int, int, int]:
""" """
Create and return as a tile grid a room that is circular in shape, and Create and return as a tile grid a room that is circular in shape, and
may have a center, also circular hole may have a center, also circular hole
Also return door info so we know how to place the room in the level Also return door info so we know how to place the room in the level
""" """
@ -342,10 +342,46 @@ class Generator:
return room, door_y, door_x, dy, dx return room, door_y, door_x, dy, dx
def create_rectangle_room(self, spawnable: bool = True) \
-> Tuple[List[List[Tile]], int, int, int, int]:
"""
create and return as a tile grid a rectangular room
also return door info so we know how to place the room in the level
"""
shrt, lng = randint(3, 6), randint(6, 12)
if random() < .5:
height, width = shrt, lng
else:
height, width = lng, shrt
room = []
h_sup, w_sup, h_off, w_off = self.corr_meta_info()
min_w, max_w = w_off + 1, width + w_off
min_h, max_h = h_off + 1, height + h_off
for i in range(height + h_sup + 2):
room.append([])
for j in range(width + w_sup + 2):
if min_h <= i <= max_h and min_w <= j <= max_w:
room[-1].append(Tile.FLOOR)
else:
room[-1].append(Tile.EMPTY)
# log all placed tiles as spawn positions
if spawnable:
self.register_spawn_area(room)
# attach exit
door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup,
h_off, w_off)
return room, door_y, door_x, dy, dx
def create_random_room(self, spawnable: bool = True) \ def create_random_room(self, spawnable: bool = True) \
-> Tuple[List[list], int, int, int, int]: -> Tuple[List[list], int, int, int, int]:
""" """
Randomly select a room shape and return one such room along with its Randomly select a room shape and return one such room along with its
door info. Set spawnable to False is the room should be marked as a door info. Set spawnable to False is the room should be marked as a
potential spawning region on the map potential spawning region on the map
""" """
@ -359,9 +395,11 @@ class Generator:
break break
if key == "circular": if key == "circular":
return self.create_circular_room(spawnable=spawnable) return self.create_circular_room(spawnable = spawnable)
elif key == "chunks": elif key == "chunks":
return self.create_chunk_room(spawnable=spawnable) return self.create_chunk_room(spawnable = spawnable)
elif key == "rectangle":
return self.create_rectangle_room(spawnable = spawnable)
def register_spawn_area(self, area: List[List[Tile]]) -> None: def register_spawn_area(self, area: List[List[Tile]]) -> None:
""" """
@ -378,8 +416,8 @@ class Generator:
def update_spawnable(self, y: int, x: int) -> None: def update_spawnable(self, y: int, x: int) -> None:
""" """
Convert previous spawn positions relative to the room grid to actual Convert previous spawn positions relative to the room grid to actual
actual spawn positions on the level grid, using the position of the actual spawn positions on the level grid, using the position of the
top left corner of the room on the level, then log them as a top left corner of the room on the level, then log them as a
spawnable region spawnable region
""" """
if self.queued_area is not None: if self.queued_area is not None:
@ -451,7 +489,6 @@ class Generator:
tries += 1 tries += 1
# post-processing # post-processing
self.place_walls(level)
# because when a room is placed, it leads to exactly one previously # because when a room is placed, it leads to exactly one previously
# placed room, the level has a tree like structure with the starting # placed room, the level has a tree like structure with the starting
@ -465,6 +502,9 @@ class Generator:
y, x = randint(0, height - 1), randint(0, width - 1) y, x = randint(0, height - 1), randint(0, width - 1)
loops += self.add_loop(level, y, x) loops += self.add_loop(level, y, x)
# surround the floor with walls
self.place_walls(level)
# place an exit ladder # place an exit ladder
y, x = randint(0, height - 1), randint(0, width - 1) y, x = randint(0, height - 1), randint(0, width - 1)
while level[y][x] != Tile.FLOOR or \ while level[y][x] != Tile.FLOOR or \

View File

@ -134,13 +134,13 @@ class TestEntities(unittest.TestCase):
self.map.remove_entity(entity2) self.map.remove_entity(entity2)
# Test following the player and finding the player as target # Test following the player and finding the player as target
self.player.move(6, 5) self.player.move(5, 5)
fam.move(5, 5) fam.move(4, 5)
fam.target = None fam.target = None
self.player.move_down() self.player.move_down()
self.map.tick(self.player) self.map.tick(self.player)
self.assertTrue(fam.target == self.player) self.assertTrue(fam.target == self.player)
self.assertEqual(fam.y, 6) self.assertEqual(fam.y, 5)
self.assertEqual(fam.x, 5) self.assertEqual(fam.x, 5)
# Test random move # Test random move
@ -155,9 +155,9 @@ class TestEntities(unittest.TestCase):
""" """
item = Item() item = Item()
self.map.add_entity(item) self.map.add_entity(item)
self.assertIsNone(item.held_by) self.assertFalse(item.held)
item.hold(self.player) item.hold(self.player)
self.assertEqual(item.held_by, self.player) self.assertTrue(item.held)
item.drop() item.drop()
self.assertEqual(item.y, 1) self.assertEqual(item.y, 1)
self.assertEqual(item.x, 6) self.assertEqual(item.x, 6)
@ -165,6 +165,7 @@ class TestEntities(unittest.TestCase):
# Pick up item # Pick up item
self.player.move_left() self.player.move_left()
self.player.move_right() self.player.move_right()
self.assertTrue(item.held)
self.assertEqual(item.held_by, self.player) self.assertEqual(item.held_by, self.player)
self.assertIn(item, self.player.inventory) self.assertIn(item, self.player.inventory)
self.assertNotIn(item, self.map.entities) self.assertNotIn(item, self.map.entities)
@ -207,10 +208,9 @@ class TestEntities(unittest.TestCase):
# The player can't hold the explosion # The player can't hold the explosion
explosion.hold(self.player) explosion.hold(self.player)
self.assertNotIn(explosion, self.player.inventory) self.assertNotIn(explosion, self.player.inventory)
self.assertIsNone(explosion.held_by) self.assertFalse(explosion.held)
# The explosion disappears after two ticks # The explosion disappears after one tick
explosion.act(self.map)
explosion.act(self.map) explosion.act(self.map)
self.assertNotIn(explosion, self.map.entities) self.assertNotIn(explosion, self.map.entities)

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
import os
import unittest import unittest
from ..bootstrap import Bootstrap from ..bootstrap import Bootstrap
@ -48,7 +49,6 @@ class TestGame(unittest.TestCase):
# Add items in the inventory to check that it is well loaded # Add items in the inventory to check that it is well loaded
bomb.hold(self.game.player) bomb.hold(self.game.player)
sword.hold(self.game.player) sword.hold(self.game.player)
sword.equip()
# Ensure that merchants can be saved # Ensure that merchants can be saved
merchant = Merchant() merchant = Merchant()
@ -100,6 +100,7 @@ class TestGame(unittest.TestCase):
Yeah, that's only for coverage. Yeah, that's only for coverage.
""" """
self.assertRaises(Exception, Bootstrap.run_game) self.assertRaises(Exception, Bootstrap.run_game)
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
def test_key_translation(self) -> None: def test_key_translation(self) -> None:
""" """
@ -255,7 +256,6 @@ class TestGame(unittest.TestCase):
self.game.map.add_entity(explosion) self.game.map.add_entity(explosion)
self.assertIn(explosion, self.game.map.entities) self.assertIn(explosion, self.game.map.entities)
self.game.handle_key_pressed(KeyValues.WAIT) self.game.handle_key_pressed(KeyValues.WAIT)
self.game.handle_key_pressed(KeyValues.WAIT)
self.assertNotIn(explosion, self.game.map.entities) self.assertNotIn(explosion, self.game.map.entities)
rabbit = Rabbit() rabbit = Rabbit()
@ -497,8 +497,10 @@ class TestGame(unittest.TestCase):
# Drop an item # Drop an item
bomb = self.game.player.inventory[-1] bomb = self.game.player.inventory[-1]
self.assertEqual(self.game.inventory_menu.validate(), bomb) self.assertEqual(self.game.inventory_menu.validate(), bomb)
self.assertTrue(bomb.held)
self.assertEqual(bomb.held_by, self.game.player) self.assertEqual(bomb.held_by, self.game.player)
self.game.handle_key_pressed(KeyValues.DROP) self.game.handle_key_pressed(KeyValues.DROP)
self.assertFalse(bomb.held)
self.assertIsNone(bomb.held_by) self.assertIsNone(bomb.held_by)
self.assertIsNone(bomb.owner) self.assertIsNone(bomb.owner)
self.assertFalse(bomb.exploding) self.assertFalse(bomb.exploding)
@ -508,8 +510,10 @@ class TestGame(unittest.TestCase):
# Use the bomb # Use the bomb
bomb = self.game.player.inventory[-1] bomb = self.game.player.inventory[-1]
self.assertEqual(self.game.inventory_menu.validate(), bomb) self.assertEqual(self.game.inventory_menu.validate(), bomb)
self.assertTrue(bomb.held)
self.assertEqual(bomb.held_by, self.game.player) self.assertEqual(bomb.held_by, self.game.player)
self.game.handle_key_pressed(KeyValues.USE) self.game.handle_key_pressed(KeyValues.USE)
self.assertFalse(bomb.held)
self.assertIsNone(bomb.held_by) self.assertIsNone(bomb.held_by)
self.assertEqual(bomb.owner, self.game.player) self.assertEqual(bomb.owner, self.game.player)
self.assertTrue(bomb.exploding) self.assertTrue(bomb.exploding)
@ -662,37 +666,42 @@ class TestGame(unittest.TestCase):
sword.hold(self.game.player) sword.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.EQUIP) self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_main, sword) self.assertEqual(self.game.player.equipped_main, sword)
self.assertFalse(self.game.player.inventory)
# shield goes into the secondary equipment slot # shield goes into the secondary equipment slot
shield = Shield() shield = Shield()
shield.hold(self.game.player) shield.hold(self.game.player)
shield.equip() self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_secondary, shield) self.assertEqual(self.game.player.equipped_secondary, shield)
self.assertFalse(self.game.player.inventory)
# helmet goes into the helmet slot # helmet goes into the helmet slot
helmet = Helmet() helmet = Helmet()
helmet.hold(self.game.player) helmet.hold(self.game.player)
helmet.equip() self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_helmet, helmet) self.assertEqual(self.game.player.equipped_helmet, helmet)
self.assertFalse(self.game.player.inventory)
# helmet goes into the armor slot # helmet goes into the armor slot
chestplate = Chestplate() chestplate = Chestplate()
chestplate.hold(self.game.player) chestplate.hold(self.game.player)
chestplate.equip() self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_armor, chestplate) self.assertEqual(self.game.player.equipped_armor, chestplate)
self.assertFalse(self.game.player.inventory)
# Use bomb # Use bomb
bomb = Bomb() bomb = Bomb()
bomb.hold(self.game.player) bomb.hold(self.game.player)
bomb.equip() self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_secondary, bomb) self.assertEqual(self.game.player.equipped_secondary, bomb)
self.assertFalse(shield.equipped) self.assertIn(shield, self.game.player.inventory)
self.game.state = GameMode.PLAY self.game.state = GameMode.PLAY
self.game.handle_key_pressed(KeyValues.USE) self.game.handle_key_pressed(KeyValues.USE)
self.assertIsNone(self.game.player.equipped_secondary) self.assertIsNone(self.game.player.equipped_secondary)
self.game.state = GameMode.INVENTORY self.game.state = GameMode.INVENTORY
shield.equip() self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_secondary, shield) self.assertEqual(self.game.player.equipped_secondary, shield)
self.assertFalse(self.game.player.inventory)
# Reequip, which is useless but covers code # Reequip, which is useless but covers code
sword.equip() sword.equip()
@ -714,13 +723,11 @@ class TestGame(unittest.TestCase):
self.assertIn(shield, self.game.player.inventory) self.assertIn(shield, self.game.player.inventory)
self.assertIn(helmet, self.game.player.inventory) self.assertIn(helmet, self.game.player.inventory)
self.assertIn(chestplate, self.game.player.inventory) self.assertIn(chestplate, self.game.player.inventory)
self.game.display_actions(DisplayActions.REFRESH)
# Test rings # Test rings
self.game.player.inventory.clear() self.game.player.inventory.clear()
ring = RingCritical() ring = RingCritical()
ring.hold(self.game.player) ring.hold(self.game.player)
self.game.display_actions(DisplayActions.REFRESH)
old_critical = self.game.player.critical old_critical = self.game.player.critical
self.game.handle_key_pressed(KeyValues.EQUIP) self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.critical, self.assertEqual(self.game.player.critical,
@ -944,18 +951,3 @@ class TestGame(unittest.TestCase):
# Exit the menu # Exit the menu
self.game.handle_key_pressed(KeyValues.SPACE) self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY) self.assertEqual(self.game.state, GameMode.PLAY)
def test_doors(self) -> None:
"""
Check that the user can open doors.
"""
self.game.state = GameMode.PLAY
self.game.player.move(9, 8)
self.assertEqual(self.game.map.tiles[10][8], Tile.DOOR)
# Open door
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.map.tiles[10][8], Tile.FLOOR)
self.assertEqual(self.game.player.y, 10)
self.assertEqual(self.game.player.x, 8)
self.game.display_actions(DisplayActions.REFRESH)

View File

@ -26,17 +26,16 @@ class TestBroguelike(unittest.TestCase):
def is_connex(self, grid: List[List[Tile]]) -> bool: def is_connex(self, grid: List[List[Tile]]) -> bool:
h, w = len(grid), len(grid[0]) h, w = len(grid), len(grid[0])
y, x = -1, -1 y, x = randint(0, h - 1), randint(0, w - 1)
while not grid[y][x].can_walk(): while not (grid[y][x].can_walk()):
y, x = randint(0, h - 1), randint(0, w - 1) y, x = randint(0, h - 1), randint(0, w - 1)
queue = Map.neighbourhood(grid, y, x) queue = Map.neighbourhood(grid, y, x)
while queue: while queue:
y, x = queue.pop() y, x = queue.pop()
if grid[y][x].can_walk() or grid[y][x] == Tile.DOOR: if grid[y][x].can_walk():
grid[y][x] = Tile.WALL grid[y][x] = Tile.WALL
queue += Map.neighbourhood(grid, y, x) queue += Map.neighbourhood(grid, y, x)
return not any([t.can_walk() or t == Tile.DOOR return not any([t.can_walk() for row in grid for t in row])
for row in grid for t in row])
def test_build_doors(self) -> None: def test_build_doors(self) -> None:
m = self.stom(". .\n. .\n. .\n") m = self.stom(". .\n. .\n. .\n")

View File

@ -25,8 +25,6 @@ class Translator:
Loads compiled translations. Loads compiled translations.
""" """
for language in cls.SUPPORTED_LOCALES: for language in cls.SUPPORTED_LOCALES:
if language == "en":
continue
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES" rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
rep.mkdir(parents=True) if not rep.is_dir() else None rep.mkdir(parents=True) if not rep.is_dir() else None
if os.path.isfile(rep / "squirrelbattle.mo"): if os.path.isfile(rep / "squirrelbattle.mo"):
@ -67,7 +65,7 @@ class Translator:
args = ["xargs", "xgettext", "--from-code", "utf-8", args = ["xargs", "xgettext", "--from-code", "utf-8",
"--add-comments", "--add-comments",
"--package-name=squirrelbattle", "--package-name=squirrelbattle",
"--package-version=23.14", "--package-version=3.14.1",
"--copyright-holder=ÿnérant, eichhornchen, " "--copyright-holder=ÿnérant, eichhornchen, "
"nicomarg, charlse, ifugao", "nicomarg, charlse, ifugao",
"--msgid-bugs-address=squirrel-battle@crans.org", "--msgid-bugs-address=squirrel-battle@crans.org",