Merge branch 'master' into 'equipment'

# Conflicts:
#   squirrelbattle/display/statsdisplay.py
#   squirrelbattle/entities/items.py
#   squirrelbattle/entities/player.py
#   squirrelbattle/interfaces.py
#   squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po
#   squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po
#   squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po
#   squirrelbattle/tests/game_test.py
This commit is contained in:
2021-01-08 02:11:40 +01:00
34 changed files with 1167 additions and 333 deletions

View File

@ -1,17 +1,18 @@
from ..interfaces import FriendlyEntity, InventoryHolder
from ..interfaces import FriendlyEntity, InventoryHolder, Map, FightingEntity
from ..translations import gettext as _
from .player import Player
from .monsters import Monster
from .items import Item
from random import choice
from random import choice, shuffle
class Merchant(InventoryHolder, FriendlyEntity):
"""
The class for merchants in the dungeon
The class of merchants in the dungeon.
"""
def keys(self) -> list:
"""
Returns a friendly entitie's specific attributes
Returns a friendly entitie's specific attributes.
"""
return super().keys() + ["inventory", "hazel"]
@ -20,7 +21,6 @@ class Merchant(InventoryHolder, FriendlyEntity):
super().__init__(name=name, *args, **kwargs)
self.inventory = self.translate_inventory(inventory or [])
self.hazel = hazel
if not self.inventory:
for i in range(5):
self.inventory.append(choice(Item.get_all_items())())
@ -28,20 +28,20 @@ class Merchant(InventoryHolder, FriendlyEntity):
def talk_to(self, player: Player) -> str:
"""
This function is used to open the merchant's inventory in a menu,
and allow the player to buy/sell objects
and allows the player to buy/sell objects.
"""
return _("I don't sell any squirrel")
def change_hazel_balance(self, hz: int) -> None:
"""
Change the number of hazel the merchant has by hz.
Changes the number of hazel the merchant has by hz.
"""
self.hazel += hz
class Sunflower(FriendlyEntity):
"""
A friendly sunflower
A friendly sunflower.
"""
def __init__(self, maxhealth: int = 15,
*args, **kwargs) -> None:
@ -49,4 +49,80 @@ class Sunflower(FriendlyEntity):
@property
def dialogue_option(self) -> list:
"""
Lists all that a sunflower can say to the player.
"""
return [_("Flower power!!"), _("The sun is warm today")]
class Familiar(FightingEntity):
"""
A friendly familiar that helps the player defeat monsters.
"""
def __init__(self, maxhealth: int = 25,
*args, **kwargs) -> None:
super().__init__(maxhealth=maxhealth, *args, **kwargs)
self.target = None
# @property
# def dialogue_option(self) -> list:
# """
# Debug function (to see if used in the real game)
# """
# return [_("My target is"+str(self.target))]
def act(self, p: Player, m: Map) -> None:
"""
By default, the familiar tries to stay at distance at most 2 of the
player and if a monster comes in range 3, it focuses on the monster
and attacks it.
"""
if self.target is None:
# If the previous target is dead(or if there was no previous target)
# the familiar tries to get closer to the player.
self.target = p
elif self.target.dead:
self.target = p
if self.target == p:
# Look for monsters around the player to kill TOFIX : if monster is
# out of range, continue targetting player.
for entity in m.entities:
if (p.y - entity.y) ** 2 + (p.x - entity.x) ** 2 <= 9 and\
isinstance(entity, Monster):
self.target = entity
entity.paths = dict() # Allows the paths to be calculated.
break
# Familiars move according to a Dijkstra algorithm
# that targets their target.
# If they can not move and are already close to their target,
# they hit, except if their target is the player.
if self.target and (self.y, self.x) in self.target.paths:
# Moves to target player by choosing the best available path
for next_y, next_x in self.target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
break
if self.distance_squared(self.target) <= 1 and \
not isinstance(self.target, Player):
self.map.logs.add_message(self.hit(self.target))
break
else:
# Moves in a random direction
# If the direction is not available, tries another one
moves = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
for move in moves:
if move():
break
class Trumpet(Familiar):
"""
A class of familiars.
"""
def __init__(self, name: str = "trumpet", strength: int = 3,
maxhealth: int = 20, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -10,7 +10,7 @@ from ..translations import gettext as _
class Item(Entity):
"""
A class for items
A class for items.
"""
held: bool
held_by: Optional[InventoryHolder]
@ -26,7 +26,7 @@ class Item(Entity):
def drop(self) -> None:
"""
The item is dropped from the inventory onto the floor
The item is dropped from the inventory onto the floor.
"""
if self.held:
self.held_by.remove_from_inventory(self)
@ -59,7 +59,7 @@ class Item(Entity):
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
@ -68,7 +68,7 @@ class Item(Entity):
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
Saves the state of the item into a dictionary.
"""
d = super().save_state()
d["held"] = self.held
@ -76,6 +76,9 @@ class Item(Entity):
@staticmethod
def get_all_items() -> list:
"""
Returns the list of all item classes.
"""
return [BodySnatchPotion, Bomb, Heart, Shield, Sword,
Chestplate, Helmet, RingCritical, RingXP]
@ -83,7 +86,7 @@ class Item(Entity):
"""
Does all necessary actions when an object is to be sold.
Is overwritten by some classes that cannot exist in the player's
inventory
inventory.
"""
if buyer.hazel >= self.price:
self.hold(buyer)
@ -97,7 +100,7 @@ class Item(Entity):
class Heart(Item):
"""
A heart item to return health to the player
A heart item to return health to the player.
"""
healing: int
@ -108,14 +111,15 @@ class Heart(Item):
def hold(self, entity: InventoryHolder) -> None:
"""
When holding a heart, heal the player and don't put item in inventory.
When holding a heart, the player is healed and
the item is not put in the inventory.
"""
entity.health = min(entity.maxhealth, entity.health + self.healing)
entity.map.remove_entity(self)
def save_state(self) -> dict:
"""
Saves the state of the header into a dictionary
Saves the state of the heart into a dictionary.
"""
d = super().save_state()
d["healing"] = self.healing
@ -141,7 +145,7 @@ class Bomb(Item):
def use(self) -> None:
"""
When the bomb is used, throw it and explodes it.
When the bomb is used, it is thrown and then it explodes.
"""
if self.held:
self.owner = self.held_by
@ -150,7 +154,7 @@ class Bomb(Item):
def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
Special exploding action of the bomb.
"""
if self.exploding:
if self.tick > 0:
@ -176,7 +180,7 @@ class Bomb(Item):
def save_state(self) -> dict:
"""
Saves the state of the bomb into a dictionary
Saves the state of the bomb into a dictionary.
"""
d = super().save_state()
d["exploding"] = self.exploding
@ -193,13 +197,13 @@ class Explosion(Item):
def act(self, m: Map) -> None:
"""
The explosion instant dies.
The bomb disappears after exploding.
"""
m.remove_entity(self)
def hold(self, player: InventoryHolder) -> None:
"""
The player can't hold any explosion.
The player can't hold an explosion.
"""

View File

@ -10,8 +10,8 @@ from ..interfaces import FightingEntity, Map
class Monster(FightingEntity):
"""
The class for all monsters in the dungeon.
A monster must override this class, and the parameters are given
in the __init__ function.
All specific monster classes overwrite this class,
and the parameters are given in the __init__ function.
An example of the specification of a monster that has a strength of 4
and 20 max HP:
@ -21,7 +21,7 @@ class Monster(FightingEntity):
super().__init__(name="my_monster", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
With that way, attributes can be overwritten when the entity got created.
With that way, attributes can be overwritten when the entity is created.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -29,7 +29,7 @@ class Monster(FightingEntity):
def act(self, m: Map) -> None:
"""
By default, a monster will move randomly where it is possible
And if a player is close to the monster, the monster run on the player.
If the player is closeby, the monster runs to the player.
"""
target = None
for entity in m.entities:
@ -38,12 +38,12 @@ class Monster(FightingEntity):
target = entity
break
# A Dijkstra algorithm has ran that targets the player.
# With that way, monsters can simply follow the path.
# If they can't move and they are already close to the player,
# They hit.
# Monsters move according to a Dijkstra algorithm
# that targets the player.
# If they can not move and are already close to the player,
# they hit.
if target and (self.y, self.x) in target.paths:
# Move to target player by choosing the best avaliable path
# Moves to target player by choosing the best available path
for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
@ -52,8 +52,8 @@ class Monster(FightingEntity):
self.map.logs.add_message(self.hit(target))
break
else:
# Move in a random direction
# If the direction is not available, try another one
# Moves in a random direction
# If the direction is not available, tries another one
moves = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
@ -61,10 +61,17 @@ class Monster(FightingEntity):
if move():
break
def move(self, y: int, x: int) -> None:
"""
Overwrites the move function to recalculate paths.
"""
super().move(y, x)
self.recalculate_paths()
class Tiger(Monster):
"""
A tiger monster
A tiger monster.
"""
def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None:
@ -74,7 +81,7 @@ class Tiger(Monster):
class Hedgehog(Monster):
"""
A really mean hedgehog monster
A really mean hedgehog monster.
"""
def __init__(self, name: str = "hedgehog", strength: int = 3,
maxhealth: int = 10, *args, **kwargs) -> None:
@ -84,7 +91,7 @@ class Hedgehog(Monster):
class Rabbit(Monster):
"""
A rabbit monster
A rabbit monster.
"""
def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, critical: int = 30,
@ -96,7 +103,7 @@ class Rabbit(Monster):
class TeddyBear(Monster):
"""
A cute teddybear monster
A cute teddybear monster.
"""
def __init__(self, name: str = "teddy_bear", strength: int = 0,
maxhealth: int = 50, *args, **kwargs) -> None:

View File

@ -1,10 +1,11 @@
# 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
<<<<<<< squirrelbattle/entities/player.py
from typing import Dict, Optional, Tuple
=======
>>>>>>> squirrelbattle/entities/player.py
from .items import Item
from ..interfaces import FightingEntity, InventoryHolder
@ -12,7 +13,7 @@ from ..interfaces import FightingEntity, InventoryHolder
class Player(InventoryHolder, FightingEntity):
"""
The class of the player
The class of the player.
"""
current_xp: int = 0
max_xp: int = 10
@ -31,7 +32,7 @@ class Player(InventoryHolder, FightingEntity):
equipped_armor: Optional[Item] = None, critical: int = 5,
equipped_secondary: Optional[Item] = None,
equipped_helmet: Optional[Item] = None, xp_buff: float = 1,
*args, **kwargs) -> None:
vision: int = 5, *args, **kwargs) -> None:
super().__init__(name=name, maxhealth=maxhealth, strength=strength,
intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution,
@ -50,6 +51,7 @@ class Player(InventoryHolder, FightingEntity):
if isinstance(equipped_secondary, dict) else equipped_secondary
self.equipped_helmet = self.dict_to_item(equipped_helmet) \
if isinstance(equipped_helmet, dict) else equipped_helmet
self.vision = vision
def move(self, y: int, x: int) -> None:
"""
@ -60,10 +62,11 @@ class Player(InventoryHolder, FightingEntity):
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
self.map.compute_visibility(self.y, self.x, self.vision)
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
Add as many levels as possible to the player.
"""
while self.current_xp > self.max_xp:
self.level += 1
@ -77,8 +80,8 @@ class Player(InventoryHolder, FightingEntity):
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
Adds some experience to the player.
If the required amount is reached, the player levels up.
"""
self.current_xp += int(xp * self.xp_buff)
self.level_up()
@ -120,56 +123,6 @@ class Player(InventoryHolder, FightingEntity):
entity.hold(self)
return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths for monsters to go to
the player. Actually, the paths are computed for each tile adjacent to
the player then for each step the monsters use the best path avaliable.
"""
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary