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);
}
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 );
}
if (chars) {
for (Char c : Actor.chars()) {
if (visible[c.pos]) {
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;
}
@ -848,17 +850,19 @@ public class Dungeon {
BArray.and( pass, Dungeon.level.openSpace, passable );
}
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) {
@ -871,14 +875,16 @@ public class Dungeon {
BArray.and( pass, Dungeon.level.openSpace, passable );
}
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 );
}

View File

@ -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)) {
if (!Dungeon.level.passable[path.get(0)] || Actor.findChar(path.get(0)) != null) {
newPath = true;
break;
}
}
}
@ -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;

View File

@ -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;

View File

@ -383,7 +383,7 @@ public abstract class Mob extends Char {
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) {
@ -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)) {
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;
}
}
}
if (newPath) {
path = Dungeon.findPath(this, pos, target,
Dungeon.level.passable,
fieldOfView);
}
} else {
path.addFirst(nextCell);
}
}
//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)))) {
//
//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 (path != null) {
step = path.removeFirst();
} else {
return false;
}
}
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();

View File

@ -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);
}
}

View File

@ -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;