diff --git a/Projektgruppe_175/src/game/players/StrongAI.java b/Projektgruppe_175/src/game/players/StrongAI.java index 4b6fa03..61f3b97 100644 --- a/Projektgruppe_175/src/game/players/StrongAI.java +++ b/Projektgruppe_175/src/game/players/StrongAI.java @@ -13,53 +13,68 @@ import game.AI; import game.Game; import game.Player; import game.map.Castle; +import game.map.PathFinding; import gui.AttackThread; +import gui.components.MapPanel.Action; public class StrongAI extends AI { - - private static boolean DEBUG = false; - private static double UTILITY_F1 = 3.0; - private static double UTILITY_F2 = 5.0; - private static double UTILITY_F3 = 1.0; - private static double UTILITY_F4 = -10.0; - private static double UTILITY_F5 = 3.0; - private static double UTILITY_F6 = 2.0; + // These values were determined mostly by trial-and-error - private static double REINFORCE_F1 = 1.0; - private static double REINFORCE_F2 = -5.0; - private static double REINFORCE_F3 = 0.5; - private static double REINFORCE_F4 = -100.0; - private static double REINFORCE_F5 = -2.0; + private static double UTILITY_F1 = 4.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 ATTACK_F1 = 1.0; + 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 EVAL_CASTLES = 1.0; + 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 boolean playedJoker1 = false; + private boolean playedJoker2 = false; + + + 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)? 3.0 : -10.0; + double s = (p == player)? EVAL_OWN : EVAL_OPP; for(Castle c : castles) { - value += s * utilityCastle(g, player, c) * EVAL_CASTLES; + value += s * utilityCastle(g, player, c); } } + // Hand "change" back to its previous owner if(change.isPresent()) { change.get().setOwner(owner); } @@ -104,21 +119,7 @@ public class StrongAI extends AI { // Castles missing to a full kingdom double f6 = c.getKingdom().getCastles().stream() .filter(x->x.getOwner() == p).count() - c.getKingdom().getCastles().size() + 2; - - if(DEBUG) { - System.out.println("-----"); - System.out.println("Castle: " + c.getName()); - System.out.println("F1: " + f1 * UTILITY_F1); - System.out.println("F2: " + f2 * UTILITY_F2); - System.out.println("F3: " + f3 * UTILITY_F3); - System.out.println("F4: " + f4 * UTILITY_F4); - System.out.println("F5: " + f5 * UTILITY_F5); - System.out.println("F6: " + f6 * UTILITY_F6); - System.out.println("Total: " + (UTILITY_F1 * f1 + UTILITY_F2 * f2 - + UTILITY_F3 * f3 + UTILITY_F4 * f4 + f5 * UTILITY_F5 - + f6 * UTILITY_F6)); - System.out.println("-----"); - } + return UTILITY_F1 * f1 + UTILITY_F2 * f2 + UTILITY_F3 * f3 + UTILITY_F4 * f4 + f5 * UTILITY_F5 + f6 * UTILITY_F6; @@ -126,10 +127,10 @@ public class StrongAI extends AI { /** * Analyzes the utility to attack a certain castle - * @param g - * @param p - * @param c - * @return + * @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); @@ -152,21 +153,9 @@ public class StrongAI extends AI { // Is border castle double f4 = isBorder(g, c)? 0.0 : 1.0; - // Number of troops exceeding 4 - double f5 = c.getTroopCount()-4; - - if(DEBUG) { - System.out.println("-----"); - System.out.println("Castle: " + c.getName()); - System.out.println("F1: " + f1 * REINFORCE_F1); - System.out.println("F2: " + f2 * REINFORCE_F2); - System.out.println("F3: " + f3 * REINFORCE_F3); - System.out.println("F4: " + f4 * REINFORCE_F4); - System.out.println("F5: " + f5 * REINFORCE_F5); - System.out.println("Total: " + (REINFORCE_F1 * f1 + REINFORCE_F2 * f2 + f3 * REINFORCE_F3 - + f4 * REINFORCE_F4 + f5 * REINFORCE_F5)); - System.out.println("-----"); - } + // Number of troops exceeding + double f5 = c.getTroopCount(); + return REINFORCE_F1 * f1 + REINFORCE_F2 * f2 + f3 * REINFORCE_F3 + f4 * REINFORCE_F4 + f5 * REINFORCE_F5; } @@ -184,40 +173,11 @@ public class StrongAI extends AI { double ratio = ((double)a.getTroopCount())/t.getTroopCount(); - return ratio * (ATTACK_F1 * f1); + return ATTACK_F1 * f1 * ratio; } /** - * 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; - } - - public StrongAI(String name, Color color) { - super(name, color); - } - - /** - * Chooses the initial castles in round 1 + * Chooses the initial castles in round one * @param game the game * @throws InterruptedException */ @@ -227,15 +187,12 @@ public class StrongAI extends AI { .collect(Collectors.toList()); while(availableCastles.size() > 0 && getRemainingTroops() > 0) { - - sleep(3); - + sleep(1000); 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)); - //score = evaluateLookahead(game, game.getMap().getGraph(), this, 10); if(score > bestScore) { bestScore = score; best = c; @@ -256,6 +213,7 @@ public class StrongAI extends AI { 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; @@ -266,7 +224,7 @@ public class StrongAI extends AI { best = c; } } - sleep(5); + sleep(500); game.addTroops(this, best, 1); } @@ -295,12 +253,16 @@ public class StrongAI extends AI { 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) { - bestScore = score; - best = c; + if(score > bestScore && path.getPath(c) != null) { + if(path.getPath(c).size() > 0) { + bestScore = score; + best = c; + } } } if(best == d) break; @@ -342,7 +304,8 @@ public class StrongAI extends AI { best = t; } } - if(bestScore > evaluateState(game, g, this, Optional.empty())) { + // 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()) { AttackThread attackThread = game.startAttack(a, best, a.getTroopCount()); if(fastForward) attackThread.fastForward(); @@ -357,20 +320,38 @@ public class StrongAI extends AI { return false; } + /** + * Play some jokers + * @param game the current game + */ + private void playJokers(Game game) { + if(!playedJoker1) { + if(this.getCastles(game).size() <= game.getMap().getCastles().size()/2) { + playedJoker1 = true; + System.out.println("StrongAI played joker 1."); + this.addTroops(5); + } + } + if(!playedJoker2) { + // TODO: Play joker 2 + } + } + @Override protected void actions(Game game) throws InterruptedException { if(game.getRound() == 1) { chooseInitialCastles(game); }else { + playJokers(game); distributeTroops(game); boolean shouldAttack = false; do { - //fastForward(); shouldAttack = attackCastles(game); reinforceCastles(game); }while(shouldAttack); } } + /** * Checks, whether a given castle has adjacent opponent castles * @param g the graph @@ -386,5 +367,29 @@ public class StrongAI extends AI { 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; + } } diff --git a/doc/Dokumentation.tex b/doc/Dokumentation.tex index 288a830..66b5d7a 100644 --- a/doc/Dokumentation.tex +++ b/doc/Dokumentation.tex @@ -298,7 +298,55 @@ $$\Theta(n^4)$$ \section{Weitergestaltung des Spiels} \subsection{Computergegner} +\subsubsection{Funktionsweise} +Die hier verwendete Implementation ist eine \textit{Utility Based AI}. Die Funktionalität beruht hauptsächlich auf mehreren \textit{Utility-Funktionen}, welche die Nützlichkeit einer bestimmten Aktion bewerten. +Der Computer prüft verschiedene verfügbare Züge, bestimmt jeweils deren \textit{utility} und wählt den besten aus, wobei darauf geachtet wird, dass der Zug dem Computer den größtmöglichen Vorteil und dem Gegner den größtmöglichen Nachteil verschafft. + +In der ersten Runde nutzt der Computer die \texttt{evaluateState} Funktion (siehe 2.1.2), um immer die beste verfügbare Burg, in Abhängigkeit der bereits gewählten Burgen zu bestimmen. + +Ab der zweiten Runde werden, falls sinnvoll, zunächst Joker gespielt und anschließend die neuen Truppen auf die Burgen verteilt, wobei mit der Funktion \texttt{utilityReinforce} bestimmt wird, welche Burgen am nötigsten Truppen brauchen. + +Danach beginnt die Eroberungsphase der Runde: mithilfe der \texttt{utilityCastle}-Funktion wird die wertvollste gegnerische Burg bestimmt und angegriffen, sofern der Angriff für den Computer sinnvoll ist. + +Nach jedem Angriff werden die Truppen zu den Burgen am Rand umverteilt, wobei wieder nach \textit{utility} vorgegangen wird. Die Angriffsphase geht solange, wie es noch Burgen gibt, die sinnvoll angegriffen werden können. + +\subsubsection{Utility-Funktionen} + +Es werden drei verschiedene Utility-Funktionen verwendet, um die Nützlichkeit einer bestimmten Aktion zu bewerten: \texttt{utilityCastle}, \texttt{utilityReinforce} und \texttt{utilityAttack}, sowie eine vierte Funktion \texttt{evaluateState}, die den gesamten Spielzustand bewertet. +Bei den ersten drei Funktionen handelt es sich um lineare Polynome der Form + +\begin{center} +\begin{large} +$\sum \limits_{k=1}^{n} \alpha_k f_k$ +\end{large} +\end{center} + +wobei $f_k$ wiederum Funktionen sind, welche jeweils einen Aspekt der Bewertung betrachten, mehr dazu unten. + +Bei den Koeffizienten $\alpha_k$ handelt es sich um um \texttt{double}-Zahlen, welche die einzelnen Funktionen gewichten. Die drei Utility-Funktionen unterscheiden sich in den jeweils betrachteten Aspekten. + +Die Funktion \texttt{evaluateState} berechnet den kumulierten Wert aller Burgen für den Computer und subtrahiert einen gewichteten Wert der Burgen für den Gegner. Sie bietet also eine Möglichkeit, den gesamten Wert eines Spielzustandes zu bestimmen. +\\\\ +\textbf{utilityCastle}\\\\ +Diese Funktion bestimmt den "Wert" einer Burg für einen bestimmten Spieler. Hierbei fließen die Anzahl der angrenzenden eigenen und gegnerischen Burgen, die Zahl der abgehenden Kanten, die Größe des Blocks von zusammenhängenden eigenen Burgen, in dem sich die Burg befindet und weitere Parameter ein. Diese werden gewichtet und addiert, um einen heuristischen \textit{utility}-Wert zu erzeugen. + +Eine Burg wird als "wertvoll" betrachtet, wenn sie an einer strategisch günstigen Position ist, und durch andere Burgen unterstützt werden kann. Dies soll dann durch einen hohen Ausgabewert ausgedrückt werden. +\\\\ +\textbf{utilityReinforce}\\\\ +Diese Funktion gibt an, wie sinnvoll es ist, eine bestimmte Burg mit Truppen zu versorgen. Hierbei spielen die Zahl der angrenzenden gegnerischen Burgen, die Zahl der Truppen in der Burg, der Wert der Burg und weitere Parameter eine Rolle. +Generell sollen Burgen unterstützt werden, die entweder durch viele starke gegnerische Burgen gefährdet sind, oder für einen taktischen Angriff verwendet werden können. +\\\\ +\textbf{utilityAttack}\\\\ +Diese Funktion gibt an, wie sinnvoll es ist, eine bestimmte gegnerische Burg anzugreifen. Hier fließen der Wert der Burg für den Computer und für den Gegner und das Truppenverhältnis von Angreifer- zu Zielburg ein. + +Burgen sollten dann angegriffen werden, wenn ihre Eroberung den Gesamtspielzustand zugunsten des Computers verändert und der Angriff eine große Erfolgschance hat. + +\subsubsection{Motivation und Anmerkung} + +Die \textit{Utility Based AI} ist ein häufig verwendeter und erfolgreicher Ansatz zur Entwicklung von Computergegnern für Strategiespiele. Die Umsetzung ist inspiriert von Artikeln wie "\textit{An Introduction to Utility Theory}" von David Graham und "\textit{Designing AI Algorithms For Turn-Based Strategy Games}" von Ed Welch, wurde aber vollständig selbst durch Trial-And-Error implementiert und optimiert. Sie ist der \textit{Basic AI} überlegen, hat aber noch Raum für Verbesserung. + +Ein möglicher Verbesserungsvorschlag wäre, den Computer mehrere Züge in die Zukunft zu planen zu lassen und somit intelligenteres, strategisches Vorgehen zu ermöglichen (beispielsweise durch Minimax und co.). \subsection{Missionen} \subsubsection{Begrenzte Rundenanzahl}