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:
@ -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)
|
||||
|
@ -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.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user