From 7823a422b9a7f35dcefa85e1ec218b9eddd3a1fe Mon Sep 17 00:00:00 2001 From: Nicolas Margulies Date: Tue, 8 Dec 2020 00:59:19 +0100 Subject: [PATCH 1/4] Start f new pathfinding, not working --- squirrelbattle/display/mapdisplay.py | 19 +++++++++++++++++++ squirrelbattle/entities/player.py | 25 ++++++++++++++----------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index 445f736..93be162 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -3,6 +3,7 @@ from squirrelbattle.interfaces import Map from .display import Display +from squirrelbattle.entities.player import Player class MapDisplay(Display): @@ -22,6 +23,24 @@ class MapDisplay(Display): for e in self.map.entities: self.addstr(self.pad, e.y, self.pack.tile_width * e.x, self.pack[e.name.upper()], self.color_pair(2)) + players = [ p for p in self.map.entities if isinstance(p,Player) ] + player = players[0] if len(players) > 0 else None + if player: + for x in range(self.map.width): + for y in range(self.map.height): + if (y,x) in player.paths: + deltay, deltax = (y - player.path[(y, x)][0], + x - player.path[(y, x)][1]) + if (deltay, deltax) == (-1, 0): + character = '╹' + elif (deltay, deltax) == (1, 0): + character = '╻' + elif (deltay, deltax) == (0, -1): + character = '╺' + else: + character = '╸' + self.addstr(self.pad, y, self.pack.tile_width * x, + character, self.color_pair(1)) def display(self) -> None: y, x = self.map.currenty, self.pack.tile_width * self.map.currentx diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 3f1d941..0875d3a 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -3,6 +3,7 @@ from random import randint from typing import Dict, Tuple +from queue import PriorityQueue from ..interfaces import FightingEntity @@ -95,26 +96,28 @@ class Player(FightingEntity): Use Dijkstra algorithm to calculate best paths for monsters to go to the player. """ - queue = [(self.y, self.x)] + queue = PriorityQueue() + queue.put((0, (self.y, self.x))) visited = [] distances = {(self.y, self.x): 0} predecessors = {} - while queue: - y, x = queue.pop(0) - visited.append((y, x)) - if distances[(y, x)] >= max_distance: + while not queue.empty: + dist, (y, x) = queue.get() + if dist >= 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[y][x].can_walk() or \ - (new_y, new_x) in visited or \ - (new_y, new_x) in queue: + not self.map.tiles[y][x].can_walk(): continue - predecessors[(new_y, new_x)] = (y, x) - distances[(new_y, new_x)] = distances[(y, x)] + 1 - queue.append((new_y, new_x)) + new_distance = dist + 1 + if not (new_y, new_x) in distances or \ + distances[(new_y, new_x)] > new_distance: + predecessors[(new_y, new_x)] = (y, x) + distances[(new_y, new_x)] = new_distance + queue.put(new_distance, (new_y, new_x)) self.paths = predecessors def save_state(self) -> dict: From 50d806cdcf7e4d34225f7a2dcff0e039c2c6933f Mon Sep 17 00:00:00 2001 From: Nicolas Margulies Date: Tue, 8 Dec 2020 22:22:20 +0100 Subject: [PATCH 2/4] Working Dijkstra --- squirrelbattle/display/mapdisplay.py | 12 ++++++------ squirrelbattle/entities/player.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index 93be162..1ac99bb 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -29,16 +29,16 @@ class MapDisplay(Display): for x in range(self.map.width): for y in range(self.map.height): if (y,x) in player.paths: - deltay, deltax = (y - player.path[(y, x)][0], - x - player.path[(y, x)][1]) + deltay, deltax = (y - player.paths[(y, x)][0], + x - player.paths[(y, x)][1]) if (deltay, deltax) == (-1, 0): - character = '╹' + character = '↓' elif (deltay, deltax) == (1, 0): - character = '╻' + character = '↑' elif (deltay, deltax) == (0, -1): - character = '╺' + character = '→' else: - character = '╸' + character = '←' self.addstr(self.pad, y, self.pack.tile_width * x, character, self.color_pair(1)) diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index 0875d3a..b814d88 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -101,7 +101,7 @@ class Player(FightingEntity): visited = [] distances = {(self.y, self.x): 0} predecessors = {} - while not queue.empty: + while not queue.empty(): dist, (y, x) = queue.get() if dist >= max_distance or (y,x) in visited: continue @@ -110,14 +110,14 @@ class Player(FightingEntity): 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[y][x].can_walk(): + not self.map.tiles[new_y][new_x].can_walk(): continue new_distance = dist + 1 if not (new_y, new_x) in distances or \ distances[(new_y, new_x)] > new_distance: predecessors[(new_y, new_x)] = (y, x) distances[(new_y, new_x)] = new_distance - queue.put(new_distance, (new_y, new_x)) + queue.put((new_distance, (new_y, new_x))) self.paths = predecessors def save_state(self) -> dict: From cc6033e8e437587e236debe32efb4073cfe90b5a Mon Sep 17 00:00:00 2001 From: Nicolas Margulies Date: Thu, 10 Dec 2020 22:21:09 +0100 Subject: [PATCH 3/4] New pathfinding that avoids most of the mobs getting stuck, closes #35 --- squirrelbattle/display/mapdisplay.py | 40 ++++++++-------- squirrelbattle/entities/monsters.py | 13 ++++-- squirrelbattle/entities/player.py | 69 ++++++++++++++++++---------- 3 files changed, 74 insertions(+), 48 deletions(-) diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index 1ac99bb..d403f7f 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -3,7 +3,6 @@ from squirrelbattle.interfaces import Map from .display import Display -from squirrelbattle.entities.player import Player class MapDisplay(Display): @@ -23,24 +22,27 @@ class MapDisplay(Display): for e in self.map.entities: self.addstr(self.pad, e.y, self.pack.tile_width * e.x, self.pack[e.name.upper()], self.color_pair(2)) - players = [ p for p in self.map.entities if isinstance(p,Player) ] - player = players[0] if len(players) > 0 else None - if player: - for x in range(self.map.width): - for y in range(self.map.height): - if (y,x) in player.paths: - deltay, deltax = (y - player.paths[(y, x)][0], - x - player.paths[(y, x)][1]) - if (deltay, deltax) == (-1, 0): - character = '↓' - elif (deltay, deltax) == (1, 0): - character = '↑' - elif (deltay, deltax) == (0, -1): - character = '→' - else: - character = '←' - self.addstr(self.pad, y, self.pack.tile_width * x, - character, self.color_pair(1)) + + # Display Path map for deubg purposes + # from squirrelbattle.entities.player import Player + # players = [ p for p in self.map.entities if isinstance(p,Player) ] + # player = players[0] if len(players) > 0 else None + # if player: + # for x in range(self.map.width): + # for y in range(self.map.height): + # if (y,x) in player.paths: + # deltay, deltax = (y - player.paths[(y, x)][0], + # x - player.paths[(y, x)][1]) + # if (deltay, deltax) == (-1, 0): + # character = '↓' + # elif (deltay, deltax) == (1, 0): + # character = '↑' + # elif (deltay, deltax) == (0, -1): + # character = '→' + # else: + # character = '←' + # self.addstr(self.pad, y, self.pack.tile_width * x, + # character, self.color_pair(1)) def display(self) -> None: y, x = self.map.currenty, self.pack.tile_width * self.map.currentx diff --git a/squirrelbattle/entities/monsters.py b/squirrelbattle/entities/monsters.py index feff81a..34cd4bf 100644 --- a/squirrelbattle/entities/monsters.py +++ b/squirrelbattle/entities/monsters.py @@ -43,11 +43,14 @@ class Monster(FightingEntity): # If they can't move and they are already close to the player, # They hit. if target and (self.y, self.x) in target.paths: - # Move to target player - next_y, next_x = target.paths[(self.y, self.x)] - moved = self.check_move(next_y, next_x, True) - if not moved and self.distance_squared(target) <= 1: - self.map.logs.add_message(self.hit(target)) + # Move to target player by choosing the best avaliable path + for next_y, next_x in target.paths[(self.y, self.x)]: + moved = self.check_move(next_y, next_x, True) + if moved: + break + if self.distance_squared(target) <= 1: + self.map.logs.add_message(self.hit(target)) + break else: # Move in a random direction # If the direction is not available, try another one diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index b814d88..dd4d6f0 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -1,9 +1,10 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later +from functools import reduce +from queue import PriorityQueue from random import randint from typing import Dict, Tuple -from queue import PriorityQueue from ..interfaces import FightingEntity @@ -93,32 +94,52 @@ class Player(FightingEntity): def recalculate_paths(self, max_distance: int = 8) -> None: """ - Use Dijkstra algorithm to calculate best paths - for monsters to go to the player. + 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. """ - queue = PriorityQueue() - queue.put((0, (self.y, self.x))) - visited = [] - distances = {(self.y, self.x): 0} - predecessors = {} - while not queue.empty(): - dist, (y, x) = queue.get() - if dist >= max_distance or (y,x) in visited: + 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 - 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(): + 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 - new_distance = dist + 1 - if not (new_y, new_x) in distances or \ - distances[(new_y, new_x)] > new_distance: - predecessors[(new_y, new_x)] = (y, x) - distances[(new_y, new_x)] = new_distance - queue.put((new_distance, (new_y, new_x))) - self.paths = predecessors + 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]): + 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: """ From 01cc77e1463623a6e78490849c0f25ba2731d724 Mon Sep 17 00:00:00 2001 From: Nicolas Margulies Date: Thu, 10 Dec 2020 22:28:12 +0100 Subject: [PATCH 4/4] Fixed a bug when trying to pathfind when player is surrounded by inaccessible tiles --- squirrelbattle/entities/player.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index dd4d6f0..45e2bdf 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -133,10 +133,11 @@ class Player(FightingEntity): # 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 + # 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]): + 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]])]