package onlinefrontlines.game.ai;
import java.awt.Point;
import java.util.*;
import org.apache.log4j.Logger;
import onlinefrontlines.Constants;
import onlinefrontlines.game.*;
import onlinefrontlines.game.actions.*;
import onlinefrontlines.utils.*;
/**
* This thread wakes up every X seconds to check if there are games that need a move by the AI
*
* @author jorrit
*
* Copyright (C) 2009-2013 Jorrit Rouwe
*
* This file is part of Online Frontlines.
*
* Online Frontlines is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Online Frontlines is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Online Frontlines. If not, see <http://www.gnu.org/licenses/>.
*/
public class GameAIThread extends Thread
{
private static final Logger log = Logger.getLogger(GameAIThread.class);
/**
* Constructor
*/
public GameAIThread()
{
super("GameAIThread");
}
/**
* Run the thread
*/
public void run()
{
log.info("Thread started");
for (;;)
{
try
{
// Loop through all games that currently need interaction by AI
List<Integer> games = GameStateDAO.getGamesIdsToContinue(Constants.USER_ID_AI);
for (Integer g : games)
{
try
{
// Get the game
GameState gameState = GameStateCache.getInstance().get(g);
if (gameState == null)
throw new Exception("No game found");
// Prevent concurrent access
synchronized (gameState)
{
// Process the game
processGame(gameState);
}
}
catch (Exception e)
{
// Log error, next game
Tools.logException(e);
}
}
}
catch (Exception e)
{
// Log error, retry
Tools.logException(e);
}
try
{
// Wait for next cycle
synchronized (this)
{
wait(5 * 1000);
}
}
catch (InterruptedException e)
{
// Normal termination
break;
}
}
log.info("Thread stopped");
}
/**
* Process a single game
*/
private void processGame(GameState gameState) throws Exception
{
// Determine AI faction
Faction aiFaction = gameState.getPlayerId(Faction.f1) == Constants.USER_ID_AI? Faction.f1 : Faction.f2;
Map<UnitClass, AttackGrid> grids = new TreeMap<UnitClass, AttackGrid>();
// Process all units
ArrayList<UnitState> units = new ArrayList<UnitState>(gameState.getUnits());
while (!units.isEmpty() && gameState.winningFaction == Faction.invalid)
{
UnitState u = units.get(0);
// We only need to process our own units
if (u.faction != aiFaction)
{
units.remove(u);
continue;
}
// Make unit move
if (!processUnit(gameState, u, grids))
{
units.remove(u);
continue;
}
}
// End the turn
if (gameState.winningFaction == Faction.invalid)
gameState.execute(new ActionEndTurn(), true);
}
private static class AttackGrid
{
public HexagonGridImpl<Double> directAttack;
public HexagonGridImpl<Double> indirectAttack;
public double getWeighted(Point p, double directAttackWeight)
{
return directAttackWeight * directAttack.get(p) + (1.0 - directAttackWeight) * indirectAttack.get(p);
}
}
private AttackGrid getEnemyAttackPower(GameState gameState, UnitClass unitClass, Faction unitFaction) throws Exception
{
//String debugId = "class: " + unitClass.toString() + " faction: " + unitFaction.toString();
final MapConfig mapConfig = gameState.mapConfig;
// Create grid
AttackGrid grid = new AttackGrid();
grid.directAttack = new HexagonGridImpl<Double>(mapConfig.sizeX, mapConfig.sizeY, 0.0);
grid.indirectAttack = new HexagonGridImpl<Double>(mapConfig.sizeX, mapConfig.sizeY, 0.0);
// Loop through all units that are able to attack
for (final UnitState enemyUnit : gameState.getUnits())
if (enemyUnit.canAttackUnitClass(unitClass, unitFaction))
{
/*
String enemyDebugId = debugId + " enemy: " + enemyUnit.id + " (" + enemyUnit.unitConfig.name + ")";
// Identify units
ArrayList<ActionAnnotateTiles.Tile> tiles = new ArrayList<ActionAnnotateTiles.Tile>();
tiles.add(new ActionAnnotateTiles.Tile(enemyUnit.locationX, enemyUnit.locationY, Integer.toString(enemyUnit.id)));
gameState.execute(new ActionAnnotateTiles("EnemyUnit " + enemyDebugId, tiles), false);
*/
// Get range of enemy unit
int range = enemyUnit.unitConfig.getStrengthProperties(unitClass).attackRange;
// Calculate direct attack
List<Point> directAttack = grid.directAttack.extendBy(enemyUnit.getLocation(), range);
TerrainConfig attackerTerrain = mapConfig.getTerrainAt(enemyUnit.locationX, enemyUnit.locationY);
for (Point p : directAttack)
{
// Get defender terrain
TerrainConfig defenderTerrain = mapConfig.getTerrainAt(p.x, p.y);
// Add average loss
double avgLoss = enemyUnit.getAverageArmourLossForAttack(unitClass, attackerTerrain, defenderTerrain);
grid.directAttack.set(p, grid.directAttack.get(p) + enemyUnit.unitConfig.actions * avgLoss);
}
/*
// Debug output
gameState.execute(new ActionAnnotateTiles("EnemyDirectAttack " + enemyDebugId, ActionAnnotateTiles.createTiles(directAttack, "D")), false);
gameState.execute(new ActionAnnotateTiles("EnemyDirectAttackGrid " + enemyDebugId, ActionAnnotateTiles.createTiles(grid.directAttack)), false);
*/
// Calculate movement for unit
List<Point> movementRange = grid.indirectAttack.getReachableArea(enemyUnit.getLocation(),
new HexagonGrid.CostFunction()
{
public float getCost(HexagonGrid.AStarNode from, Point to)
{
TerrainConfig terrain = mapConfig.getTerrainAt(to.x, to.y);
return enemyUnit.unitConfig.getMovementCost(terrain);
}
},
enemyUnit.unitConfig.movementPoints);
// If we can move
if (movementRange.size() > 1)
{
// Calculate indirect attack
List<Point> indirectAttack = grid.indirectAttack.extendBy(movementRange, range);
for (Point p : indirectAttack)
{
// Get defender terrain
TerrainConfig defenderTerrain = mapConfig.getTerrainAt(p.x, p.y);
// Find best attacker terrain
attackerTerrain = null;
for (Point m : movementRange)
{
int d = HexagonGrid.getDistance(p, m);
if (d > 0 && d <= range)
{
TerrainConfig t = mapConfig.getTerrainAt(m.x, m.y);
if (attackerTerrain == null || t.strengthModifier > attackerTerrain.strengthModifier)
attackerTerrain = t;
}
}
// Add average loss
double avgLoss = enemyUnit.getAverageArmourLossForAttack(unitClass, attackerTerrain, defenderTerrain);
grid.indirectAttack.set(p, grid.indirectAttack.get(p) + (enemyUnit.unitConfig.actions - 1) * avgLoss);
}
}
/*
// Debug output
gameState.execute(new ActionAnnotateTiles("EnemyMovement " + enemyDebugId, ActionAnnotateTiles.createTiles(movementRange, "M")), false);
gameState.execute(new ActionAnnotateTiles("EnemyIndirectAttack " + enemyDebugId, ActionAnnotateTiles.createTiles(indirectAttack, "I")), false);
gameState.execute(new ActionAnnotateTiles("EnemyIndirectAttackGrid " + enemyDebugId, ActionAnnotateTiles.createTiles(grid.indirectAttack)), false);
*/
}
/*
// Debug output
gameState.execute(new ActionAnnotateTiles("EnemyDirectAttackGrid " + debugId, ActionAnnotateTiles.createTiles(grid.directAttack)), false);
gameState.execute(new ActionAnnotateTiles("EnemyIndirectAttackGrid " + debugId, ActionAnnotateTiles.createTiles(grid.indirectAttack)), false);
*/
return grid;
}
private boolean checkAttack(GameState gameState, UnitState unitState, Map<UnitClass, AttackGrid> grids) throws Exception
{
// Check if we have an action left
if (unitState.actionsLeft < 1)
return false;
/*
// Debug output
ArrayList<ActionAnnotateTiles.Tile> hisTiles = new ArrayList<ActionAnnotateTiles.Tile>();
ArrayList<ActionAnnotateTiles.Tile> myTiles = new ArrayList<ActionAnnotateTiles.Tile>();
ArrayList<ActionAnnotateTiles.Tile> scoreTiles = new ArrayList<ActionAnnotateTiles.Tile>();
*/
// Check if unit can attack
double bestScore = 0;
UnitState bestUnit = null;
for (UnitState enemyUnit : gameState.getUnits())
if (unitState.canAttack(enemyUnit))
{
// Get terrain
TerrainConfig attackerTerrain = gameState.getAttackTerrain(unitState);
TerrainConfig defenderTerrain = gameState.getAttackTerrain(enemyUnit);
// Calculate on average how much armour enemy would lose
double hisAvgArmourLoss = unitState.getAverageArmourLossForAttack(enemyUnit.unitConfig.unitClass, attackerTerrain, defenderTerrain);
double hisArmour = Math.max(enemyUnit.armour - hisAvgArmourLoss, 0);
// Estimate on average how much armour I would lose in the counter attack
double myAvgArmourLoss = enemyUnit.canCounterAttack(unitState)? enemyUnit.getAverageArmourLossForAttack(unitState.unitConfig.unitClass, defenderTerrain, attackerTerrain) : 0;
myAvgArmourLoss *= hisArmour / enemyUnit.armour;
// Scale to victory points (also account for units contained in the unit)
double hisLoss = enemyUnit.getTotalVictoryPoints() * Math.min(hisAvgArmourLoss, enemyUnit.armour) / enemyUnit.armour;
double myLoss = unitState.getTotalVictoryPoints() * Math.min(myAvgArmourLoss, unitState.armour) / unitState.armour;
// Calculate score
double score = hisLoss - myLoss;
/*
// Debug output
hisTiles.add(new ActionAnnotateTiles.Tile(enemyUnit.getLocation(), hisLoss));
myTiles.add(new ActionAnnotateTiles.Tile(enemyUnit.getLocation(), myLoss));
scoreTiles.add(new ActionAnnotateTiles.Tile(enemyUnit.getLocation(), score));
*/
// Check if score is better
if (bestUnit == null || score > bestScore)
{
bestScore = score;
bestUnit = enemyUnit;
}
}
if (bestUnit == null)
return false;
/*
// Debug output
gameState.execute(new ActionAnnotateTiles("CheckAttack hisLoss: " + unitState.id, hisTiles), false);
gameState.execute(new ActionAnnotateTiles("CheckAttack myLoss: " + unitState.id, myTiles), false);
gameState.execute(new ActionAnnotateTiles("CheckAttack score: " + unitState.id, scoreTiles), false);
*/
// Perform attack
gameState.execute(new ActionAttackUnit(unitState, bestUnit), true);
// Grid is now invalid
grids.remove(bestUnit.unitConfig.unitClass);
return true;
}
private boolean followPath(GameState gameState, UnitState unitState, ArrayList<Point> path) throws Exception
{
final MapConfig mapConfig = gameState.mapConfig;
int max = 1;
// Get part of the path that we have movement points for
int movementLeft = unitState.movementPointsLeft;
for (int i = 1; i < path.size(); ++i)
{
Point p = path.get(i);
TerrainConfig terrain = mapConfig.getTerrainAt(p.x, p.y);
movementLeft -= unitState.unitConfig.getMovementCost(terrain);
if (movementLeft >= 0)
max++;
else
break;
}
// Get part of the path that won't result in us getting in collision
for (int i = max - 1; i > 0; --i)
{
Point p = path.get(i);
if (gameState.getUnit(p.x, p.y) != null)
--max;
else
break;
}
// Remove extra elements
while (path.size() > max)
path.remove(path.size() - 1);
if (path.size() < 2)
return false;
// Make unit move
gameState.execute(new ActionMoveUnit(unitState, path), true);
return true;
}
private boolean checkMoveThenAttack(GameState gameState, final UnitState unitState, AttackGrid grid) throws Exception
{
// Check if still able to move and attack
if (unitState.actionsLeft < 2)
return false;
// Check if unit can move at all
final int minCost = unitState.unitConfig.getMinimumMovementCost();
if (minCost > unitState.movementPointsLeft)
return false;
final MapConfig mapConfig = gameState.mapConfig;
/*
// Debug output
ArrayList<ActionAnnotateTiles.Tile> hisTiles = new ArrayList<ActionAnnotateTiles.Tile>();
ArrayList<ActionAnnotateTiles.Tile> myTiles = new ArrayList<ActionAnnotateTiles.Tile>();
ArrayList<ActionAnnotateTiles.Tile> scoreTiles = new ArrayList<ActionAnnotateTiles.Tile>();
*/
// Calculate movement for unit
List<Point> movementRange = mapConfig.getReachableArea(unitState.getLocation(),
new HexagonGrid.CostFunction()
{
public float getCost(HexagonGrid.AStarNode from, Point to)
{
TerrainConfig terrain = mapConfig.getTerrainAt(to.x, to.y);
return unitState.unitConfig.getMovementCost(terrain);
}
},
unitState.movementPointsLeft);
// Loop through all enemy units that this unit can attack
Point bestPoint = null;
double bestScore = 0;
for (UnitState enemyUnit : gameState.getUnits())
if (unitState.canAttackFrom(0, enemyUnit))
{
// Get range of unit
int range = unitState.unitConfig.getStrengthProperties(enemyUnit.unitConfig.unitClass).attackRange;
// Evaluate from all locations
Point bestUnitPoint = null;
//double bestUnitHisLoss = 0;
//double bestUnitMyLoss = 0;
double bestUnitScore = 0;
for (Point p : movementRange)
if (gameState.getUnit(p.x, p.y) == null
&& enemyUnit.getDistanceTo(p.x, p.y) <= range)
{
TerrainConfig attackerTerrain = gameState.getAttackTerrain(unitState, p.x, p.y);
TerrainConfig defenderTerrain = gameState.getAttackTerrain(enemyUnit);
// Calculate on average how much armour enemy would lose
double hisAvgArmourLoss = unitState.getAverageArmourLossForAttack(enemyUnit.unitConfig.unitClass, attackerTerrain, defenderTerrain);
// Scale to victory points (also account for units contained in the unit)
double hisLoss = enemyUnit.getTotalVictoryPoints() * Math.min(hisAvgArmourLoss, enemyUnit.armour) / enemyUnit.armour;
double myLoss = unitState.getTotalVictoryPoints() * Math.min(grid.getWeighted(p, 0.66) / unitState.armour, 2.0);
// Calculate score
double score = hisLoss - myLoss;
// Check if score is better
if (bestUnitPoint == null || score > bestUnitScore)
{
bestUnitScore = score;
//bestUnitHisLoss = hisLoss;
//bestUnitMyLoss = myLoss;
bestUnitPoint = p;
}
}
if (bestUnitPoint != null)
{
/*
// Debug output
hisTiles.add(new ActionAnnotateTiles.Tile(enemyUnit.getLocation(), bestUnitHisLoss));
myTiles.add(new ActionAnnotateTiles.Tile(enemyUnit.getLocation(), bestUnitMyLoss));
scoreTiles.add(new ActionAnnotateTiles.Tile(enemyUnit.getLocation(), bestUnitScore));
*/
// Check if score is better
if (bestPoint == null || bestUnitScore > bestScore)
{
bestScore = bestUnitScore;
bestPoint = bestUnitPoint;
}
}
}
if (bestPoint == null)
return false;
/*
// Debug output
gameState.execute(new ActionAnnotateTiles("MoveThenAttack hisLoss: " + unitState.id, hisTiles), false);
gameState.execute(new ActionAnnotateTiles("MoveThenAttack myLoss: " + unitState.id, myTiles), false);
gameState.execute(new ActionAnnotateTiles("MoveThenAttack score: " + unitState.id, scoreTiles), false);
*/
// Plan shortest path
HexagonGrid.PlanResult result = mapConfig.planPath(unitState.getLocation(), bestPoint,
new HexagonGrid.CostFunction()
{
public float getCost(HexagonGrid.AStarNode from, Point to)
{
TerrainConfig terrain = mapConfig.getTerrainAt(to.x, to.y);
return unitState.unitConfig.getMovementCost(terrain);
}
},
new HexagonGrid.CostEstimator()
{
public float getEstimatedCost(Point from, Point goal)
{
return MapConfig.getDistance(from, goal) * minCost;
}
});
if (result == null)
return false;
// Move unit into place
return followPath(gameState, unitState, result.path);
}
private boolean checkMoveTowardsEnemy(final GameState gameState, final UnitState unitState, AttackGrid grid) throws Exception
{
// Only do this when we have all our actions left
if (unitState.actionsLeft != unitState.unitConfig.actions)
return false;
// Check if unit can move
final int minCost = unitState.unitConfig.getMinimumMovementCost();
if (minCost > unitState.movementPointsLeft)
return false;
final MapConfig mapConfig = gameState.mapConfig;
// Loop through all enemy units that this unit can attack
HexagonGrid.PlanResult best = null;
for (UnitState enemyUnit : gameState.getUnits())
if (unitState.canAttackFrom(0, enemyUnit))
{
// Plan shortest path
HexagonGrid.PlanResult result = mapConfig.planPath(unitState.getLocation(), enemyUnit.getLocation(),
new HexagonGrid.CostFunction()
{
public float getCost(HexagonGrid.AStarNode from, Point to)
{
TerrainConfig terrain = mapConfig.getTerrainAt(to.x, to.y);
float cost = unitState.unitConfig.getMovementCost(terrain);
// If we've just expended all our movement points end ended up in another unit, add a bit to the cost
if (from.costFromStart <= unitState.movementPointsLeft
&& from.costFromStart + cost > unitState.movementPointsLeft)
{
UnitState unit = gameState.getUnit(from.point.x, from.point.y);
if (unit != null)
return cost + 2 * minCost;
}
return cost;
}
},
new HexagonGrid.CostEstimator()
{
public float getEstimatedCost(Point from, Point goal)
{
return MapConfig.getDistance(from, goal) * minCost;
}
});
// Store best so far
if (result != null && (best == null || best.cost > result.cost))
best = result;
}
if (best == null)
return false;
// Check if we can be attacked
boolean isUnderAttack = grid.directAttack.get(unitState.getLocation()) > 0;
if (!isUnderAttack)
{
// Get top speed of nearby units
int otherMax = unitState.unitConfig.getMaxMovement();
for (UnitState friendlyUnit : gameState.getUnits())
if (friendlyUnit != unitState
&& friendlyUnit.faction == unitState.faction
&& friendlyUnit.container == null
&& friendlyUnit.getDistanceTo(unitState.locationX, unitState.locationY) < 4
&& friendlyUnit.unitConfig.getMaxMovement() > 0
&& grid.directAttack.get(friendlyUnit.getLocation()) == 0)
otherMax = Math.min(otherMax, friendlyUnit.unitConfig.getMaxMovement());
// Limit our speed
while (best.path.size() > otherMax + 1)
best.path.remove(best.path.size() - 1);
}
return followPath(gameState, unitState, best.path);
}
private boolean processUnit(final GameState gameState, final UnitState unitState, Map<UnitClass, AttackGrid> grids) throws Exception
{
// We only process units that have actions left
if (unitState.actionsLeft <= 0)
return false;
// Cannot do anything with dead units
if (unitState.armour <= 0)
return false;
// Don't do anything with contained units (yet)
if (unitState.container != null)
return false;
// Check if unit can attack
if (checkAttack(gameState, unitState, grids))
return true;
// Get enemy influence map
UnitClass unitClass = unitState.unitConfig.unitClass;
AttackGrid grid = grids.get(unitClass);
if (grid == null)
{
grid = getEnemyAttackPower(gameState, unitClass, unitState.faction);
grids.put(unitClass, grid);
}
// Check if we can move to attack
if (checkMoveThenAttack(gameState, unitState, grid))
return true;
// Move in general direction of enemy
if (checkMoveTowardsEnemy(gameState, unitState, grid))
return true;
return false;
}
}