New pathfinding that avoids most of the mobs getting stuck, closes #35

This commit is contained in:
Nicolas Margulies 2020-12-10 22:21:09 +01:00
parent 50d806cdcf
commit cc6033e8e4
3 changed files with 74 additions and 48 deletions

View File

@ -3,7 +3,6 @@
from squirrelbattle.interfaces import Map from squirrelbattle.interfaces import Map
from .display import Display from .display import Display
from squirrelbattle.entities.player import Player
class MapDisplay(Display): class MapDisplay(Display):
@ -23,24 +22,27 @@ class MapDisplay(Display):
for e in self.map.entities: for e in self.map.entities:
self.addstr(self.pad, e.y, self.pack.tile_width * e.x, self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2)) 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 # Display Path map for deubg purposes
if player: # from squirrelbattle.entities.player import Player
for x in range(self.map.width): # players = [ p for p in self.map.entities if isinstance(p,Player) ]
for y in range(self.map.height): # player = players[0] if len(players) > 0 else None
if (y,x) in player.paths: # if player:
deltay, deltax = (y - player.paths[(y, x)][0], # for x in range(self.map.width):
x - player.paths[(y, x)][1]) # for y in range(self.map.height):
if (deltay, deltax) == (-1, 0): # if (y,x) in player.paths:
character = '' # deltay, deltax = (y - player.paths[(y, x)][0],
elif (deltay, deltax) == (1, 0): # x - player.paths[(y, x)][1])
character = '' # if (deltay, deltax) == (-1, 0):
elif (deltay, deltax) == (0, -1): # character = '↓'
character = '' # elif (deltay, deltax) == (1, 0):
else: # character = '↑'
character = '' # elif (deltay, deltax) == (0, -1):
self.addstr(self.pad, y, self.pack.tile_width * x, # character = '→'
character, self.color_pair(1)) # else:
# character = '←'
# self.addstr(self.pad, y, self.pack.tile_width * x,
# character, self.color_pair(1))
def display(self) -> None: def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx y, x = self.map.currenty, self.pack.tile_width * self.map.currentx

View File

@ -43,11 +43,14 @@ class Monster(FightingEntity):
# If they can't move and they are already close to the player, # If they can't move and they are already close to the player,
# They hit. # They hit.
if target and (self.y, self.x) in target.paths: if target and (self.y, self.x) in target.paths:
# Move to target player # Move to target player by choosing the best avaliable path
next_y, next_x = target.paths[(self.y, self.x)] for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True) moved = self.check_move(next_y, next_x, True)
if not moved and self.distance_squared(target) <= 1: if moved:
self.map.logs.add_message(self.hit(target)) break
if self.distance_squared(target) <= 1:
self.map.logs.add_message(self.hit(target))
break
else: else:
# Move in a random direction # Move in a random direction
# If the direction is not available, try another one # If the direction is not available, try another one

View File

@ -1,9 +1,10 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from functools import reduce
from queue import PriorityQueue
from random import randint from random import randint
from typing import Dict, Tuple from typing import Dict, Tuple
from queue import PriorityQueue
from ..interfaces import FightingEntity from ..interfaces import FightingEntity
@ -93,32 +94,52 @@ class Player(FightingEntity):
def recalculate_paths(self, max_distance: int = 8) -> None: def recalculate_paths(self, max_distance: int = 8) -> None:
""" """
Use Dijkstra algorithm to calculate best paths Use Dijkstra algorithm to calculate best paths for monsters to go to
for monsters to go to the player. 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() distances = []
queue.put((0, (self.y, self.x))) predecessors = []
visited = [] # four Dijkstras, one for each adjacent tile
distances = {(self.y, self.x): 0} for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
predecessors = {} queue = PriorityQueue()
while not queue.empty(): new_y, new_x = self.y + dir_y, self.x + dir_x
dist, (y, x) = queue.get() if not 0 <= new_y < self.map.height or \
if dist >= max_distance or (y,x) in visited: not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue continue
visited.append((y, x)) queue.put(((1, 0), (new_y, new_x)))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]: visited = [(self.y, self.x)]
new_y, new_x = y + diff_y, x + diff_x distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
if not 0 <= new_y < self.map.height or \ predecessors.append({(new_y, new_x): (self.y, self.x)})
not 0 <= new_x < self.map.width or \ while not queue.empty():
not self.map.tiles[new_y][new_x].can_walk(): dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue continue
new_distance = dist + 1 visited.append((y, x))
if not (new_y, new_x) in distances or \ for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
distances[(new_y, new_x)] > new_distance: new_y, new_x = y + diff_y, x + diff_x
predecessors[(new_y, new_x)] = (y, x) if not 0 <= new_y < self.map.height or \
distances[(new_y, new_x)] = new_distance not 0 <= new_x < self.map.width or \
queue.put((new_distance, (new_y, new_x))) not self.map.tiles[new_y][new_x].can_walk():
self.paths = predecessors 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: def save_state(self) -> dict:
""" """