# 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, Optional, Tuple from .items import Item from ..interfaces import FightingEntity, InventoryHolder class Player(InventoryHolder, FightingEntity): """ The class of the player """ current_xp: int = 0 max_xp: int = 10 paths: Dict[Tuple[int, int], Tuple[int, int]] equipped_item: Optional[Item] equipped_armor: Optional[Item] 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, hazel: int = 42, equipped_item: Optional[Item] = None, equipped_armor: Optional[Item] = 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 = self.translate_inventory(inventory or []) self.paths = dict() self.hazel = hazel if isinstance(equipped_item, dict): equipped_item = self.dict_to_item(equipped_item) if isinstance(equipped_armor, dict): equipped_armor = self.dict_to_item(equipped_armor) self.equipped_item = equipped_item self.equipped_armor = equipped_armor 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() 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_item: self.equipped_item = None elif obj == self.equipped_armor: self.equipped_armor = None else: return super().remove_from_inventory(obj) # 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["equipped_item"] = self.equipped_item.save_state()\ if self.equipped_item else None d["equipped_armor"] = self.equipped_armor.save_state()\ if self.equipped_armor else None return d