\documentclass{article} \usepackage{amsmath} \usepackage{amsfonts} \usepackage{algorithm} \usepackage[noend]{algpseudocode} \usepackage{hyperref} \usepackage[utf8]{inputenc} \title{FOP Projektgruppe 175} \author{Steffen Wagner\\ Dennis Weinberger\\ Jonas Süß\\ Joachim Schmidt} \begin{document} \maketitle \tableofcontents \pagebreak \section{Der Graph} \subsection{Bildung der Kanten} Der Algorithmus für die Bildung der Kanten ist eine Version des \href{https://de.wikipedia.org/wiki/Algorithmus_von_Prim}{Algorithmus von Prim}: \begin{algorithm} \caption{Bildung von Kanten}\label{generate} \begin{algorithmic}[1] \Procedure{generateEdges}{} \If{nodes is empty} return \EndIf \State $castle \gets allCastles[0]$ \State $remainingCastles \gets allCastles$ \State $connectedCastles \gets empty$ \Loop \If{$remainingCastles$ is empty} break \EndIf \State connect $castle$ to nearest castle \State remove $castle$ from $remainingCastles$ \State add $castle$ to $connectedCastles$ \EndLoop \ForAll{castle in allCastles} \ForAll{nearCastle in allCastlesInRadius(castle)} \State connect $castle$ to $nearCastle$ \EndFor \EndFor \EndProcedure \end{algorithmic} \end{algorithm} % Erklärung des Algorithmus Der Algorithmus ist in zwei Schritte aufgeteilt: \begin{itemize} \item Minimale Verbindung von allen Burgen \item Ästhetische Verbesserung der Kanten \end{itemize} Die minimale Verbindung aller Burgen erfolgt, indem sichergestellt wird, dass jede Burg mit einer anderen verbunden ist und dass alle Burgen in einer gemeinsamen Verbindung zusammenhängen. Der Algorithmus fängt bei einer bestimmten Start-Burg an und verbindet diese Burg mit der nächstliegenden Burg, die noch nicht verbunden wurde. Daraufhin wird das gleiche mit der nächsten, übernächsten, usw. Burg getan, bis die letzte Burg erreicht wurde. Zu diesem Zeitpunkt sind alle Burgen durch einen Graph verbunden. Die Ästhetische Verbesserung erfolgt, indem alle Burgen im Umkreis einer Burg durch eine Kante verbunden werden. \subsection{Überprüfung der Erreichbarkeit aller Knoten} Der Algorithmus, der prüft, ob alle Knoten erreichbar sind, ist folgender: \begin{algorithm} \caption{Erreichbarkeit aller Knoten}\label{connected} \begin{algorithmic}[1] \Procedure{allNodesConnected}{} \State $\textit{firstNode} \gets \text{first element of }\textit{nodes}$ \State $allVisitedNodes \gets \textit{empty}$ \State $nextVisitNodes \gets empty$ \State $\text{append } firstNode \text{ to } allVisitedNodes$ \State $\text{neighborsOf } firstNode$ \State $\rightarrow \text{filter out all } x \text{ where } allVisitedNodes \text{ contains } x$ \State $\rightarrow \text{append to } nextVisitNodes$ \Loop \If {$nextVisitNodes \text{ is empty}$} break \EndIf \State $\text{append first element of } nextVisitNodes \text{ to } allVisitedNodes$ \State $\text{neighborsOf first element of } nextVisitNodes$ \State $\rightarrow \text{filter out all } x \text{ where } allVisitedNodes \text{ contains } x$ \State $\rightarrow \text{append to } nextVisitNodes$ \State $\text{delete first element of } nextVisitNodes$ \EndLoop \EndProcedure \end{algorithmic} \end{algorithm} Der Algorithmus verwendet zwei unterschiedliche Datentypen: \begin{itemize} \item HashSet wird verwendet, um die bisher besuchten Knoten zu speichern. Eine HashSet hat den Vorteil, dass Elemente nur einmal gespeichert werden können. \item ArrayDeque wird verwendet, um die nächsten Knoten, die besucht werden, zu speichern. \end{itemize} Der Algorithmus sammelt sozusagen alle Knoten, die aufgrund von momentanen Erkenntnissen erreichbar sind, in der ArrayDeque \texttt{nextVisitNodes}. Hingegen sind alle Knoten, die schon erreicht worden sind, in dem HashSet \texttt{allVisitedNodes} gespeichert. Der Algorithmus geht die ArrayDeque \texttt{nextVisitNodes} solange durch, bis diese leer ist. In jeder Iteration wird das erste Element der Liste aus der Liste entfernt. Zunächst wird dieses Element dem HashSet \texttt{allVisitedNodes} hinzugefügt. Daraufhin werden die Nachbarn dieses Elements herausgefunden. Diejenigen Nachbarn, die schon in dem HashSet \texttt{allVisitedNodes} vorhanden sind, werden verworfen. Die restlichen Nachbarn werden der ArrayDeque \texttt{nextVisitNodes} hinzugefügt. \subsection{Wege finden} \subsubsection{Teil (a)} Eine pseudocode-Darstellung des Algorithmus: \begin{algorithm} \caption{Berechnung der Distanzen}\label{path} \begin{algorithmic}[1] \Procedure{run}{} \State $v \gets$ getSmallestNode() \Loop \If{$v$ is null} \Return \EndIf \ForAll{edges} \If{edge is passable} \State $n \gets$ otherNode \State $a \gets$ v.value + edge.value \If{n.value $= -1$ or n.value $> a$} \State n.value $\gets$ a \State n.previous $\gets$ v \EndIf \EndIf \EndFor \State $v \gets getSmallestNode()$ \EndLoop \EndProcedure \end{algorithmic} \end{algorithm} Die algorithmische Komplexität im worst-case ist: $$O(n^2)$$ $n$ soll in diesem Fall die Anzahl an Knoten wiedergeben. Da es sich bei \texttt{availableNodes} um eine \texttt{LinkedList>} handelt, muss bei jedem Durchgehen der Liste jedes Element einzeln abgearbeitet werden. In der Funktion \texttt{getSmallestNode()} ist dies der Fall. Das bedeutet, dass bei jeder Aufruf von \texttt{getSmallestNode()} eine Komplexität von $O(n)$ hat, wenn $n$ die Anzahl der Knoten darstellt. Bei jeder Iteration des Algorithmus wird zuerst der Knoten mit dem kleinsten Wert gesucht. Dann werden alle Nachbarn dieses Knotens abgeprüft. Das können höchstens $n - 1$ Knoten sein, da ausgeschlossen wird, dass ein Knoten eine Kante mit sich selber haben kann. \subsubsection{Teil (b)} Anstelle einer \texttt{LinkedList} braucht man eine Datenstruktur, die den Zugriff auf das kleinste Element effektiver gestaltet. Ein Beispiel dafür ist die \texttt{PriorityQueue}, die wir in Hausübung 8 implementiert haben. Dort werden Elemente beim Einfügen bereits sortiert, sodass der Zugriff auf das (in diesem Fall) kleinste Element in $O(1)$, also konstanter Zeit, gemacht werden kann. Die algorithmische Komplexität mit dieser Änderung beträgt dann im worst case $$O(n)$$ Für einen allgemeine Datentyp \texttt{T} kann man folgendes feststellen: Sei $x$ die algorithmische Komplexität für das Suchen vom kleinsten Element (z.B. $x = O(n)$ bei \texttt{LinkedList}). Sei $y$ die algorithmische Komplexität für das Entfernen vom kleinsten Element (bei einer normalen \texttt{LinkedList} $O(1)$). Dann beträgt die algorithmische Komplexität $$O(n \cdot (x + y))$$ \subsubsection{Teil (c)} Sei $n$ die Anzahl an Knoten. \textbf{Invariante}: Nach $h \geq 0$ Durchläufen gilt: \texttt{availableNodes}, die Liste von Knoten, die noch nicht bearbeitet wurden enthält $n - h$ Elemente. Diese Knoten sind alle Knoten ohne die $h$ kleinsten Knoten, also alle $n - h$ größten Knoten. Für alle noch nicht abgearbeiteten Knoten gilt: $n - h$ Elemente wurden noch nicht abgearbeitet. Für diese Knoten gilt, dass sie entweder ein nicht-gesetzten Wert (bei unserer Implementation $-1$) besitzen oder ein Wert $x$ besitzen. $x$ stellt die minimale Distanz dar, die von dem Ursprungsknoten zu diesem Knoten erreicht werden kann, wenn man als Zwischenschritte nur die $h$ abgearbeiteten Knoten nimmt. Für alle abgearbeiteten Knoten gilt: $h$ Knoten sind schon abgearbeitet worden. Die zu den Knoten zugehörigen \texttt{AlgorithmNodes}-Objekte beinhalten die insgesamte Länge zu dem Startknoten. Diese Länge wird nicht weiter modifiziert, da sie die minimale Distanz zum Startknoten ist. \subsection{Kürzester Pfad zu allen Knoten} \subsubsection{Teil (a)} Wenn ein Zykel auftaucht, dann bedeutet das, dass eine Menge $K$ an Knoten gibt, sodass $K = \{k_1 ... k_n\}$ und Kanten zwischen $k_1$ und $k_2$ usw. bis $k_n$ und $k_1$ existieren. Ein negativer Zykel ist dann vorhanden, wenn $M^0(k_1, k_2) + ... + M^0(k_n, k_1) < 0$. Oder mathematischer aufgeschrieben: $$\sum_{l=1}^{n-1} {M^0(k_l, k_{l+1})} + M^0(k_n, k_1) < 0$$ Laut der Schleifeninvariante gilt, dass nach $h \geq 0$ Schritten $M^h$ die Längen von den Pfaden enthält, die am kürzesten sind und dabei nicht mehr als $h + 1$ Kanten besuchen. Sei also $x$ die Länge des Zykels. Es gilt: $x < 0$. Daraus folgt, dass $2 \cdot x < x$ und $3 \cdot x < 2 \cdot x < x$ usw. Bei beliebig großen $n$ gibt es also die Gefahr, dass der Algorithmus versucht, so oft wie möglich diesen Zyklus zu durchlaufen, um die kleinste Länge zu erreichen. Wenn aber der Algorithmus terminiert, ist garantiert, dass der Pfad, der bei dem negativen Zyklus angegeben wird, nicht der kürzeste ist. Die Länge des Pfades ist nämlich der Form $n \cdot x$ mit $n \in \mathbb{N}$. Dann gilt $(n+1) \cdot x < n \cdot x$. \subsubsection{Teil (b)} Best case: Der best case wäre ein Graph, der keine Kanten hat, bzw. die dazugeörige Matrix, die leer wäre. In diesem Fall gibt es weiterhin $n$ Iterationen, bei der jeweils für jeden Eintrag in der Matrix geschaut wird, ob ein Pfad existiert. Die Asymptotik ist also $$\Theta(n^3)$$ Worst case: Im schlimmsten Fall wäre in dem Graph jeder Knoten mit jedem anderen Knoten verbunden. Dann müsste der Algorithmus in jeder Iteration der Schleife die Länge von jedem Knoten zu jedem anderen Knoten neu berechnen. Es müssten also alle Felder der $(n \times n)$-Matrix neu berechnet werden. Deswegen müssten jede Iteration $n^2$ Schritte durchgeführt werden. In jedem Schritt wird verglichen, ob der momentane Eintrag in der Matrix bereits der kürzeste Weg ist oder ob es noch einen kürzeren Weg gibt, der aber ein Knoten mehr besuchen darf ($h+1$). Es müssen also pro Schritt $n$ Vergleiche stattfinden. Daraus resultiert die Asymptotik $$\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{Countdown} Wenn man diese Mission wählt, gilt es, nach einer festgelegten Anzahl an Runden die meisten Burgen zu besitzen. Diese ist auf sechs Runden festgelegt, kann aber im Quellcode leicht modizifiert werden. Sollten zwei oder mehr Spieler gleich viele Burgen zu diesem Zeitpunkt in Besitz haben, gibt es ein Unentschieden. Um diese Mission bereitzustellen, wurde die Klasse \texttt{TimeGoal} erstellt. Sie behandelt die gesamte interne Logik der Mission. \subsubsection{Capture the Flag} In dieser Mission werden wichtige Burgen, sogenannte Flags, gleichmäßig auf die Spieler verteilt. Es ist das Ziel, zuerst vor allen anderen Spielern alle diese Burgen zu erobern. Zur Erstellung dieser Mission wurde die Klasse \texttt{CaptureTheFlagGoal} erstellt. Diese Klasse behandelt die gesamte Logik der Mission. Zu diesen Aufgaben gehört nicht nur die Überprüfung, ob zu einem Zeitpunkt ein Spieler gewonnen oder verloren hat, sondern auch bei dieser Mission die Verteilung der Flaggen-Burgen auf die Spieler. Das wird dann gemacht, sobald alle Burgen auf die Spieler aufgeteilt wurden. Dann werden per Zufallsprinzip die Burgen ausgewählt. Zur vollständigen visuellen Darstellung dieser Mission wurde noch die Klasse \texttt{MapPanel} erweitert. Nun wird bei jedem Aufruf von \texttt{ImagePanel.paintComponent} eine weiße Umrandung um die Flaggen-Burgen gezeichnet, wenn die Mission aktiv ist und die Burgen verteilt wurden. \subsubsection{Bevölkerung} Das Ziel der Spieler, die diese Mission ausgewählt haben, ist es, vor allen anderen Spielern eine bestimmte Anzahl an Truppen auf verschiedenen Burgen zum Rundenanfang stationiert zu haben. Falls zwei oder mehr Spieler die benötigte Anzahl an Truppen haben, so wird nach der Anzahl der Burgen entschieden. Dabei ist die Bevölkerung/Truppenanzahl in den zusätzlichen Burgen unerheblich, es zählt nur die Gesamtanzahl. Ist diese ebenfalls bei mindestens zwei Spielern gleich, gibt es ein Unentschieden. Die Mission ist in der Klasse \texttt{PopulationGoal} verankert. \subsection{Joker} Wir haben zwei Joker in unser Spiel eingebaut. Die meiste Logik wurde in \texttt{JokerPanel} implementiert, darunter auch in \texttt{MapPanel} und anderen Klassen. Diese sind in einem Panel über dem MapPanel auswählbar. \subsubsection{Truppen-Joker} Der Truppen-Joker hat eine ganz einfache Funktion: Der Spieler, der ihn benutzt, bekommt fünf neue Truppen zum Verteilen. Der Truppen-Joker kann nur einmal verwendet werden. Man findet ihn direkt über der Karte oben links. Der Truppen-Joker ist mit einem braunen Plus dargestellt. \subsubsection{Truppen-Verscheuchen-Joker} Ein etwas kreativerer Joker ist der Truppen-Verscheuchen-Joker. Dieser befindet sich ebenfalls im Joker-Bereich über der Map an zweiter Stelle rechts neben dem Truppen-Joker. Der Spieler, der den Truppen-Verscheuchen-Joker benutzt, darf sich eine gegnerische Burg auswählen. Aus dieser Burg fliehen dann alle Truppen in benachbarte Burgen des Spielers, dem die ausgewählte Burg gehört. Die Aufteilung auf die benachbarten Burgen ist dabei zufällig. Falls es nur eine benachbarte Burg des gleichen Spielers gibt, fliehen alle Truppen dorthin. Falls es keine benachbarte Burg des gleichen Spielers gibt, d.h. die Burg isoliert ist, so bleibt bis auf eine Truppe niemand in dieser Burg übrig. Der Truppen-Verscheuchen-Joker ist mit einem erschreckten Pixel-Gesicht dargestellt. Der Truppen-Verscheuchen-Joker erlaubt eine Vielzahl an neuen Strategien im Spiel, weshalb wir uns trotz der etwas schwereren Implementation dafür entschieden hatten, ihn einzubauen. So ermöglicht er es z.B. einen Spieler, der nur auf einer einzelnen Burg alle seine Truppen behaust, zu bestrafen. Diese kann man umzingeln und dann den Joker einsetzen. Vor allem ist das nützlich, wenn nur noch eine einzelne Burg nicht erobert wurde, und das Spiel sich sonst lange ziehen würde, d.h. dieser Joker kann das Spiel ebenso beschleunigen. Umgekehrt müssen Spieler nun darauf achten, Truppen möglichst gleichmäßig zu verteilen, um dieser Taktik entgegenzuwirken, und Burgen möglichst zusammenhängend zu erobern, damit im Falle des Joker-Einsatzes die Truppen zumindest nicht komplett verloren gehen, sondern in eine andere Burg wandern. Der Joker erhöht also auch die Spielkomplexität und macht das Spiel damit insgesamt interessanter. \end{document}