This commit is contained in:
joachimschmidt557 2019-03-26 17:41:06 +01:00
commit d0aa8fda5d
2 changed files with 147 additions and 94 deletions

View file

@ -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;
// These values were determined mostly by trial-and-error
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;
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 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 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;
private static double ATTACK_F1 = 1.0; // Castle utility
private static double EVAL_CASTLES = 1.0;
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<Castle> g, Player p, Optional<Castle> 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<Castle> 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);
}
@ -105,20 +120,6 @@ public class StrongAI extends AI {
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<Castle> g, Player p, Castle c) {
Node<Castle> 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;
// Number of troops exceeding
double f5 = c.getTroopCount();
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("-----");
}
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<Castle> getBatch(Graph<Castle> g, Player p, Castle castle, List<Castle> checked) {
Node<Castle> n = g.getNode(castle);
Set<Castle> 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<Castle> 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
@ -387,4 +368,28 @@ 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<Castle> getBatch(Graph<Castle> g, Player p, Castle castle, List<Castle> checked) {
Node<Castle> n = g.getNode(castle);
Set<Castle> 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;
}
}

View file

@ -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}