package game.players; import java.awt.Color; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import base.Graph; import base.Node; import game.AI; import game.Game; import game.Player; import game.map.Castle; import game.map.PathFinding; import gui.AttackThread; import gui.components.JokerPanel.JokerTypes; import gui.components.MapPanel.Action; public class StrongAI extends AI { // These values were determined mostly by trial-and-error private static double UTILITY_F1 = 5.0; // Number of adjacent friendly castles private static double UTILITY_F2 = 1.0; // Number of adjacent enemy castles private static double UTILITY_F3 = 3.0; // Number of connected edges private static double UTILITY_F4 = -10.0; // Is surrounded by opponents castles private static double UTILITY_F5 = 3.0; // Size of the castles batch private static double UTILITY_F6 = 4.0; // Castles missing to a full kingdom private static double REINFORCE_F1 = 1.0; // Number of adjacent enemy castles private static double REINFORCE_F2 = -5.0; // Is surrounded by opponents castles private static double REINFORCE_F3 = 0.5; // Castle utility private static double REINFORCE_F4 = -100.0; // Is border castle private static double REINFORCE_F5 = -2.0; // Number of troops private static double ATTACK_F1 = 1.0; // Castle utility private static double EVAL_OWN = 3.0; // Weight of own utilities private static double EVAL_OPP = -10.0; // Weight of opponents utilities private static int scare_threshold = 5; // Number of troops to use scare joker public StrongAI(String name, Color color) { super(name, color); } /** * Evaluates the current game state * @param game the current game * @param graph the graph to be evaluated * @param p the player * @param change the castle that should be considered to belong to p * @return the state value */ public static double evaluateState(Game game, Graph g, Player p, Optional change) { // Temporarily give castle "change" to p Player owner = null; if(change.isPresent()) { owner = change.get().getOwner(); change.get().setOwner(p); } // Calculate total state utility double value = 0; for(Player player : game.getPlayers()) { List castles = g.getAllValues().stream() .filter(x->x.getOwner() == player) .collect(Collectors.toList()); double s = (p == player)? EVAL_OWN : EVAL_OPP; for(Castle c : castles) { value += s * utilityCastle(g, player, c); } } // Hand "change" back to its previous owner if(change.isPresent()) { change.get().setOwner(owner); } return value; } /** /** * Analyzes the utility of a given castle to the player p * @param g the game graph * @param p the player * @param c the castle to be analyzed * @return the utility value */ public static double utilityCastle(Graph g, Player p, Castle c) { Node node = g.getNode(c); // Number of adjacent friendly castles double f1 = g.getEdges(node).stream() .filter(x->x.getOtherNode(node).getValue().getOwner() != null) .filter(x->x.getOtherNode(node).getValue().getOwner() == p) .count(); // Number of adjacent enemy castles double f2 = g.getEdges(node).stream() .filter(x->x.getOtherNode(node).getValue().getOwner() != null) .filter(x->x.getOtherNode(node).getValue().getOwner() != p) .count(); // Number of connected edges double f3 = g.getEdges(node).size() - 1; // Is surrounded by opponents castles double f4 = (g.getEdges(node).stream() .filter(x->x.getOtherNode(node).getValue().getOwner() != null) .filter(x->x.getOtherNode(node).getValue().getOwner() != p) .count() == g.getEdges(node).size() - 1)? 1.0 : 0; // Size of the castles batch double f5 = getBatch(g, p, c, new ArrayList<>()).size(); // Castles missing to a full kingdom double f6 = c.getKingdom().getCastles().stream() .filter(x->x.getOwner() == p).count() - c.getKingdom().getCastles().size() + 2; return UTILITY_F1 * f1 + UTILITY_F2 * f2 + UTILITY_F3 * f3 + UTILITY_F4 * f4 + f5 * UTILITY_F5 + f6 * UTILITY_F6; } /** * Analyzes the utility to attack a certain castle * @param g the graph * @param p the current player * @param c the castle * @return the reinforcement utility */ public static double utilityReinforce(Graph g, Player p, Castle c) { Node node = g.getNode(c); // Number of adjacent enemy castles double f1 = g.getEdges(node).stream() .filter(x->x.getOtherNode(node).getValue().getOwner() != null) .filter(x->x.getOtherNode(node).getValue().getOwner() != p) .count(); // Is surrounded by opponents castles double f2 = (g.getEdges(node).stream() .filter(x->x.getOtherNode(node).getValue().getOwner() != null) .filter(x->x.getOtherNode(node).getValue().getOwner() != p) .count() == g.getEdges(node).size() - 1)? 1.0 : 0; // Castle utility double f3 = utilityCastle(g, p, c); // Is border castle double f4 = isBorder(g, c)? 0.0 : 1.0; // Number of troops exceeding double f5 = c.getTroopCount(); return REINFORCE_F1 * f1 + REINFORCE_F2 * f2 + f3 * REINFORCE_F3 + f4 * REINFORCE_F4 + f5 * REINFORCE_F5; } /** * Analyzes the utility to attack a certain castle * @param g the graph * @param p the attacking player * @param a the attacking castle * @param t the target castle * @return */ public static double utilityAttack(Game game, Graph g, Player p, Castle a, Castle t) { double f1 = evaluateState(game, g, p, Optional.of(t)); double ratio = ((double)a.getTroopCount())/t.getTroopCount(); return ATTACK_F1 * f1 * ratio; } /** * Chooses the initial castles in round one * @param game the game * @throws InterruptedException */ private void chooseInitialCastles(Game game) throws InterruptedException { List availableCastles = game.getMap().getCastles().stream() .filter(c -> c.getOwner() == null) .collect(Collectors.toList()); while(availableCastles.size() > 0 && getRemainingTroops() > 0) { sleep(1000); // Find best castle Castle best = availableCastles.get(0); double bestScore = 0; double score = 0; for(Castle c : availableCastles) { score = evaluateState(game, game.getMap().getGraph(), this, Optional.of(c)); if(score > bestScore) { bestScore = score; best = c; } } availableCastles.remove(best); game.chooseCastle(best, this); } } /** * Distribute new troops to castles * @param game the game * @throws InterruptedException */ private void distributeTroops(Game game) throws InterruptedException { Graph g = game.getMap().getGraph(); List castles = game.getMap().getCastles().stream() .filter(x->x.getOwner() == this) .collect(Collectors.toList()); while(this.getRemainingTroops() > 0) { Castle best = castles.get(0); double bestScore = 0; for(Castle c : castles) { double score = utilityReinforce(g, this, c); if(score > bestScore) { bestScore = score; best = c; } } sleep(500); game.addTroops(this, best, 1); } } /** * Reinforces the castles with troops * @param game the game */ private void reinforceCastles(Game game) { Graph g = game.getMap().getGraph(); // The castles that can send troops List distributors = game.getMap().getCastles().stream() .filter(x->x.getOwner() == this) .filter(x->x.getTroopCount() > 1) .filter(x->isBorder(g, x) == false) .collect(Collectors.toList()); // The castles that can receive troops List receivers = game.getMap().getCastles().stream() .filter(x->x.getOwner() == this) .filter(x->isBorder(g, x) == true) .collect(Collectors.toList()); if(receivers.size() == 0 || distributors.size() == 0) return; for(Castle d : distributors) { while(d.getTroopCount() > 1) { double bestScore = 0; Castle best = receivers.get(0); // Do not cheat like evil BasicAI, // only move troops if there is a path PathFinding path = new PathFinding(g, d, Action.MOVING, this); for(Castle c : receivers) { double score = utilityReinforce(g, this, c); if(score > bestScore && path.getPath(c) != null) { if(path.getPath(c).size() > 0) { bestScore = score; best = c; } } } if(best == d) break; game.moveTroops(d, best, 1); } } } /** * Attacks opponents castles * @param game the game * @throws InterruptedException */ private boolean attackCastles(Game game) throws InterruptedException { Graph g = game.getMap().getGraph(); // Castles that can attack List attackers = g.getAllValues().stream() .filter(x->x.getOwner() == this) .filter(x->isBorder(g, x)) .filter(x->x.getTroopCount() > 1) .collect(Collectors.toList()); for(Castle a : attackers) { Node na = g.getNode(a); List targets = g.getEdges(na).stream() .map(x->x.getOtherNode(na).getValue()) .filter(x->x.getOwner() != this) .collect(Collectors.toList()); if(targets.isEmpty()) continue; // Find best target to attack Castle best = targets.get(0); double bestScore = 0; for(Castle t : targets) { double score = utilityAttack(game, g, this, a, t); if(score > bestScore) { bestScore = score; best = t; } } // Attack, if the chance of taking the castle is more useful than not attacking if(bestScore > evaluateState(game, g, this, Optional.empty()) && a.getTroopCount() > best.getTroopCount()) { if(best.getTroopCount() > scare_threshold || getBatch(g, best.getOwner(), best, new ArrayList<>()).size() == 1) { if(getJokers()[1] == JokerTypes.SCARE_TROOPS) { playScareJoker(best, game); } } AttackThread attackThread = game.startAttack(a, best, a.getTroopCount()); if(fastForward) attackThread.fastForward(); attackThread.join(); return true; }else { return false; } } return false; } @Override protected void actions(Game game) throws InterruptedException { if(game.getRound() == 1) { chooseInitialCastles(game); }else { if(getJokers()[0] == JokerTypes.ADD_TROOPS) { playTroopsJoker(); } distributeTroops(game); boolean shouldAttack = false; do { shouldAttack = attackCastles(game); reinforceCastles(game); }while(shouldAttack); } } /** * Checks, whether a given castle has adjacent opponent castles * @param g the graph * @param castle the castle to be checked * @return true, if castle is on border */ private static boolean isBorder(Graph g, Castle castle) { Node n = g.getNode(castle); int a = (int) g.getEdges(n).stream() .filter(x->x.getOtherNode(n).getValue().getOwner() != null) .filter(x->x.getOtherNode(n).getValue().getOwner() != castle.getOwner()) .count(); return (a > 0)? true : false; } /** * Returns a batch of connected castles around some start castle that belong to the player p * @param g the graph * @param p the owner of the castles * @param castle the start castle * @param checked a list of checked castles, initialize as new ArrayList * @return a list of castles */ public static List getBatch(Graph g, Player p, Castle castle, List checked) { Node n = g.getNode(castle); Set adjacent = g.getEdges(n).stream() .map(x->x.getOtherNode(n).getValue()) .filter(x->x.getOwner() == p) .filter(x->checked.contains(x) == false) .collect(Collectors.toSet()); if(!checked.contains(castle)) checked.add(castle); for(Castle c : adjacent) { checked.addAll(getBatch(g, p, c, checked).stream() .filter(x->checked.contains(x) == false) .collect(Collectors.toSet())); } return checked; } }