patternjavaMinor
Teaching an Old AI New Magic Tricks
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:
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
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.