HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Teaching an Old AI New Magic Tricks

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
newmagicteachingoldtricks

Problem

Recently I wanted to teach the AI of my castle game how to use the spells that I have added to the game. You can try out the game here: Castleparts

Initially I thought that I would have to add subclasses for each of the four playable races in order to account for the different spells that each one has available. However I ended up coming up with a more generic approach that works just as well.

I'm using the gdx-ai library for the AI. Initially (before spells added) the core AI logic looked like this:

@Override
public void update(AIPlayerConquer entity) {
    //this is the hierarchy of action priorities
    //when idle, this will be called to search for what state to select next

    //shoot ogres first because they will destroy walls
    entity.findPersons();
    if (entity.shouldAttackPersons()) {
        entity.stateMachine.changeState(SHOOT_PERSONS);
    }

    //shoot if the opponent is aggressive or controls a lot of the map
    else if (entity.isOpponentAggressive() ||
             entity.shouldAttackBasedOnOpponentPercentage() ||
             entity.shouldAttackBasedOnPercentOwned()) {

        entity.stateMachine.changeState(SHOOT);

    //otherwise try to build
    } else {
        entity.stateMachine.changeState(BUILD);
    }
}


With just some small changes, I added spells to the core logic. I wanted to put spells at a higher priority than regular actions because they make the gameplay more exciting. Here's the entire class:

```
public enum AIPlayerConquerState implements State {
IDLE,

SHOOT {
@Override
public void enter(AIPlayerConquer entity) {

entity.findOpponentWallTiles();

if (!entity.doesOpponentHaveWalls()) {
entity.stateMachine.changeState(BUILD);
} else {
entity.angryAsEnemy();
entity.tryToShootWalls();
}

entity.stateMachine.changeState(IDLE);
}
},

SHOOT_PERSONS {
@Override

Solution

AIPlayerConquerState is quite unusual - enums are not meant to hold "business" logic but it seems its enforced by used API.

General: prefer Collection over its specializations (List, Set) in public API; it gives you more flexibility for choosing/changing implementations (see below).

SpellType:
1) constants are missing in the post
2) collections will be much more efficient (and safer) when defined as:

public final static Collection defensiveSpells = Collections.unmodifiableSet(
        EnumSet.of( SHIELD, STATIC_CHARGE, DIG, SHROUD, BONUS_WALLS ) );
public final static Collection aggresiveSpells
        = Collections.unmodifiableSet( EnumSet.complementOf(defensiveSpells) );


Spells collections initialization could be then simplified as (removing both loops):

aggresiveSpells = EnumSet.copyOf( playerType.spells ).retainAll( SpellType.aggresiveSpells );
defensiveSpells = EnumSet.copyOf( playerType.spells ).retainAll( SpellType.defensiveSpells );


"some simple code" can be simplified (and speed up) further:

protected boolean shouldCastSpell(boolean offensive) {
    if (timeSinceSpellCast  availableSpells) {
    return availableSpells.stream().anyMatch( s -> hasEnergyForSpell(s) );
}


The final part can be simplified as well:

public void tryToCastOffensiveSpell() {
    List affordableSpells = aggresiveSpells.stream()
            .filter( s -> hasEnergyForSpell(s) )
            .collect( Collectors.toList() );
    if (!affordableSpells.isEmpty()) { tryToCastSpell(affordableSpells); }
}


Good luck with casting spells :)

Code Snippets

public final static Collection<SpellType> defensiveSpells = Collections.unmodifiableSet(
        EnumSet.of( SHIELD, STATIC_CHARGE, DIG, SHROUD, BONUS_WALLS ) );
public final static Collection<SpellType> aggresiveSpells
        = Collections.unmodifiableSet( EnumSet.complementOf(defensiveSpells) );
aggresiveSpells = EnumSet.copyOf( playerType.spells ).retainAll( SpellType.aggresiveSpells );
defensiveSpells = EnumSet.copyOf( playerType.spells ).retainAll( SpellType.defensiveSpells );
protected boolean shouldCastSpell(boolean offensive) {
    if (timeSinceSpellCast < difficulty.minSecondsBetweenSpells) {
        return false;
    }
    return offensive ? hasAffordableSpells(aggresiveSpells) : hasAffordableSpells(defensiveSpells);
}

protected boolean hasAffordableSpells(Collection<SpellType> availableSpells) {
    return availableSpells.stream().anyMatch( s -> hasEnergyForSpell(s) );
}
public void tryToCastOffensiveSpell() {
    List<SpellType> affordableSpells = aggresiveSpells.stream()
            .filter( s -> hasEnergyForSpell(s) )
            .collect( Collectors.toList() );
    if (!affordableSpells.isEmpty()) { tryToCastSpell(affordableSpells); }
}

Context

StackExchange Code Review Q#156220, answer score: 2

Revisions (0)

No revisions yet.