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
This commit is contained in:
Evan Debenham 2020-04-03 18:26:01 -04:00
parent 5206141aeb
commit 0653d0d1f2
6 changed files with 105 additions and 119 deletions

View File

@ -808,7 +808,7 @@ public class Dungeon {
BArray.setFalse(passable); 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(); setupPassable();
if (ch.flying || ch.buff( Amok.class ) != null) { if (ch.flying || ch.buff( Amok.class ) != null) {
@ -821,19 +821,21 @@ public class Dungeon {
BArray.and( pass, Dungeon.level.openSpace, passable ); BArray.and( pass, Dungeon.level.openSpace, passable );
} }
for (Char c : Actor.chars()) { if (chars) {
if (visible[c.pos]) { for (Char c : Actor.chars()) {
passable[c.pos] = false; 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; 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)){ if (Char.hasProp(ch, Char.Property.LARGE)){
BArray.and( pass, Dungeon.level.openSpace, passable ); BArray.and( pass, Dungeon.level.openSpace, passable );
} }
for (Char c : Actor.chars()) { if (chars){
if (visible[c.pos]) { for (Char c : Actor.chars()) {
passable[c.pos] = false; 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(); setupPassable();
if (ch.flying) { if (ch.flying) {
@ -870,15 +874,17 @@ public class Dungeon {
if (Char.hasProp(ch, Char.Property.LARGE)){ if (Char.hasProp(ch, Char.Property.LARGE)){
BArray.and( pass, Dungeon.level.openSpace, passable ); BArray.and( pass, Dungeon.level.openSpace, passable );
} }
for (Char c : Actor.chars()) { if (chars) {
if (visible[c.pos]) { for (Char c : Actor.chars()) {
passable[c.pos] = false; 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 );
} }

View File

@ -1156,15 +1156,8 @@ public class Hero extends Char {
else if (path.getLast() != target) else if (path.getLast() != target)
newPath = true; newPath = true;
else { else {
//looks ahead for path validity, up to length-1 or 2. if (!Dungeon.level.passable[path.get(0)] || Actor.findChar(path.get(0)) != null) {
//Note that this is shorter than for mobs, so that mobs usually yield to the hero newPath = true;
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;
}
} }
} }
@ -1179,7 +1172,12 @@ public class Hero extends Char {
passable[i] = p[i] && (v[i] || m[i]); 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; if (path == null) return false;

View File

@ -49,7 +49,6 @@ public class Ghoul extends Mob {
SLEEPING = new Sleeping(); SLEEPING = new Sleeping();
WANDERING = new Wandering(); WANDERING = new Wandering();
HUNTING = new Hunting();
state = SLEEPING; state = SLEEPING;
properties.add(Property.UNDEAD); 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{ public static class GhoulLifeLink extends Buff{
private Ghoul ghoul; private Ghoul ghoul;

View File

@ -377,21 +377,21 @@ public abstract class Mob extends Char {
//shorten for a closer one //shorten for a closer one
if (Dungeon.level.adjacent(target, pos)) { if (Dungeon.level.adjacent(target, pos)) {
path.add(target); path.add(target);
//extend the path for a further target //extend the path for a further target
} else { } else {
path.add(last); path.add(last);
path.add(target); path.add(target);
} }
} else if (!path.isEmpty()) { } else {
//if the new target is simply 1 earlier in the path shorten the path //if the new target is simply 1 earlier in the path shorten the path
if (path.getLast() == target) { 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())) { } else if (Dungeon.level.adjacent(target, path.getLast())) {
path.add(target); 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 { } else {
path.add(last); path.add(last);
path.add(target); 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) { if (!newPath) {
//looks ahead for path validity, up to length-1 or 4, but always at least 1. int nextCell = path.removeFirst();
int lookAhead = (int)GameMath.gate(1, path.size()-1, 4); if (!Dungeon.level.passable[nextCell]
for (int i = 0; i < lookAhead; i++) { || (!flying && Dungeon.level.avoid[nextCell])
int cell = path.get(i); || (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[nextCell])
if (!Dungeon.level.passable[cell] || Actor.findChar(nextCell) != null) {
|| (!flying && Dungeon.level.avoid[target])
|| (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[cell]) newPath = true;
|| (fieldOfView[cell] && Actor.findChar(cell) != null)) { //If the next cell on the path can't be moved into, see if there is another cell that could replace it
newPath = true; if (!path.isEmpty()) {
break; 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) { if (path != null) {
path = Dungeon.findPath(this, pos, target, step = path.removeFirst();
Dungeon.level.passable, } else {
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)))) {
//
return false; return false;
} }
step = path.removeFirst();
} }
if (step != -1) { if (step != -1) {
move( step ); move( step );
@ -451,9 +477,7 @@ public abstract class Mob extends Char {
return false; return false;
} }
int step = Dungeon.flee( this, pos, target, int step = Dungeon.flee( this, target, Dungeon.level.passable, fieldOfView, true );
Dungeon.level.passable,
fieldOfView );
if (step != -1) { if (step != -1) {
move( step ); move( step );
return true; return true;
@ -845,6 +869,17 @@ public abstract class Mob extends Char {
return moveSprite( oldPos, pos ); return moveSprite( oldPos, pos );
} else { } 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 ); spend( TICK );
if (!enemyInFOV) { if (!enemyInFOV) {
sprite.showLost(); sprite.showLost();

View File

@ -170,13 +170,13 @@ public class NewDM300 extends Mob {
if (Dungeon.level.adjacent(pos, Dungeon.hero.pos)){ if (Dungeon.level.adjacent(pos, Dungeon.hero.pos)){
canReach = true; canReach = true;
} else { } 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 { } else {
if (Dungeon.level.adjacent(pos, enemy.pos)){ if (Dungeon.level.adjacent(pos, enemy.pos)){
canReach = true; canReach = true;
} else { } 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);
} }
} }

View File

@ -121,9 +121,7 @@ public class Piranha extends Mob {
return false; return false;
} }
int step = Dungeon.findStep( this, pos, target, int step = Dungeon.findStep( this, target, Dungeon.level.water, fieldOfView, true );
Dungeon.level.water,
fieldOfView );
if (step != -1) { if (step != -1) {
move( step ); move( step );
return true; return true;
@ -134,9 +132,7 @@ public class Piranha extends Mob {
@Override @Override
protected boolean getFurther( int target ) { protected boolean getFurther( int target ) {
int step = Dungeon.flee( this, pos, target, int step = Dungeon.flee( this, target, Dungeon.level.water, fieldOfView, true );
Dungeon.level.water,
fieldOfView );
if (step != -1) { if (step != -1) {
move( step ); move( step );
return true; return true;