364 lines
12 KiB
TeX
364 lines
12 KiB
TeX
\documentclass{article}
|
|
|
|
\usepackage{amsmath}
|
|
\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<Node<T>>} 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 \cdot 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 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.
|
|
|
|
\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 < 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.
|
|
|
|
\subsubsection{Teil (b)}
|
|
|
|
Best case: In diesem Fall müsste der Algorithmus
|
|
|
|
$$\Theta()$$
|
|
|
|
Worst case: Im schlimmsten Fall müsste der Algorithmus
|
|
in jeder Iteration der Schleife die Länge von jedem
|
|
Knoten zu jedem anderen Knoten 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. Das resultiert in der Asymptotik
|
|
|
|
$$\Theta(n^3)$$
|
|
|
|
\section{Weitergestaltung des Spiels}
|
|
|
|
\subsection{Computergegner}
|
|
|
|
\subsection{Missionen}
|
|
|
|
\subsubsection{Begrenzte Rundenanzahl}
|
|
|
|
Wenn man diese Mission wählt, gilt es, nach einer
|
|
festgelegten Anzahl an Runden die meisten Burgen
|
|
zu besitzen. Sollten zwei oder mehr Spieler
|
|
gleich viele Burgen zu diesem Zeitpunkt in Besitz haben,
|
|
gibt es ein Unentschieden.
|
|
|
|
\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 stationiert zu haben.
|
|
|
|
\subsection{Joker}
|
|
|
|
Wir haben zwei Joker in unser Spiel eingebaut.
|
|
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}
|