From 0653d0d1f2f2d82ca5cc4c9a5c908c74eb15d29a Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 3 Apr 2020 18:26:01 -0400 Subject: [PATCH] v0.8.0: AI improvements (primarily pathfinding): - removed path lookaheads, characters now only check the next tile for validity - the hero is now interrupted if their path is broken and replaced by a much longer one - added pathfinding functionality to ignore characters - significantly improved hunting enemy pathfinding when a route is blocked - improved enemy logic for switching targets if current one is unreachable --- .../shatteredpixeldungeon/Dungeon.java | 44 ++++---- .../actors/hero/Hero.java | 18 ++-- .../actors/mobs/Ghoul.java | 49 --------- .../actors/mobs/Mob.java | 101 ++++++++++++------ .../actors/mobs/NewDM300.java | 4 +- .../actors/mobs/Piranha.java | 8 +- 6 files changed, 105 insertions(+), 119 deletions(-) diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java index 8335b81ed..a9c2da549 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java @@ -808,7 +808,7 @@ public class Dungeon { BArray.setFalse(passable); } - public static PathFinder.Path findPath(Char ch, int from, int to, boolean pass[], boolean[] visible ) { + public static PathFinder.Path findPath(Char ch, int to, boolean[] pass, boolean[] vis, boolean chars) { setupPassable(); if (ch.flying || ch.buff( Amok.class ) != null) { @@ -821,19 +821,21 @@ public class Dungeon { BArray.and( pass, Dungeon.level.openSpace, passable ); } - for (Char c : Actor.chars()) { - if (visible[c.pos]) { - passable[c.pos] = false; + if (chars) { + for (Char c : Actor.chars()) { + if (vis[c.pos]) { + passable[c.pos] = false; + } } } - return PathFinder.find( from, to, passable ); + return PathFinder.find( ch.pos, to, passable ); } - public static int findStep(Char ch, int from, int to, boolean pass[], boolean[] visible ) { + public static int findStep(Char ch, int to, boolean[] pass, boolean[] visible, boolean chars ) { - if (Dungeon.level.adjacent( from, to )) { + if (Dungeon.level.adjacent( ch.pos, to )) { return Actor.findChar( to ) == null && (pass[to] || Dungeon.level.avoid[to]) ? to : -1; } @@ -847,18 +849,20 @@ public class Dungeon { if (Char.hasProp(ch, Char.Property.LARGE)){ BArray.and( pass, Dungeon.level.openSpace, passable ); } - - for (Char c : Actor.chars()) { - if (visible[c.pos]) { - passable[c.pos] = false; + + if (chars){ + for (Char c : Actor.chars()) { + if (visible[c.pos]) { + passable[c.pos] = false; + } } } - return PathFinder.getStep( from, to, passable ); + return PathFinder.getStep( ch.pos, to, passable ); } - public static int flee( Char ch, int cur, int from, boolean pass[], boolean[] visible ) { + public static int flee( Char ch, int from, boolean[] pass, boolean[] visible, boolean chars ) { setupPassable(); if (ch.flying) { @@ -870,15 +874,17 @@ public class Dungeon { if (Char.hasProp(ch, Char.Property.LARGE)){ BArray.and( pass, Dungeon.level.openSpace, passable ); } - - for (Char c : Actor.chars()) { - if (visible[c.pos]) { - passable[c.pos] = false; + + if (chars) { + for (Char c : Actor.chars()) { + if (visible[c.pos]) { + passable[c.pos] = false; + } } } - passable[cur] = true; + passable[ch.pos] = true; - return PathFinder.getStepBack( cur, from, passable ); + return PathFinder.getStepBack( ch.pos, from, passable ); } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java index d7608d7ab..efb9b4fd5 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java @@ -1156,15 +1156,8 @@ public class Hero extends Char { else if (path.getLast() != target) newPath = true; else { - //looks ahead for path validity, up to length-1 or 2. - //Note that this is shorter than for mobs, so that mobs usually yield to the hero - int lookAhead = (int) GameMath.gate(0, path.size()-1, 2); - for (int i = 0; i < lookAhead; i++){ - int cell = path.get(i); - if (!Dungeon.level.passable[cell] || (fieldOfView[cell] && Actor.findChar(cell) != null)) { - newPath = true; - break; - } + if (!Dungeon.level.passable[path.get(0)] || Actor.findChar(path.get(0)) != null) { + newPath = true; } } @@ -1179,7 +1172,12 @@ public class Hero extends Char { passable[i] = p[i] && (v[i] || m[i]); } - path = Dungeon.findPath(this, pos, target, passable, fieldOfView); + PathFinder.Path newpath = Dungeon.findPath(this, target, passable, fieldOfView, true); + if (newpath != null && path != null && newpath.size() > 2*path.size()){ + path = null; + } else { + path = newpath; + } } if (path == null) return false; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java index 3bd0a71bb..0539f498d 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java @@ -49,7 +49,6 @@ public class Ghoul extends Mob { SLEEPING = new Sleeping(); WANDERING = new Wandering(); - HUNTING = new Hunting(); state = SLEEPING; properties.add(Property.UNDEAD); @@ -203,54 +202,6 @@ public class Ghoul extends Mob { } } - //TODO currently very similar to super.Hunting and is largely a stop-gap, need to refactor - private class Hunting extends Mob.Hunting { - - @Override - public boolean act( boolean enemyInFOV, boolean justAlerted ) { - enemySeen = enemyInFOV; - if (enemyInFOV && !isCharmedBy( enemy ) && canAttack( enemy )) { - - return doAttack( enemy ); - - } else { - - if (enemyInFOV) { - target = enemy.pos; - } else if (enemy == null) { - state = WANDERING; - target = Dungeon.level.randomDestination( Ghoul.this ); - return true; - } - - int oldPos = pos; - if (target != -1 && getCloser( target )) { - - spend( 1 / speed() ); - return moveSprite( oldPos, pos ); - - } else { - - Ghoul partner = (Ghoul) Actor.findById( partnerID ); - if (!enemyInFOV) { - spend( TICK ); - sprite.showLost(); - state = WANDERING; - target = Dungeon.level.randomDestination( Ghoul.this ); - - //try to move closer to partner if they can't move to hero - } else if (partner != null && getCloser(partner.pos)) { - spend( 1 / speed() ); - return moveSprite( oldPos, pos ); - } else { - spend( TICK ); - } - return true; - } - } - } - } - public static class GhoulLifeLink extends Buff{ private Ghoul ghoul; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java index 9f897e3e0..d145d469a 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java @@ -377,21 +377,21 @@ public abstract class Mob extends Char { //shorten for a closer one if (Dungeon.level.adjacent(target, pos)) { path.add(target); - //extend the path for a further target + //extend the path for a further target } else { path.add(last); path.add(target); } - } else if (!path.isEmpty()) { + } else { //if the new target is simply 1 earlier in the path shorten the path if (path.getLast() == target) { - //if the new target is closer/same, need to modify end of path + //if the new target is closer/same, need to modify end of path } else if (Dungeon.level.adjacent(target, path.getLast())) { path.add(target); - //if the new target is further away, need to extend the path + //if the new target is further away, need to extend the path } else { path.add(last); path.add(target); @@ -404,39 +404,65 @@ public abstract class Mob extends Char { } - + //checks if the next cell along the current path can be stepped into if (!newPath) { - //looks ahead for path validity, up to length-1 or 4, but always at least 1. - int lookAhead = (int)GameMath.gate(1, path.size()-1, 4); - for (int i = 0; i < lookAhead; i++) { - int cell = path.get(i); - if (!Dungeon.level.passable[cell] - || (!flying && Dungeon.level.avoid[target]) - || (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[cell]) - || (fieldOfView[cell] && Actor.findChar(cell) != null)) { - newPath = true; - break; + int nextCell = path.removeFirst(); + if (!Dungeon.level.passable[nextCell] + || (!flying && Dungeon.level.avoid[nextCell]) + || (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[nextCell]) + || Actor.findChar(nextCell) != null) { + + newPath = true; + //If the next cell on the path can't be moved into, see if there is another cell that could replace it + if (!path.isEmpty()) { + for (int i : PathFinder.NEIGHBOURS8) { + if (Dungeon.level.adjacent(pos, nextCell + i) && Dungeon.level.adjacent(nextCell + i, path.getFirst())) { + if (Dungeon.level.passable[nextCell+i] + && (flying || !Dungeon.level.avoid[nextCell+i]) + && (!Char.hasProp(this, Char.Property.LARGE) || Dungeon.level.openSpace[nextCell+i]) + && Actor.findChar(nextCell+i) == null){ + path.addFirst(nextCell+i); + newPath = false; + break; + } + } + } + } + } else { + path.addFirst(nextCell); + } + } + + //generate a new path + if (newPath) { + //If we aren't hunting, always take a full path + PathFinder.Path full = Dungeon.findPath(this, target, Dungeon.level.passable, fieldOfView, true); + if (state != HUNTING){ + path = full; + } else { + //otherwise, check if other characters are forcing us to take a very slow route + // and don't try to go around them yet in response, basically assume their blockage is temporary + PathFinder.Path ignoreChars = Dungeon.findPath(this, target, Dungeon.level.passable, fieldOfView, false); + if (full == null || full.size() > 2*ignoreChars.size()){ + //check if first cell of shorter path is valid. If it is, use new shorter path. Otherwise do nothing and wait. + path = ignoreChars; + if (!Dungeon.level.passable[ignoreChars.getFirst()] + || (!flying && Dungeon.level.avoid[ignoreChars.getFirst()]) + || (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[ignoreChars.getFirst()]) + || Actor.findChar(ignoreChars.getFirst()) != null) { + return false; + } + } else { + path = full; } } } - if (newPath) { - path = Dungeon.findPath(this, pos, target, - Dungeon.level.passable, - fieldOfView); - } - - //if hunting something, don't follow a path that is extremely inefficient - //FIXME this is fairly brittle, primarily it assumes that hunting mobs can't see through - // permanent terrain, such that if their path is inefficient it's always because - // of a temporary blockage, and therefore waiting for it to clear is the best option. - if (path == null || - (state == HUNTING && path.size() > Math.max(9, 2*Dungeon.level.distance(pos, target)))) { - // + if (path != null) { + step = path.removeFirst(); + } else { return false; } - - step = path.removeFirst(); } if (step != -1) { move( step ); @@ -451,9 +477,7 @@ public abstract class Mob extends Char { return false; } - int step = Dungeon.flee( this, pos, target, - Dungeon.level.passable, - fieldOfView ); + int step = Dungeon.flee( this, target, Dungeon.level.passable, fieldOfView, true ); if (step != -1) { move( step ); return true; @@ -845,6 +869,17 @@ public abstract class Mob extends Char { return moveSprite( oldPos, pos ); } else { + + //if moving towards an enemy isn't possible, try to switch targets to another enemy that is closer + Char oldEnemy = enemy; + enemy = null; + enemy = chooseEnemy(); + if (enemy != null && enemy != oldEnemy){ + return act(enemyInFOV, justAlerted); + } else { + enemy = oldEnemy; + } + spend( TICK ); if (!enemyInFOV) { sprite.showLost(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java index e48500277..609d4f4de 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java @@ -170,13 +170,13 @@ public class NewDM300 extends Mob { if (Dungeon.level.adjacent(pos, Dungeon.hero.pos)){ canReach = true; } else { - canReach = (Dungeon.findStep(this, pos, Dungeon.hero.pos, Dungeon.level.openSpace, fieldOfView) != -1); + canReach = (Dungeon.findStep(this, Dungeon.hero.pos, Dungeon.level.openSpace, fieldOfView, true) != -1); } } else { if (Dungeon.level.adjacent(pos, enemy.pos)){ canReach = true; } else { - canReach = (Dungeon.findStep(this, pos, enemy.pos, Dungeon.level.openSpace, fieldOfView) != -1); + canReach = (Dungeon.findStep(this, enemy.pos, Dungeon.level.openSpace, fieldOfView, true) != -1); } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Piranha.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Piranha.java index 7e4e19e96..803b7bc94 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Piranha.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Piranha.java @@ -121,9 +121,7 @@ public class Piranha extends Mob { return false; } - int step = Dungeon.findStep( this, pos, target, - Dungeon.level.water, - fieldOfView ); + int step = Dungeon.findStep( this, target, Dungeon.level.water, fieldOfView, true ); if (step != -1) { move( step ); return true; @@ -134,9 +132,7 @@ public class Piranha extends Mob { @Override protected boolean getFurther( int target ) { - int step = Dungeon.flee( this, pos, target, - Dungeon.level.water, - fieldOfView ); + int step = Dungeon.flee( this, target, Dungeon.level.water, fieldOfView, true ); if (step != -1) { move( step ); return true;