diff --git a/Projektgruppe_XXX.zip b/Projektgruppe_XXX.zip new file mode 100644 index 0000000..2c0532b Binary files /dev/null and b/Projektgruppe_XXX.zip differ diff --git a/Projektgruppe_XXX/.classpath b/Projektgruppe_XXX/.classpath new file mode 100644 index 0000000..3a76474 --- /dev/null +++ b/Projektgruppe_XXX/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Projektgruppe_XXX/.project b/Projektgruppe_XXX/.project new file mode 100644 index 0000000..516eb9b --- /dev/null +++ b/Projektgruppe_XXX/.project @@ -0,0 +1,17 @@ + + + Projektgruppe_XXX + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/Projektgruppe_XXX/res/arrow.png b/Projektgruppe_XXX/res/arrow.png new file mode 100644 index 0000000..aee3c01 Binary files /dev/null and b/Projektgruppe_XXX/res/arrow.png differ diff --git a/Projektgruppe_XXX/res/arrow_deactivated.png b/Projektgruppe_XXX/res/arrow_deactivated.png new file mode 100644 index 0000000..b1b33d5 Binary files /dev/null and b/Projektgruppe_XXX/res/arrow_deactivated.png differ diff --git a/Projektgruppe_XXX/res/castle1.png b/Projektgruppe_XXX/res/castle1.png new file mode 100644 index 0000000..c734ce4 Binary files /dev/null and b/Projektgruppe_XXX/res/castle1.png differ diff --git a/Projektgruppe_XXX/res/castle2.png b/Projektgruppe_XXX/res/castle2.png new file mode 100644 index 0000000..527e988 Binary files /dev/null and b/Projektgruppe_XXX/res/castle2.png differ diff --git a/Projektgruppe_XXX/res/castle3.png b/Projektgruppe_XXX/res/castle3.png new file mode 100644 index 0000000..45c0f34 Binary files /dev/null and b/Projektgruppe_XXX/res/castle3.png differ diff --git a/Projektgruppe_XXX/res/castle4.png b/Projektgruppe_XXX/res/castle4.png new file mode 100644 index 0000000..85ea4e6 Binary files /dev/null and b/Projektgruppe_XXX/res/castle4.png differ diff --git a/Projektgruppe_XXX/res/castle5.png b/Projektgruppe_XXX/res/castle5.png new file mode 100644 index 0000000..d8aaf20 Binary files /dev/null and b/Projektgruppe_XXX/res/castle5.png differ diff --git a/Projektgruppe_XXX/res/castle6.png b/Projektgruppe_XXX/res/castle6.png new file mode 100644 index 0000000..eef46c5 Binary files /dev/null and b/Projektgruppe_XXX/res/castle6.png differ diff --git a/Projektgruppe_XXX/res/castles.txt b/Projektgruppe_XXX/res/castles.txt new file mode 100644 index 0000000..c978c3e --- /dev/null +++ b/Projektgruppe_XXX/res/castles.txt @@ -0,0 +1,93 @@ +Ehrenberg +Hohenzollern +Hohenfels +Hornberg +Hohenstaufen +Katzenstein +Möckmühl +Rotwasserstelz +Sponeck +Staufenberg +Steinsberg +Stettenfels +Wertheim +Wildenstein +Hohenasperg +Rastatt +Beuggen +Brackenheim +Bronnen +Bruchsal +Ettlingen +Geislingen +Gottesaue +Haigerloch +Hellenstein +Hettingen +Horneck +Kißlegg +Kirchberg +Lichtenstein +Lindach +Menzingen +Montfort +Rechenberg +Salem +Sigmaringen +Spetzgart +Winnental +Hohentwiel +Waldenstein +Abenberg +Brennberg +Dollnstein +Donaustauf +Falkenberg +Falkenstein +Feuerstein +Guttenberg +Hiltpoltstein +Hohenberg +Hohenfreyberg +Hohenstein +Kipfenberg +Lauenstein +Lisberg +Lobenstein +Marquartstein +Pottenstein +Randeck +Schwaneck +Stockenfels +Trausnitz +Marienberg +Rosenberg +Birkenfeld +Bertholdsheim +Dillingen +Ebelsbach +Egg +Elmau +Emersacker +Frankenberg +Gleusdorf +Höchstädt +Johannisburg +Ketschendorf +Leutstetten +Neidstein +Possenhofen +Pürkelgut +Rathsmannsdorf +Regendorf +Mespelbrunn +Rabenstein +Schauenstein +Schillingsfürst +Schönberg +Sommersdorf +Thurnau +Theuern +Tüßling +Waal +Wörth \ No newline at end of file diff --git a/Projektgruppe_XXX/res/celtic.ttf b/Projektgruppe_XXX/res/celtic.ttf new file mode 100644 index 0000000..52a94db Binary files /dev/null and b/Projektgruppe_XXX/res/celtic.ttf differ diff --git a/Projektgruppe_XXX/res/check.png b/Projektgruppe_XXX/res/check.png new file mode 100644 index 0000000..40fac88 Binary files /dev/null and b/Projektgruppe_XXX/res/check.png differ diff --git a/Projektgruppe_XXX/res/dices.png b/Projektgruppe_XXX/res/dices.png new file mode 100644 index 0000000..592df7e Binary files /dev/null and b/Projektgruppe_XXX/res/dices.png differ diff --git a/Projektgruppe_XXX/res/plus.png b/Projektgruppe_XXX/res/plus.png new file mode 100644 index 0000000..654a9b8 Binary files /dev/null and b/Projektgruppe_XXX/res/plus.png differ diff --git a/Projektgruppe_XXX/res/plus_deactivated.png b/Projektgruppe_XXX/res/plus_deactivated.png new file mode 100644 index 0000000..d2ef8a2 Binary files /dev/null and b/Projektgruppe_XXX/res/plus_deactivated.png differ diff --git a/Projektgruppe_XXX/res/soldier1.png b/Projektgruppe_XXX/res/soldier1.png new file mode 100644 index 0000000..0764d1b Binary files /dev/null and b/Projektgruppe_XXX/res/soldier1.png differ diff --git a/Projektgruppe_XXX/res/soldier2.png b/Projektgruppe_XXX/res/soldier2.png new file mode 100644 index 0000000..6bb8630 Binary files /dev/null and b/Projektgruppe_XXX/res/soldier2.png differ diff --git a/Projektgruppe_XXX/res/swords.png b/Projektgruppe_XXX/res/swords.png new file mode 100644 index 0000000..fbdf3bb Binary files /dev/null and b/Projektgruppe_XXX/res/swords.png differ diff --git a/Projektgruppe_XXX/res/unit.png b/Projektgruppe_XXX/res/unit.png new file mode 100644 index 0000000..ea4b209 Binary files /dev/null and b/Projektgruppe_XXX/res/unit.png differ diff --git a/Projektgruppe_XXX/src/META-INF/MANIFEST.MF b/Projektgruppe_XXX/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..86b930d --- /dev/null +++ b/Projektgruppe_XXX/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: gui.GameWindow + diff --git a/Projektgruppe_XXX/src/base/Edge.java b/Projektgruppe_XXX/src/base/Edge.java new file mode 100644 index 0000000..195837f --- /dev/null +++ b/Projektgruppe_XXX/src/base/Edge.java @@ -0,0 +1,54 @@ +package base; + +/** + * Diese Klasse representiert eine generische Kante zwischen zwei Knoten + * @param die zugrunde liegende Datenstruktur + */ +public class Edge { + + private Node nodeA, nodeB; + + /** + * Erstellt eine neue Kante zwischen zwei gegebenen Knoten + * @param nodeA der erste Knoten + * @param nodeB der zweite Knoten + */ + Edge(Node nodeA, Node nodeB) { + this.nodeA = nodeA; + this.nodeB = nodeB; + } + + /** + * Gibt an, ob die Kante mit dem gegebenen Knoten verbunden ist + * @param node Der Knoten der überprüft wird + * @return true, wenn die Kante mit dem Knoten verbunden ist + */ + boolean contains(Node node) { + return nodeA == node || nodeB == node; + } + + /** + * Gibt den ersten Knoten zurück + * @return der erste Knoten + */ + public Node getNodeA() { + return nodeA; + } + + /** + * Gibt den zweiten Knoten zurück + * @return der zweite Knoten + */ + public Node getNodeB() { + return nodeB; + } + + /** + * Gibt den jeweils anderen Knoten zurück, abhängig von dem gegebenen + * @param source der eine Knoten + * @return der andere Knoten + */ + public Node getOtherNode(Node source) { + return (nodeA == source ? nodeB : nodeA); + } +} diff --git a/Projektgruppe_XXX/src/base/Graph.java b/Projektgruppe_XXX/src/base/Graph.java new file mode 100644 index 0000000..3957bab --- /dev/null +++ b/Projektgruppe_XXX/src/base/Graph.java @@ -0,0 +1,127 @@ +package base; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collector; + +import game.map.Castle; + +/** + * Diese Klasse representiert einen generischen Graphen mit einer Liste aus Knoten und Kanten. + * + * @param Die zugrundeliegende Datenstruktur, beispielsweise {@link game.map.Castle} + */ +public class Graph { + + private List> edges; + private List> nodes; + + /** + * Konstruktor für einen neuen, leeren Graphen + */ + public Graph() { + this.nodes = new ArrayList<>(); + this.edges = new LinkedList<>(); + } + + /** + * Einen neuen Knoten zum Graphen hinzufügen + * @param value Der Wert des Knotens + * @return Der erstellte Knoten + */ + public Node addNode(T value) { + Node node = new Node<>(value); + this.nodes.add(node); + return node; + } + + /** + * Eine neue Kante zwischen zwei Knoten hinzufügen. Sollte die Kante schon existieren, wird die vorhandene Kante zurückgegeben. + * @param nodeA Der erste Knoten + * @param nodeB Der zweite Knoten + * @return Die erstellte oder bereits vorhandene Kante zwischen beiden gegebenen Knoten + */ + public Edge addEdge(Node nodeA, Node nodeB) { + Edge edge = getEdge(nodeA, nodeB); + if(edge != null) { + return edge; + } + + edge = new Edge<>(nodeA, nodeB); + this.edges.add(edge); + return edge; + } + + /** + * Gibt die Liste aller Knoten zurück + * @return die Liste aller Knoten + */ + public List> getNodes() { + return this.nodes; + } + + /** + * Gibt die Liste aller Kanten zurück + * @return die Liste aller Kanten + */ + public List> getEdges() { + return this.edges; + } + + /** + * Diese Methode gibt alle Werte der Knoten in einer Liste mittels Streams zurück. + * @see java.util.stream.Stream#map(Function) + * @see java.util.stream.Stream#collect(Collector) + * @return Eine Liste aller Knotenwerte + */ + public List getAllValues() { + // TODO: Graph#getAllValues() + return new ArrayList<>(); + } + + /** + * Diese Methode gibt alle Kanten eines Knotens als Liste mittels Streams zurück. + * @param node Der Knoten für die dazugehörigen Kanten + * @see java.util.stream.Stream#filter(Predicate) + * @see java.util.stream.Stream#collect(Collector) + * @return Die Liste aller zum Knoten zugehörigen Kanten + */ + public List> getEdges(Node node) { + // TODO: Graph#getEdges(Node) + return new ArrayList<>(); + } + + /** + * Diese Methode sucht eine Kante zwischen beiden angegebenen Knoten und gibt diese zurück + * oder null, falls diese Kante nicht existiert + * @param nodeA Der erste Knoten + * @param nodeB Der zweite Knoten + * @return Die Kante zwischen beiden Knoten oder null + */ + public Edge getEdge(Node nodeA, Node nodeB) { + // TODO: Graph#getEdge(Node, Node) + return null; + } + + /** + * Gibt den ersten Knoten mit dem angegebenen Wert zurück oder null, falls dieser nicht gefunden wurde + * @param value Der zu suchende Wert + * @return Ein Knoten mit dem angegebenen Wert oder null + */ + public Node getNode(T value) { + // TODO: Graph#getNode(T) + return null; + } + + /** + * Überprüft, ob alle Knoten in dem Graphen erreichbar sind. + * @return true, wenn alle Knoten erreichbar sind + */ + public boolean allNodesConnected() { + // TODO: Graph#allNodesConnected() + return false; + } +} diff --git a/Projektgruppe_XXX/src/base/GraphAlgorithm.java b/Projektgruppe_XXX/src/base/GraphAlgorithm.java new file mode 100644 index 0000000..c06563d --- /dev/null +++ b/Projektgruppe_XXX/src/base/GraphAlgorithm.java @@ -0,0 +1,118 @@ +package base; + +import java.util.*; + +/** + * Abstrakte generische Klasse um Wege zwischen Knoten in einem Graph zu finden. + * Eine implementierende Klasse ist beispielsweise {@link game.map.PathFinding} + * @param Die Datenstruktur des Graphen + */ +public abstract class GraphAlgorithm { + + /** + * Innere Klasse um {@link Node} zu erweitern, aber nicht zu verändern + * Sie weist jedem Knoten einen Wert und einen Vorgängerknoten zu. + * @param + */ + private static class AlgorithmNode { + + private Node node; + private double value; + private AlgorithmNode previous; + + AlgorithmNode(Node parentNode, AlgorithmNode previousNode, double value) { + this.node = parentNode; + this.previous = previousNode; + this.value = value; + } + } + + private Graph graph; + + // Diese Liste enthält alle Knoten, die noch nicht abgearbeitet wurden + private List> availableNodes; + + // Diese Map enthält alle Zuordnungen + private Map, AlgorithmNode> algorithmNodes; + + /** + * Erzeugt ein neues GraphAlgorithm-Objekt mit dem dazugehörigen Graphen und dem Startknoten. + * @param graph der zu betrachtende Graph + * @param sourceNode der Startknoten + */ + public GraphAlgorithm(Graph graph, Node sourceNode) { + this.graph = graph; + this.availableNodes = new LinkedList<>(graph.getNodes()); + this.algorithmNodes = new HashMap<>(); + + for(Node node : graph.getNodes()) + this.algorithmNodes.put(node, new AlgorithmNode<>(sourceNode, null, -1)); + + this.algorithmNodes.get(sourceNode).value = 0; + } + + /** + * Diese Methode gibt einen Knoten mit dem kleinsten Wert, der noch nicht abgearbeitet wurde, zurück und entfernt ihn aus der Liste {@link #availableNodes}. + * Sollte kein Knoten gefunden werden, wird null zurückgegeben. + * Verbindliche Anforderung: Verwenden Sie beim Durchlaufen der Liste Iteratoren + * @return Der nächste abzuarbeitende Knoten oder null + */ + private AlgorithmNode getSmallestNode() { + // TODO: GraphAlgorithm#getSmallestNode() + return null; + } + + /** + * Diese Methode startet den Algorithmus. Dieser funktioniert wie folgt: + * 1. Suche den Knoten mit dem geringsten Wert (siehe {@link #getSmallestNode()}) + * 2. Für jede angrenzende Kante: + * 2a. Überprüfe ob die Kante passierbar ist ({@link #isPassable(Edge)}) + * 2b. Berechne den Wert des Knotens, in dem du den aktuellen Wert des Knotens und den der Kante addierst + * 2c. Ist der alte Wert nicht gesetzt (-1) oder ist der neue Wert kleiner, setze den neuen Wert und den Vorgängerknoten + * 3. Wiederhole solange, bis alle Knoten abgearbeitet wurden + + * Nützliche Methoden: + * @see #getSmallestNode() + * @see #isPassable(Edge) + * @see Graph#getEdges(Node) + * @see Edge#getOtherNode(Node) + */ + public void run() { + // TODO: GraphAlgorithm#run() + } + + /** + * Diese Methode gibt eine Liste von Kanten zurück, die einen Pfad zu dem angegebenen Zielknoten representiert. + * Dabei werden zuerst beginnend mit dem Zielknoten alle Kanten mithilfe des Vorgängerattributs {@link AlgorithmNode#previous} zu der Liste hinzugefügt. + * Zum Schluss muss die Liste nur noch umgedreht werden. Sollte kein Pfad existieren, geben Sie null zurück. + * @param destination Der Zielknoten des Pfads + * @return eine Liste von Kanten oder null + */ + public List> getPath(Node destination) { + // TODO: GraphAlgorithm#getPath(Node) + return null; + } + + /** + * Gibt den betrachteten Graphen zurück + * @return der zu betrachtende Graph + */ + protected Graph getGraph() { + return this.graph; + } + + /** + * Gibt den Wert einer Kante zurück. + * Diese Methode ist abstrakt und wird in den implementierenden Klassen definiert um eigene Kriterien für Werte zu ermöglichen. + * @param edge Eine Kante + * @return Ein Wert, der der Kante zugewiesen wird + */ + protected abstract double getValue(Edge edge); + + /** + * Gibt an, ob eine Kante passierbar ist. + * @param edge Eine Kante + * @return true, wenn die Kante passierbar ist. + */ + protected abstract boolean isPassable(Edge edge); +} diff --git a/Projektgruppe_XXX/src/base/Node.java b/Projektgruppe_XXX/src/base/Node.java new file mode 100644 index 0000000..4a8a733 --- /dev/null +++ b/Projektgruppe_XXX/src/base/Node.java @@ -0,0 +1,26 @@ +package base; + +/** + * Diese Klasse representiert einen generischen Knoten + * @param + */ +public class Node { + + private T value; + + /** + * Erzeugt einen neuen Knoten mit dem gegebenen Wert + * @param value der Wert des Knotens + */ + Node(T value) { + this.value = value; + } + + /** + * Gibt den Wert des Knotens zurück + * @return der Wert des Knotens + */ + public T getValue() { + return value; + } +} diff --git a/Projektgruppe_XXX/src/base/PerlinNoise.java b/Projektgruppe_XXX/src/base/PerlinNoise.java new file mode 100644 index 0000000..9b3c840 --- /dev/null +++ b/Projektgruppe_XXX/src/base/PerlinNoise.java @@ -0,0 +1,153 @@ +package base; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Random; + +/** + * @author Philipp Imperatori, Nils Nedderhut, Louis Neumann + * modified by Roman Hergenreder + *

+ * Version 1.0 + */ +public class PerlinNoise { + + private int width ; + private int height; + private int scale; + + private int gwidth; // scaled width + private int gheight; // scaled height + + private Random random; + private ArrayList> Vectors; //gradients + + public PerlinNoise(int width, int height, int scale) { + + this.width = width * scale; + this.height = height * scale; + this.scale = scale; + this.gwidth = width; + this.gheight = height; + + this.random = new Random(); + this.Vectors = new ArrayList<>(); + createVectors((1 + gwidth) * (1 + gheight)); + } + + public Dimension getRealSize() { + return new Dimension(this.width, this.height); + } + + public Dimension getScaledSize() { + return new Dimension(this.gwidth, this.gheight); + } + + /** + * create's a list with n elements where each element is a vector in the union circle (gradient) + * @param n: number of gradients to be created + */ + private void createVectors(int n) { + this.Vectors = new ArrayList<>(); + for(int i=0;i grad = new Vector<>(x,y); + this.Vectors.add(grad); + } + } + + /** + * Uses a Sigmoid function to smooth numbers betwenn 0 to 1 + * @param t: number to smooth + * @return smoothed number + */ + private double fade(double t){ + return (((6*(t*t*t*t*t))-(15*t*t*t*t))+(10*t*t*t)); + } + + /** Computes a weighted linear interpolation with point (x,y) and weight w + * @param x: value of x-coordinate + * @param y: value of y-coordinate + * @param w: weight w to interpolate + * @return result of linear interpolation + */ + private double linearInterpolation(double x, double y, double w){ + return ((1.0-w)*x+w*y); + } + + /** + * Calculates the scalar product between the direction vector and the gradient in the corner + * @param vector: direction vector + * @param gradient: gradient in the corner + * @return scalar product + */ + private double scalarVekGrad(Vector vector, Vector gradient){ + return (vector.getX()*gradient.getX() + vector.getY()*gradient.getY()); + } + + /** + * Converts the value from its old interval [oldMin,oldMax] to a new interval [newMin,newMax] + * @param value : value to be converted + * @return converted value + */ + private double mapToInterval(double value) { + return (value + 1.0)/2.0; + } + + + /** + * Creates the noise value for the given point (x,y) + * @param x: x-coordinate of point + * @param y: y-coordinate of point + * @return noise of point + */ + public double getNoise (double x, double y) { + if (x >= width || y >= height) { + System.out.println("ERROR: x or/and y is not in picture"); + return 0; + } + + double scaledX = x / this.scale; + double scaledY = y / this.scale; + + //Left upper edge + int xlo = (int) scaledX; + int ylo = (int) scaledY; + + //right upper egde + int xro = ((int) scaledX)+1; + int yro = ((int) scaledY); + + //left lower edge + int xlu = (int) scaledX; + int ylu = ((int) scaledY)+1; + + //right lower edge + int xru = ((int) scaledX)+1; + int yru = ((int) scaledY)+1; + + // int cell_nr = xlo+(ylo*this.gwidth); + + Vector gradLO = this.Vectors.get(xlo+(ylo*(this.gwidth+1))); + Vector gradRO = this.Vectors.get(xro+(yro*(this.gwidth+1))); + Vector gradLU = this.Vectors.get(xlu+((ylu)*(this.gwidth+1))); + Vector gradRU = this.Vectors.get(xru+((yru)*(this.gwidth+1))); + + Vector rvLO = new Vector<>(scaledX - xlo,scaledY - ylo); + Vector rvRO = new Vector<>(scaledX - xro,scaledY - yro); + Vector rvLU = new Vector<>(scaledX - xlu,scaledY - ylu); + Vector rvRU = new Vector<>(scaledX - xru,scaledY - yru); + + //upper edges + double linIntOben = this.linearInterpolation(this.scalarVekGrad(rvLO,gradLO),this.scalarVekGrad(rvRO,gradRO),this.fade(rvLO.getX())); + + //lower edges + double linIntUnten = this.linearInterpolation(this.scalarVekGrad(rvLU,gradLU),this.scalarVekGrad(rvRU,gradRU),this.fade(rvLO.getX())); + + //final interpolation + return this.mapToInterval(this.linearInterpolation(linIntOben,linIntUnten,this.fade(rvLO.getY()))); + } +} diff --git a/Projektgruppe_XXX/src/base/Vector.java b/Projektgruppe_XXX/src/base/Vector.java new file mode 100644 index 0000000..fea3400 --- /dev/null +++ b/Projektgruppe_XXX/src/base/Vector.java @@ -0,0 +1,33 @@ +package base; + +/** + * @author Philipp Imperatori, Nils Nedderhut, Louis Neumann + *

+ * Version 1.0 + */ +public class Vector { + + private T x; + private T y; + + public Vector(T x, T y) { + this.x = x; + this.y = y; + } + + public T getX() { + return this.x; + } + + public T getY() { + return this.y; + } + + public void setX(T x) { + this.x = x; + } + + public void setY(T y) { + this.y = y; + } +} \ No newline at end of file diff --git a/Projektgruppe_XXX/src/dice3d/main/MainWindow.java b/Projektgruppe_XXX/src/dice3d/main/MainWindow.java new file mode 100644 index 0000000..60cc63e --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/main/MainWindow.java @@ -0,0 +1,85 @@ +package dice3d.main; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import dice3d.models.cuboids.Cuboid; +import dice3d.models.cuboids.Dice; + + +public class MainWindow extends JPanel { + World w = new World(); + + + public MainWindow() { + addKeyListener(new KeyHandler()); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + draw((Graphics2D) g); + } + + + + public void draw(Graphics2D g) { + for(Cuboid c : w.cuboids) { + c.draw(g); + } + int yOffset = 0; + g.drawString("Press space key to roll again", 10, yOffset =+ 20); + + for(int i = 0; i < w.cuboids.size(); i++) { + Cuboid c = w.cuboids.get(i); + if(c instanceof Dice) { + Dice d = (Dice) c; + g.drawString("dice[" + i + "]: moving=" + !d.notMoving() + " number_rolled=" + d.getNumberRolled(), 10, yOffset += 20); + } else { + g.drawString("cuboid[" + i + "]: moving=" + !c.notMoving(), 10, yOffset += 20); + } + + } + + + repaint(); + } + + private class KeyHandler extends KeyAdapter { + @Override + public void keyPressed(KeyEvent e) { + if (e.getID() != KeyEvent.KEY_PRESSED) { + return; + } + if (e.getKeyCode() == KeyEvent.VK_SPACE) { + for(Cuboid c : w.cuboids) { + c.reset(); + } + } + + } + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + MainWindow view = new MainWindow(); + JFrame frame = new JFrame(); + frame.setTitle("Dice"); + frame.getContentPane().add(view); + frame.setSize(1280, 720); + frame.setLocationRelativeTo(null); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + // frame.setResizable(false); + frame.setVisible(true); + view.requestFocus(); + } + }); + } + +} diff --git a/Projektgruppe_XXX/src/dice3d/main/World.java b/Projektgruppe_XXX/src/dice3d/main/World.java new file mode 100644 index 0000000..0b28c27 --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/main/World.java @@ -0,0 +1,43 @@ +package dice3d.main; + +import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; + +import dice3d.models.cuboids.Cuboid; +import dice3d.models.cuboids.Dice; + +public class World { + + public static final double projectionDistance = 1000; + + public ArrayList cuboids; + public Cuboid floor; + + public World() { + cuboids = new ArrayList(); + + floor = new Cuboid(10, 400, 600, 700, 1000, 10); + cuboids.add(floor); + + cuboids.add(new Dice(150, 100, 800, 80)); + cuboids.add(new Dice(50, 120, 800, 80)); + + long delayInMS = 500; // start updating after 500ms + long intervalInMS = 15; // update every 15ms + + new Timer().scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + for(Cuboid c : cuboids) { + if(c == floor) { + continue; + } + c.update(); + } + } + }, delayInMS, intervalInMS); + + } + +} diff --git a/Projektgruppe_XXX/src/dice3d/math/Vector.java b/Projektgruppe_XXX/src/dice3d/math/Vector.java new file mode 100644 index 0000000..adfe438 --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/math/Vector.java @@ -0,0 +1,75 @@ +package dice3d.math; + +public class Vector { + + public double x; + public double y; + public double z; + + public Vector() { + } + + public Vector(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector(Vector v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public void set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void set(Vector v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public void add(Vector v) { + this.x += v.x; + this.y += v.y; + this.z += v.z; + } + + public void sub(Vector v) { + this.x -= v.x; + this.y -= v.y; + this.z -= v.z; + } + + public void scale(double s) { + scale(s, s, s); + } + + public void scale(double sx, double sy, double sz) { + this.x *= sx; + this.y *= sy; + this.z *= sz; + } + + public double getSize() { + return Math.sqrt(x * x + y * y + z * z); + } + + public void normalize() { + scale(1 / getSize()); + } + + @Override + public String toString() { + return "[" + x + ", " + y + ", " + z + "]"; + } + + public boolean equals(Vector v) { + return v.x == x && v.y == y && v.z == z; + } + +} diff --git a/Projektgruppe_XXX/src/dice3d/models/Edge.java b/Projektgruppe_XXX/src/dice3d/models/Edge.java new file mode 100644 index 0000000..320d4dc --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/models/Edge.java @@ -0,0 +1,31 @@ +package dice3d.models; + +import java.awt.Graphics2D; + +import dice3d.main.World; + +public class Edge { + + private Vertex a; + private Vertex b; + + + public Edge(Vertex a, Vertex b) { + this.a = a; + this.b = b; + } + + public void update() { + + } + + public void draw(Graphics2D g) { + double ax = World.projectionDistance * a.position.x / a.position.z; + double ay = World.projectionDistance * a.position.y / a.position.z; + double bx = World.projectionDistance * b.position.x / b.position.z; + double by = World.projectionDistance * b.position.y / b.position.z; + + g.drawLine((int) ax, (int) ay, (int) bx, (int) by); + } + +} diff --git a/Projektgruppe_XXX/src/dice3d/models/Vertex.java b/Projektgruppe_XXX/src/dice3d/models/Vertex.java new file mode 100644 index 0000000..6c98542 --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/models/Vertex.java @@ -0,0 +1,40 @@ +package dice3d.models; + +import java.awt.Graphics2D; + +import dice3d.main.World; +import dice3d.math.Vector; + +public class Vertex { + + public Vector position = new Vector(); + public Vector positionReset = new Vector(); + + public String id; + + public Vertex(double x, double y, double z) { + position.set(x, y, z); + positionReset.set(position); + } + + + public void reset() { + position.set(positionReset); + } + + public void update() { + + } + + public void draw(Graphics2D g) { + double px = World.projectionDistance * position.x / position.z; + double py = World.projectionDistance * position.y / position.z; + if (id == null) { + int size = 6; + g.fillOval((int) (px - (size / 2)), (int) (py - (size / 2)), size, size); + } else { + g.drawString(id, (int)px, (int)py); + } + } + +} diff --git a/Projektgruppe_XXX/src/dice3d/models/cuboids/Cube.java b/Projektgruppe_XXX/src/dice3d/models/cuboids/Cube.java new file mode 100644 index 0000000..e2d8847 --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/models/cuboids/Cube.java @@ -0,0 +1,9 @@ +package dice3d.models.cuboids; + +public class Cube extends Cuboid { + + public Cube(double x, double y, double z, int size) { + super(x, y, z, size, size, size); + } + +} diff --git a/Projektgruppe_XXX/src/dice3d/models/cuboids/Cuboid.java b/Projektgruppe_XXX/src/dice3d/models/cuboids/Cuboid.java new file mode 100644 index 0000000..b8dd8f2 --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/models/cuboids/Cuboid.java @@ -0,0 +1,147 @@ +package dice3d.models.cuboids; +import java.awt.Color; +import java.awt.Graphics2D; +import java.util.ArrayList; + +import dice3d.math.Vector; +import dice3d.models.Edge; +import dice3d.models.Vertex; + +public class Cuboid { + + public ArrayList vertices = new ArrayList(); + public ArrayList edges = new ArrayList(); + + + public boolean collided = false; + + public Cuboid(double x, double y, double z, int length, int width, int height) { + + Vector corner = new Vector(x, y, z); + + Vertex a = new Vertex(corner.x, corner.y, corner.z); + Vertex b = new Vertex(corner.x + length, corner.y, corner.z); + Vertex c = new Vertex(corner.x + length, corner.y + height, corner.z); + Vertex d = new Vertex(corner.x, corner.y + height, corner.z); + + Vertex e = new Vertex(corner.x, corner.y, corner.z + width); + Vertex f = new Vertex(corner.x + length, corner.y, corner.z + width); + Vertex g = new Vertex(corner.x + length, corner.y + height, corner.z + width); + Vertex h = new Vertex(corner.x, corner.y + height, corner.z + width); + + a.id = "A"; + b.id = "B"; + c.id = "C"; + d.id = "D"; + e.id = "E"; + f.id = "F"; + g.id = "G"; + h.id = "H"; + + vertices.add(a); + vertices.add(b); + vertices.add(c); + vertices.add(d); + + vertices.add(e); + vertices.add(f); + vertices.add(g); + vertices.add(h); + + Edge edge01 = new Edge(a, b); + Edge edge02 = new Edge(b, c); + Edge edge03 = new Edge(c, d); + Edge edge04 = new Edge(d, a); + + Edge edge05 = new Edge(e, f); + Edge edge06 = new Edge(f, g); + Edge edge07 = new Edge(g, h); + Edge edge08 = new Edge(h, e); + + Edge edge09 = new Edge(a, e); + Edge edge10 = new Edge(b, f); + Edge edge11 = new Edge(c, g); + Edge edge12 = new Edge(d, h); + + Edge edge13 = new Edge(a, g); + Edge edge14 = new Edge(b, h); + Edge edge15 = new Edge(c, e); + Edge edge16 = new Edge(d, f); + + Edge edge17 = new Edge(a, c); + Edge edge18 = new Edge(b, g); + Edge edge19 = new Edge(f, h); + Edge edge20 = new Edge(d, e); + Edge edge21 = new Edge(d, g); + Edge edge22 = new Edge(a, f); + + edges.add(edge01); + edges.add(edge02); + edges.add(edge03); + edges.add(edge04); + edges.add(edge05); + edges.add(edge06); + edges.add(edge07); + edges.add(edge08); + edges.add(edge09); + edges.add(edge10); + edges.add(edge11); + edges.add(edge12); + + edges.add(edge13); + edges.add(edge14); + edges.add(edge15); + edges.add(edge16); + + edges.add(edge17); + edges.add(edge18); + edges.add(edge19); + edges.add(edge20); + edges.add(edge21); + edges.add(edge22); + + } + + public void update() { + for (Vertex v : vertices) { + v.update(); + } + for (Edge e : edges) { + e.update(); + } + + } + + public void reset() { + for (Vertex v : vertices) { + v.reset(); + } + } + + public void draw(Graphics2D g) { + // draw all edges, even inner ones + g.setColor(Color.GREEN); + + if(collided) { + g.setColor(Color.RED); + } + for(Edge e: edges) { + e.draw(g); + } + + g.setColor(Color.BLACK); + for (Vertex point : vertices) { + point.draw(g); + } + } + + public boolean notMoving() { + return false; + } + + public void updateCollision(Cuboid d) { + collided = false; + } + + +} diff --git a/Projektgruppe_XXX/src/dice3d/models/cuboids/Dice.java b/Projektgruppe_XXX/src/dice3d/models/cuboids/Dice.java new file mode 100644 index 0000000..a0de00a --- /dev/null +++ b/Projektgruppe_XXX/src/dice3d/models/cuboids/Dice.java @@ -0,0 +1,111 @@ +package dice3d.models.cuboids; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; + +import dice3d.main.World; +import dice3d.models.Vertex; + +public class Dice extends Cube { + + + private Vertex[][] face = new Vertex[6][4]; + + + public Dice(double x, double y, double z, int size) { + super(x,y,z, size); + + face[0][0] = vertices.get(0); // a + face[0][1] = vertices.get(1); // b + face[0][2] = vertices.get(2); // c ... + face[0][3] = vertices.get(3); + + face[1][0] = vertices.get(1); + face[1][1] = vertices.get(5); + face[1][2] = vertices.get(6); + face[1][3] = vertices.get(2); + + face[2][0] = vertices.get(5); + face[2][1] = vertices.get(4); + face[2][2] = vertices.get(7); + face[2][3] = vertices.get(6); + + face[3][0] = vertices.get(4); + face[3][1] = vertices.get(0); + face[3][2] = vertices.get(3); + face[3][3] = vertices.get(7); + + face[4][0] = vertices.get(3); + face[4][1] = vertices.get(2); + face[4][2] = vertices.get(6); + face[4][3] = vertices.get(7); + + face[5][0] = vertices.get(1); + face[5][1] = vertices.get(0); + face[5][2] = vertices.get(4); + face[5][3] = vertices.get(5); + } + + + public void draw(Graphics2D g) { + drawDice(g); + // draw all edges, even inner ones + // for(Edge e: edges) { + // e.draw(g); + // } + + // for (Vertex point : vertices) { + // point.draw(g); + // } + } + + public int getNumberRolled() { + return 1; + } + + private void drawDice(Graphics2D g) { + for (int f = 0; f < 6; f++) { + // each face as polygon + Polygon polygon = new Polygon(); + + for (int v = 0; v < 4; v++) { + Vertex vertex = face[f][v]; + double vx = World.projectionDistance * vertex.position.x / vertex.position.z; + double vy = World.projectionDistance * vertex.position.y / vertex.position.z; + + polygon.addPoint((int) vx, (int) vy); + } + + // back-face culling + double v1x = polygon.xpoints[0] - polygon.xpoints[1]; + double v1y = polygon.ypoints[0] - polygon.ypoints[1]; + double v2x = polygon.xpoints[2] - polygon.xpoints[1]; + double v2y = polygon.ypoints[2] - polygon.ypoints[1]; + double visible = v1x * v2y - v1y * v2x; + if (visible < 0) { + g.setColor(Color.WHITE); + if(collided) { + g.setColor(Color.RED); + } + g.fill(polygon); + g.setColor(Color.BLACK); + g.draw(polygon); + + // draw number of each face (that is visible) + // calc center of each face/square + double xs = 0d; + double ys = 0d; + + for (int i = 0; i < 4; i++) { + xs += polygon.xpoints[i]; + ys += polygon.ypoints[i]; + } + xs = xs / 4; + ys = ys / 4; + g.drawString(f + 1 + "", (int) xs, (int) ys); + } + } + + } + +} diff --git a/Projektgruppe_XXX/src/game/AI.java b/Projektgruppe_XXX/src/game/AI.java new file mode 100644 index 0000000..af3cda9 --- /dev/null +++ b/Projektgruppe_XXX/src/game/AI.java @@ -0,0 +1,72 @@ +package game; + +import java.awt.Color; +import java.util.Random; + +public abstract class AI extends Player { + + private AIThread aiThread; + private Random random; + protected boolean fastForward; + + public AI(String name, Color color) { + super(name, color); + this.random = new Random(); + } + + protected Random getRandom() { + return this.random; + } + + protected abstract void actions(Game game) throws InterruptedException; + + public void doNextTurn(Game game) { + if(aiThread != null) + return; + + fastForward = false; + aiThread = new AIThread(game); + aiThread.start(); + } + + public void fastForward() { + if(aiThread != null) + fastForward = true; + } + + protected void sleep(int ms) throws InterruptedException { + long end = System.currentTimeMillis() + ms; + while(System.currentTimeMillis() < end && !fastForward) { + Thread.sleep(10); + } + } + + private class AIThread extends Thread { + + private Game game; + + private AIThread(Game game) { + this.game = game; + } + + private void finishTurn() { + aiThread = null; + fastForward = false; + + // Trigger next round, if not automatically + if(game.getRound() > 1 && game.getCurrentPlayer() == AI.this) + game.nextTurn(); + } + + @Override + public void run() { + try { + actions(game); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + finishTurn(); + } + } +} diff --git a/Projektgruppe_XXX/src/game/Game.java b/Projektgruppe_XXX/src/game/Game.java new file mode 100644 index 0000000..a675573 --- /dev/null +++ b/Projektgruppe_XXX/src/game/Game.java @@ -0,0 +1,293 @@ +package game; + +import java.util.*; + +import game.map.Castle; +import game.map.Kingdom; +import game.map.GameMap; +import game.map.MapSize; +import gui.AttackThread; +import gui.Resources; + +public class Game { + + private Goal goal; + private List players; + private boolean isOver; + private boolean hasStarted; + private int round; + private MapSize mapSize; + private GameMap gameMap; + private Queue playerQueue; + private Player startingPlayer; + private Player currentPlayer; + private GameInterface gameInterface; + private AttackThread attackThread; + + public Game() { + this.isOver = false; + this.hasStarted = false; + this.mapSize = MapSize.MEDIUM; + this.players = new LinkedList<>(); + } + + public void addPlayer(Player p) { + if(players.contains(p)) + throw new IllegalArgumentException("Spieler wurde bereits hinzugefügt"); + + this.players.add(p); + } + + public void setGoal(Goal goal) { + this.goal = goal; + this.goal.setGame(this); + } + + public int getRound() { + return round; + } + + public void setMapSize(MapSize mapSize) { + this.mapSize = mapSize; + } + + private void generateMap() { + + int mapSizeMultiplier = this.mapSize.ordinal() + 1; + int playerCount = players.size(); + int numRegions = playerCount * GameConstants.CASTLES_NUMBER_MULTIPLIER * mapSizeMultiplier; + double tileMultiplier = 1.0 + (mapSizeMultiplier * 0.3); + + // We set up space for 2 times the region count + int numTiles = (int) Math.ceil(numRegions * tileMultiplier); + + // Our map should be 3:2 + int width = (int) Math.ceil(0.6 * numTiles); + int height = (int) Math.ceil(0.4 * numTiles); + + int continents = Math.min(3, playerCount + this.mapSize.ordinal()); + + this.gameMap = GameMap.generateRandomMap(width, height, 40, numRegions, continents); + } + + public void start(GameInterface gameInterface) { + + if(hasStarted) + throw new IllegalArgumentException("Spiel wurde bereits gestartet"); + + if(players.size() < 2) + throw new IllegalArgumentException("Nicht genug Spieler"); + + if(goal == null) + throw new IllegalArgumentException("Kein Spielziel gesetzt"); + + this.generateMap(); + + // Create random player order + this.gameInterface = gameInterface; + List tempList = new ArrayList<>(players); + playerQueue = new ArrayDeque<>(); + while(!tempList.isEmpty()) { + Player player = tempList.remove((int) (Math.random() * tempList.size())); + player.reset(); + playerQueue.add(player); + } + + startingPlayer = playerQueue.peek(); + hasStarted = true; + isOver = false; + round = 0; + + gameInterface.onGameStarted(this); + nextTurn(); + } + + public AttackThread startAttack(Castle source, Castle target, int troopCount) { + if(attackThread != null) + return attackThread; + + if(source.getOwner() == target.getOwner() || troopCount < 1) + return null; + + attackThread = new AttackThread(this, source, target, troopCount); + attackThread.start(); + gameInterface.onAttackStarted(source, target, troopCount); + return attackThread; + } + + public void doAttack(Castle attackerCastle, Castle defenderCastle, int[] rollAttacker, int[] rollDefender) { + + Integer[] rollAttackerSorted = Arrays.stream(rollAttacker).boxed().sorted(Comparator.reverseOrder()).toArray(Integer[]::new); + Integer[] rollDefenderSorted = Arrays.stream(rollDefender).boxed().sorted(Comparator.reverseOrder()).toArray(Integer[]::new); + + Player attacker = attackerCastle.getOwner(); + Player defender = defenderCastle.getOwner(); + + for(int i = 0; i < Math.min(rollAttacker.length, rollDefender.length); i++) { + if(rollAttackerSorted[i] > rollDefenderSorted[i]) { + defenderCastle.removeTroops(1); + if(defenderCastle.getTroopCount() == 0) { + attackerCastle.removeTroops(1); + defenderCastle.setOwner(attacker); + defenderCastle.addTroops(1); + gameInterface.onConquer(defenderCastle, attacker); + addScore(attacker, 50); + break; + } else { + addScore(attacker, 20); + } + } else { + attackerCastle.removeTroops(1); + addScore(defender, 30); + } + } + + gameInterface.onUpdate(); + } + + public void moveTroops(Castle source, Castle destination, int troopCount) { + if(troopCount >= source.getTroopCount() || source.getOwner() != destination.getOwner()) + return; + + source.moveTroops(destination, troopCount); + gameInterface.onUpdate(); + } + + public void stopAttack() { + this.attackThread = null; + this.gameInterface.onAttackStopped(); + } + + public int[] roll(Player player, int dices, boolean fastForward) { + return gameInterface.onRoll(player, dices, fastForward); + } + + private boolean allCastlesChosen() { + return gameMap.getCastles().stream().noneMatch(c -> c.getOwner() == null); + } + + public AttackThread getAttackThread() { + return this.attackThread; + } + + public void chooseCastle(Castle castle, Player player) { + if(castle.getOwner() != null || player.getRemainingTroops() == 0) + return; + + gameInterface.onCastleChosen(castle, player); + player.removeTroops(1); + castle.setOwner(currentPlayer); + castle.addTroops(1); + addScore(player, 5); + + if(player.getRemainingTroops() == 0 || allCastlesChosen()) { + player.removeTroops(player.getRemainingTroops()); + nextTurn(); + } + } + + public void addTroops(Player player, Castle castle, int count) { + if(count < 1 || castle.getOwner() != player) + return; + + count = Math.min(count, player.getRemainingTroops()); + castle.addTroops(count); + player.removeTroops(count); + } + + public void addScore(Player player, int score) { + player.addPoints(score); + gameInterface.onAddScore(player, score); + } + + public void endGame() { + isOver = true; + Player winner = goal.getWinner(); + + if(winner != null) + addScore(goal.getWinner(), 150); + + Resources resources = Resources.getInstance(); + for(Player player : players) { + resources.addScoreEntry(new ScoreEntry(player, goal)); + } + + gameInterface.onGameOver(winner); + } + + public void nextTurn() { + + if(goal.isCompleted()) { + endGame(); + return; + } + + // Choose next player + Player nextPlayer; + do { + nextPlayer = playerQueue.remove(); + + // if player has already lost, remove him from queue + if(goal.hasLost(nextPlayer)) { + if(startingPlayer == nextPlayer) { + startingPlayer = playerQueue.peek(); + } + nextPlayer = null; + } + } while(nextPlayer == null && !playerQueue.isEmpty()); + + if(nextPlayer == null) { + isOver = true; + gameInterface.onGameOver(goal.getWinner()); + return; + } + + currentPlayer = nextPlayer; + if(round == 0 || (round == 1 && allCastlesChosen()) || (round > 1 && currentPlayer == startingPlayer)) { + round++; + gameInterface.onNewRound(round); + } + + int numRegions = currentPlayer.getNumRegions(this); + + int addTroops; + if(round == 1) + addTroops = GameConstants.CASTLES_AT_BEGINNING; + else { + addTroops = Math.max(3, numRegions / GameConstants.TROOPS_PER_ROUND_DIVISOR); + addScore(currentPlayer, addTroops * 5); + + for(Kingdom kingdom : gameMap.getKingdoms()) { + if(kingdom.getOwner() == currentPlayer) { + addScore(currentPlayer, 10); + addTroops++; + } + } + } + + currentPlayer.addTroops(addTroops); + boolean isAI = (currentPlayer instanceof AI); + gameInterface.onNextTurn(currentPlayer, addTroops, !isAI); + if(isAI) { + ((AI)currentPlayer).doNextTurn(this); + } + + playerQueue.add(currentPlayer); + } + + public Player getCurrentPlayer() { + return this.currentPlayer; + } + + public List getPlayers() { + return this.players; + } + + public GameMap getMap() { + return this.gameMap; + } + + public boolean isOver() { + return this.isOver; + } +} diff --git a/Projektgruppe_XXX/src/game/GameConstants.java b/Projektgruppe_XXX/src/game/GameConstants.java new file mode 100644 index 0000000..f963fbf --- /dev/null +++ b/Projektgruppe_XXX/src/game/GameConstants.java @@ -0,0 +1,41 @@ +package game; + +import game.goals.*; +import game.players.*; + +import java.awt.*; + +public class GameConstants { + + public static final int MAX_PLAYERS = 4; + + // Determines how many regions are generated per player, + // e.g. PlayerCount * 7 for Small, PlayerCount * 14 for Medium and PlayerCount * 21 for Large Maps + public static final int CASTLES_NUMBER_MULTIPLIER = 7; + public static final int CASTLES_AT_BEGINNING = 3; + public static final int TROOPS_PER_ROUND_DIVISOR = 3; + + public static final Color COLOR_WATER = Color.BLUE; + public static final Color COLOR_SAND = new Color(210, 170, 109); + public static final Color COLOR_GRASS = new Color(50, 89, 40); + public static final Color COLOR_STONE = Color.GRAY; + public static final Color COLOR_SNOW = Color.WHITE; + + public static final Color PLAYER_COLORS[] = { + Color.CYAN, + Color.RED, + Color.GREEN, + Color.ORANGE + }; + + public static final Goal GAME_GOALS[] = { + new ConquerGoal(), + // TODO: Add more Goals + }; + + public static final Class PLAYER_TYPES[] = { + Human.class, + BasicAI.class, + // TODO: Add more Player types, like different AIs + }; +} diff --git a/Projektgruppe_XXX/src/game/GameInterface.java b/Projektgruppe_XXX/src/game/GameInterface.java new file mode 100644 index 0000000..0ddb18e --- /dev/null +++ b/Projektgruppe_XXX/src/game/GameInterface.java @@ -0,0 +1,18 @@ +package game; + +import game.map.Castle; + +public interface GameInterface { + + void onAttackStopped(); + void onAttackStarted(Castle source, Castle target, int troopCount); + void onCastleChosen(Castle castle, Player player); + void onNextTurn(Player currentPlayer, int troopsGot, boolean human); + void onNewRound(int round); + void onGameOver(Player winner); + void onGameStarted(Game game); + void onConquer(Castle castle, Player player); + void onUpdate(); + void onAddScore(Player player, int score); + int[] onRoll(Player player, int dices, boolean fastForward); +} diff --git a/Projektgruppe_XXX/src/game/Goal.java b/Projektgruppe_XXX/src/game/Goal.java new file mode 100644 index 0000000..c6ad2dc --- /dev/null +++ b/Projektgruppe_XXX/src/game/Goal.java @@ -0,0 +1,33 @@ +package game; + +public abstract class Goal { + + private Game game; + private final String description; + private final String name; + + public Goal(String name, String description) { + this.name = name; + this.description = description; + } + + public void setGame(Game game) { + this.game = game; + } + + public abstract boolean isCompleted(); + public abstract Player getWinner(); + public abstract boolean hasLost(Player player); + + public final String getDescription() { + return this.description; + } + + public final String getName() { + return this.name; + } + + protected Game getGame() { + return this.game; + } +} diff --git a/Projektgruppe_XXX/src/game/Player.java b/Projektgruppe_XXX/src/game/Player.java new file mode 100644 index 0000000..8c9dee6 --- /dev/null +++ b/Projektgruppe_XXX/src/game/Player.java @@ -0,0 +1,88 @@ +package game; + +import game.map.Castle; + +import java.awt.Color; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.stream.Collectors; + +public abstract class Player { + + private final String name; + private Color color; + private int points; + private int remainingTroops; + + protected Player(String name, Color color) { + this.name = name; + this.points = 0; + this.color = color; + this.remainingTroops = 0; + } + + public int getRemainingTroops() { + return this.remainingTroops; + } + + public static Player createPlayer(Class playerType, String name, Color color) { + if(!Player.class.isAssignableFrom(playerType)) + throw new IllegalArgumentException("Not a player class"); + + try { + Constructor constructor = playerType.getConstructor(String.class, Color.class); + return (Player) constructor.newInstance(name, color); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } + } + + public void setColor(Color c) { + this.color = c; + } + + public Color getColor() { + return this.color; + } + + public String getName() { + return this.name; + } + + public int getPoints() { + return points; + } + + public void addPoints(int points) { + this.points += points; + } + + public void addTroops(int troops) { + if(troops < 0) + return; + + this.remainingTroops += troops; + } + + public void removeTroops(int troops) { + if(this.remainingTroops - troops < 0 || troops < 0) + return; + + this.remainingTroops -= troops; + } + + public int getNumRegions(Game game) { + return this.getCastles(game).size(); + } + + public List getCastles(Game game) { + return game.getMap().getCastles().stream().filter(c -> c.getOwner() == this).collect(Collectors.toList()); + } + + public void reset() { + this.remainingTroops = 0; + this.points = 0; + } +} diff --git a/Projektgruppe_XXX/src/game/ScoreEntry.java b/Projektgruppe_XXX/src/game/ScoreEntry.java new file mode 100644 index 0000000..556c929 --- /dev/null +++ b/Projektgruppe_XXX/src/game/ScoreEntry.java @@ -0,0 +1,95 @@ +package game; + +import java.io.PrintWriter; +import java.util.Date; + +/** + * Diese Klasse stellt einen Eintrag in der Bestenliste dar. + * Sie enthält den Namen des Spielers, das Datum, die erreichte Punktzahl sowie den Spieltypen. + */ +public class ScoreEntry implements Comparable { + + private String name; + private Date date; + private int score; + private String gameType; + + /** + * Erzeugt ein neues ScoreEntry-Objekt + * @param name der Name des Spielers + * @param score die erreichte Punktzahl + * @param date das Datum + * @param gameGoal der Spieltyp + */ + private ScoreEntry(String name, int score, Date date, String gameGoal) { + this.name = name; + this.score = score; + this.date = date; + this.gameType = gameGoal; + } + + /** + * Erzeugt ein neues ScoreEntry-Objekt + * @param player der Spieler + * @param gameGoal der Spieltyp + */ + public ScoreEntry(Player player, Goal gameGoal) { + this.name = player.getName(); + this.score = player.getPoints(); + this.date = new Date(); + this.gameType = gameGoal.getName(); + } + + @Override + public int compareTo(ScoreEntry scoreEntry) { + return Integer.compare(this.score, scoreEntry.score); + } + + /** + * Schreibt den Eintrag als neue Zeile mit dem gegebenen {@link PrintWriter} + * Der Eintrag sollte im richtigen Format gespeichert werden. + * @see #read(String) + * @see Date#getTime() + * @param printWriter der PrintWriter, mit dem der Eintrag geschrieben wird + */ + public void write(PrintWriter printWriter) { + // TODO: ScoreEntry#write(PrintWriter) + } + + /** + * List eine gegebene Zeile ein und wandelt dies in ein ScoreEntry-Objekt um. + * Ist das Format der Zeile ungültig oder enthält es ungültige Daten, wird null zurückgegeben. + * Eine gültige Zeile enthält in der Reihenfolge durch Semikolon getrennt: + * den Namen, das Datum als Unix-Timestamp (in Millisekunden), die erreichte Punktzahl, den Spieltypen + * Gültig wäre beispielsweise: "Florian;1546947397000;100;Eroberung" + * + * + * @see String#split(String) + * @see Long#parseLong(String) + * @see Integer#parseInt(String) + * @see Date#Date(long) + * + * @param line Die zu lesende Zeile + * @return Ein ScoreEntry-Objekt oder null + */ + public static ScoreEntry read(String line) { + // TODO: ScoreEntry#read(String) + return null; + } + + public Date getDate() { + return date; + } + + public String getName() { + return this.name; + } + + public int getScore() { + return this.score; + } + + public String getMode() { + return this.gameType; + } +} diff --git a/Projektgruppe_XXX/src/game/goals/ConquerGoal.java b/Projektgruppe_XXX/src/game/goals/ConquerGoal.java new file mode 100644 index 0000000..3b49b75 --- /dev/null +++ b/Projektgruppe_XXX/src/game/goals/ConquerGoal.java @@ -0,0 +1,45 @@ +package game.goals; + +import game.Game; +import game.Goal; +import game.Player; +import game.map.Castle; + +public class ConquerGoal extends Goal { + + public ConquerGoal() { + super("Eroberung", "Derjenige Spieler gewinnt, der als erstes alle Gebiete erobert hat."); + } + + @Override + public boolean isCompleted() { + return this.getWinner() != null; + } + + @Override + public Player getWinner() { + Game game = this.getGame(); + if(game.getRound() < 2) + return null; + + Player p = null; + for(Castle c : game.getMap().getCastles()) { + if(c.getOwner() == null) + return null; + else if(p == null) + p = c.getOwner(); + else if(p != c.getOwner()) + return null; + } + + return p; + } + + @Override + public boolean hasLost(Player player) { + if(getGame().getRound() < 2) + return false; + + return player.getNumRegions(getGame()) == 0; + } +} diff --git a/Projektgruppe_XXX/src/game/map/Castle.java b/Projektgruppe_XXX/src/game/map/Castle.java new file mode 100644 index 0000000..e25d9c9 --- /dev/null +++ b/Projektgruppe_XXX/src/game/map/Castle.java @@ -0,0 +1,128 @@ +package game.map; + +import game.Player; + +import java.awt.*; + +/** + * Diese Klasse representiert eine Burg. + * Jede Burg hat Koordinaten auf der Karte und einen Namen. + * Falls die Burg einen Besitzer hat, hat sie auch eine Anzahl von zugewiesenen Truppen. + * Die Burg kann weiterhin Teil eines Königreichs sein. + */ +public class Castle { + + private int troopCount; + private Player owner; + private Kingdom kingdom; + private Point location; + private String name; + + /** + * Eine neue Burg erstellen + * @param location die Koordinaten der Burg + * @param name der Name der Burg + */ + public Castle(Point location, String name) { + this.location = location; + this.troopCount = 0; + this.owner = null; + this.kingdom = null; + this.name = name; + } + + public Player getOwner() { + return this.owner; + } + + public Kingdom getKingdom() { + return this.kingdom; + } + + public int getTroopCount() { + return this.troopCount; + } + + /** + * Truppen von dieser Burg zur angegebenen Burg bewegen. + * Dies funktioniert nur, wenn die Besitzer übereinstimmen und bei der aktuellen Burg mindestens eine Truppe übrig bleibt + * @param target + * @param troops + */ + public void moveTroops(Castle target, int troops) { + + // Troops can only be moved to own regions + if(target.owner != this.owner) + return; + + // At least one unit must remain in the source region + if(this.troopCount - troops < 1) + return; + + this.troopCount -= troops; + target.troopCount += troops; + } + + public Point getLocationOnMap() { + return this.location; + } + + /** + * Berechnet die eukldische Distanz zu dem angegebenen Punkt + * @param dest die Zielkoordinate + * @return die euklidische Distanz + */ + public double distance(Point dest) { + return Math.sqrt(Math.pow(this.location.x - dest.x, 2) + Math.pow(this.location.y - dest.y, 2)); + } + + /** + * Berechnet die eukldische Distanz zu der angegebenen Burg + * @param next die Zielburg + * @return die euklidische Distanz + * @see #distance(Point) + */ + public double distance(Castle next) { + Point otherLocation = next.getLocationOnMap(); + return this.distance(otherLocation); + } + + public void setOwner(Player player) { + this.owner = player; + } + + public void addTroops(int i) { + if(i <= 0) + return; + + this.troopCount += i; + } + + public String getName() { + return this.name; + } + + public void removeTroops(int i) { + this.troopCount = Math.max(0, this.troopCount - i); + if(this.troopCount == 0) + this.owner = null; + } + + /** + * Gibt den Burg-Typen zurück. Falls die Burg einem Königreich angehört, wird der Typ des Königreichs zurückgegeben, ansonsten 0 + * @return der Burg-Typ für die Anzeige + */ + public int getType() { + return this.kingdom == null ? 0 : this.kingdom.getType(); + } + + /** + * Die Burg einem Königreich zuordnen + * @param kingdom Ein Königreich oder null + */ + public void setKingdom(Kingdom kingdom) { + this.kingdom = kingdom; + if(kingdom != null) + kingdom.addCastle(this); + } +} diff --git a/Projektgruppe_XXX/src/game/map/Clustering.java b/Projektgruppe_XXX/src/game/map/Clustering.java new file mode 100644 index 0000000..d27078e --- /dev/null +++ b/Projektgruppe_XXX/src/game/map/Clustering.java @@ -0,0 +1,40 @@ +package game.map; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * Diese Klasse teilt Burgen in Königreiche auf + */ +public class Clustering { + + private Random random; + private final List allCastles; + private final int kingdomCount; + + /** + * Ein neues Clustering-Objekt erzeugen. + * @param castles Die Liste von Burgen, die aufgeteilt werden sollen + * @param kingdomCount Die Anzahl von Königreichen die generiert werden sollen + */ + public Clustering(List castles, int kingdomCount) { + if (kingdomCount < 2) + throw new IllegalArgumentException("Ungültige Anzahl an Königreichen"); + + this.random = new Random(); + this.kingdomCount = kingdomCount; + this.allCastles = Collections.unmodifiableList(castles); + } + + /** + * Gibt eine Liste von Königreichen zurück. + * Jedes Königreich sollte dabei einen Index im Bereich 0-5 bekommen, damit die Burg richtig angezeigt werden kann. + * Siehe auch {@link Kingdom#getType()} + */ + public List getPointsClusters() { + // TODO Clustering#getPointsClusters() + return new ArrayList<>(); + } +} diff --git a/Projektgruppe_XXX/src/game/map/GameMap.java b/Projektgruppe_XXX/src/game/map/GameMap.java new file mode 100644 index 0000000..494b896 --- /dev/null +++ b/Projektgruppe_XXX/src/game/map/GameMap.java @@ -0,0 +1,256 @@ +package game.map; + +import base.*; +import game.GameConstants; +import gui.Resources; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +/** + * Diese Klasse representiert das Spielfeld. Sie beinhaltet das Hintergrundbild, welches mit Perlin noise erzeugt wurde, + * eine Liste mit Königreichen und alle Burgen und deren Verbindungen als Graphen. + * + * Die Karte wird in mehreren Schritten generiert, siehe dazu {@link #generateRandomMap(int, int, int, int, int)} + */ +public class GameMap { + + private BufferedImage backgroundImage; + private Graph castleGraph; + private List kingdoms; + + // Map Generation + private double[][] noiseValues; + private int width, height, scale; + + /** + * Erzeugt eine neue leere Karte. Der Konstruktor sollte niemals direkt aufgerufen werden. + * Um eine neue Karte zu erstellen, muss {@link #generateRandomMap(int, int, int, int, int)} verwendet werden + * @param width die Breite der Karte + * @param height die Höhe der Karte + * @param scale der Skalierungsfaktor + */ + private GameMap(int width, int height, int scale) { + this.castleGraph = new Graph<>(); + this.width = width; + this.height = height; + this.scale = scale; + } + + /** + * Wandelt einen Noise-Wert in eine Farbe um. Die Methode kann nach belieben angepasst werden + * @param value der Perlin-Noise-Wert + * @return die resultierende Farbe + */ + private Color doubleToColor(double value) { + if (value <= 0.40) + return GameConstants.COLOR_WATER; + else if (value <= 0.5) + return GameConstants.COLOR_SAND; + else if (value <= 0.7) + return GameConstants.COLOR_GRASS; + else if (value <= 0.8) + return GameConstants.COLOR_STONE; + else + return GameConstants.COLOR_SNOW; + } + + /** + * Hier wird das Hintergrund-Bild mittels Perlin-Noise erzeugt. + * Siehe auch: {@link PerlinNoise} + */ + private void generateBackground() { + PerlinNoise perlinNoise = new PerlinNoise(width, height, scale); + Dimension realSize = perlinNoise.getRealSize(); + + noiseValues = new double[realSize.width][realSize.height]; + backgroundImage = new BufferedImage(realSize.width, realSize.height, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < realSize.width; x++) { + for (int y = 0; y < realSize.height; y++) { + double noiseValue = perlinNoise.getNoise(x, y); + noiseValues[x][y] = noiseValue; + backgroundImage.setRGB(x, y, doubleToColor(noiseValue).getRGB()); + } + } + } + + /** + * Hier werden die Burgen erzeugt. + * Dabei wir die Karte in Felder unterteilt, sodass auf jedes Fals maximal eine Burg kommt. + * Sollte auf einem Feld keine Position für eine Burg existieren (z.B. aufgrund von Wasser oder angrenzenden Burgen), wird dieses übersprungen. + * Dadurch kann es vorkommen, dass nicht alle Burgen generiert werden + * @param castleCount die maximale Anzahl der zu generierenden Burgen + */ + private void generateCastles(int castleCount) { + double square = Math.ceil(Math.sqrt(castleCount)); + double length = width + height; + + int tilesX = (int) Math.max(1, (width / length + 0.5) * square) + 5; + int tilesY = (int) Math.max(1, (height / length + 0.5) * square) + 5; + int tileW = (width * scale / tilesX); + int tileH = (height * scale / tilesY); + + if (tilesX * tilesY < castleCount) { + throw new IllegalArgumentException(String.format("CALCULATION Error: tilesX=%d * tilesY=%d < castles=%d", tilesX, tilesY, castleCount)); + } + + // Add possible tiles + List possibleFields = new ArrayList<>(tilesX * tilesY); + for (int x = 0; x < tilesX - 1; x++) { + for (int y = 0; y < tilesY - 1; y++) { + possibleFields.add(new Point(x, y)); + } + } + + // Generate castles + List possibleNames = generateCastleNames(); + int castlesGenerated = 0; + while (possibleFields.size() > 0 && castlesGenerated < castleCount) { + Point randomField = possibleFields.remove((int) (Math.random() * possibleFields.size())); + int x0 = (int) ((randomField.x + 0.5) * tileW); + int y0 = (int) ((randomField.y + 0.5) * tileH); + + for (int x = (int) (0.5 * tileW); x >= 0; x--) { + boolean positionFound = false; + for (int y = (int) (0.5 * tileH); y >= 0; y--) { + int x_mid = (int) (x0 + x + 0.5 * tileW); + int y_mid = (int) (y0 + y + 0.5 * tileH); + if (noiseValues[x_mid][y_mid] >= 0.6) { + String name = possibleNames.isEmpty() ? "Burg " + (castlesGenerated + 1) : + possibleNames.get((int) (Math.random() * possibleNames.size())); + Castle newCastle = new Castle(new Point(x0 + x, y0 + y), name); + boolean doesIntersect = false; + + for (Castle r : castleGraph.getAllValues()) { + if (r.distance(newCastle) < Math.max(tileW, tileH)) { + doesIntersect = true; + break; + } + } + + if (!doesIntersect) { + possibleNames.remove(name); + castleGraph.addNode(newCastle); + castlesGenerated++; + positionFound = true; + break; + } + } + } + + if (positionFound) + break; + } + } + } + + /** + * Hier werden die Kanten erzeugt. Dazu werden zunächst alle Burgen durch eine Linie verbunden und anschließend + * jede Burg mit allen anderen in einem bestimmten Radius nochmals verbunden + */ + private void generateEdges() { + // TODO: GameMap#generateEdges() + } + + /** + * Hier werden die Burgen in Königreiche unterteilt. Dazu wird der {@link Clustering} Algorithmus aufgerufen. + * @param kingdomCount die Anzahl der zu generierenden Königreiche + */ + private void generateKingdoms(int kingdomCount) { + if(kingdomCount > 0 && kingdomCount < castleGraph.getAllValues().size()) { + Clustering clustering = new Clustering(castleGraph.getAllValues(), kingdomCount); + kingdoms = clustering.getPointsClusters(); + } else { + kingdoms = new ArrayList<>(); + } + } + + /** + * Eine neue Spielfeldkarte generieren. + * Dazu werden folgende Schritte abgearbeitet: + * 1. Das Hintergrundbild generieren + * 2. Burgen generieren + * 3. Kanten hinzufügen + * 4. Burgen in Köngireiche unterteilen + * @param width die Breite des Spielfelds + * @param height die Höhe des Spielfelds + * @param scale die Skalierung + * @param castleCount die maximale Anzahl an Burgen + * @param kingdomCount die Anzahl der Königreiche + * @return eine neue GameMap-Instanz + */ + public static GameMap generateRandomMap(int width, int height, int scale, int castleCount, int kingdomCount) { + + width = Math.max(width, 15); + height = Math.max(height, 10); + + if (scale <= 0 || castleCount <= 0) + throw new IllegalArgumentException(); + + System.out.println(String.format("Generating new map, castles=%d, width=%d, height=%d, kingdoms=%d", castleCount, width, height, kingdomCount)); + GameMap gameMap = new GameMap(width, height, scale); + gameMap.generateBackground(); + gameMap.generateCastles(castleCount); + gameMap.generateEdges(); + gameMap.generateKingdoms(kingdomCount); + + if(!gameMap.getGraph().allNodesConnected()) { + System.out.println("Fehler bei der Verifikation: Es sind nicht alle Knoten miteinander verbunden!"); + return null; + } + + return gameMap; + } + + /** + * Generiert eine Liste von Zufallsnamen für Burgen. Dabei wird ein Prefix (Schloss, Burg oder Festung) an einen + * vorhandenen Namen aus den Resourcen angefügt. Siehe auch: {@link Resources#getcastleNames()} + * @return eine Liste mit Zufallsnamen + */ + private List generateCastleNames() { + String[] prefixes = {"Schloss", "Burg", "Festung"}; + List names = Resources.getInstance().getCastleNames(); + List nameList = new ArrayList<>(names.size()); + + for (String name : names) { + String prefix = prefixes[(int) (Math.random() * prefixes.length)]; + nameList.add(prefix + " " + name); + } + + return nameList; + } + + public int getWidth() { + return this.backgroundImage.getWidth(); + } + + public int getHeight() { + return this.backgroundImage.getHeight(); + } + + public BufferedImage getBackgroundImage() { + return this.backgroundImage; + } + + public Dimension getSize() { + return new Dimension(this.getWidth(), this.getHeight()); + } + + public List getCastles() { + return castleGraph.getAllValues(); + } + + public Graph getGraph() { + return this.castleGraph; + } + + public List> getEdges() { + return this.castleGraph.getEdges(); + } + + public List getKingdoms() { + return this.kingdoms; + } +} diff --git a/Projektgruppe_XXX/src/game/map/Kingdom.java b/Projektgruppe_XXX/src/game/map/Kingdom.java new file mode 100644 index 0000000..4e43cc3 --- /dev/null +++ b/Projektgruppe_XXX/src/game/map/Kingdom.java @@ -0,0 +1,75 @@ +package game.map; + +import game.Player; + +import java.util.LinkedList; +import java.util.List; + +/** + * Diese Klasse representiert ein Königreich. Jedes Königreich hat eine Liste von Burgen sowie einen Index {@link #type} im Bereich von 0-5 + * + */ +public class Kingdom { + + private List castles; + private int type; + + /** + * Erstellt ein neues Königreich + * @param type der Typ des Königreichs (im Bereich 0-5) + */ + public Kingdom(int type) { + this.castles = new LinkedList<>(); + this.type = type; + } + + /** + * Eine Burg zum Königreich hinzufügen + * @param castle die Burg, die hinzugefügt werden soll + */ + public void addCastle(Castle castle) { + this.castles.add(castle); + } + + /** + * Gibt den Typen des Königreichs zurück. Dies wird zur korrekten Anzeige benötigt + * @return der Typ des Königreichs. + */ + public int getType() { + return this.type; + } + + /** + * Eine Burg aus dem Königreich entfernen + * @param castle die zu entfernende Burg + */ + public void removeCastle(Castle castle) { + this.castles.remove(castle); + } + + /** + * Gibt den Spieler zurück, der alle Burgen in dem Köngreich besitzt. + * Sollte es keinen Spieler geben, der alle Burgen besitzt, wird null zurückgegeben. + * @return der Besitzer oder null + */ + public Player getOwner() { + if(castles.isEmpty()) + return null; + + Player owner = castles.get(0).getOwner(); + for(Castle castle : castles) { + if(castle.getOwner() != owner) + return null; + } + + return owner; + } + + /** + * Gibt alle Burgen zurück, die in diesem Königreich liegen + * @return Liste von Burgen im Königreich + */ + public List getCastles() { + return this.castles; + } +} diff --git a/Projektgruppe_XXX/src/game/map/MapSize.java b/Projektgruppe_XXX/src/game/map/MapSize.java new file mode 100644 index 0000000..2a1d188 --- /dev/null +++ b/Projektgruppe_XXX/src/game/map/MapSize.java @@ -0,0 +1,27 @@ +package game.map; + +import java.util.Arrays; +import java.util.Vector; +import java.util.stream.Collectors; + +public enum MapSize { + + SMALL("Klein"), + MEDIUM("Mittel"), + LARGE("Groß"); + + private String label; + MapSize(String lbl) { + this.label = lbl; + } + + @Override + public String toString() { + return this.label; + } + + public static Vector getMapSizes() { + return Arrays.stream(values()).map(MapSize::toString).collect(Collectors.toCollection(Vector::new)); + } + +} diff --git a/Projektgruppe_XXX/src/game/map/PathFinding.java b/Projektgruppe_XXX/src/game/map/PathFinding.java new file mode 100644 index 0000000..2120838 --- /dev/null +++ b/Projektgruppe_XXX/src/game/map/PathFinding.java @@ -0,0 +1,58 @@ +package game.map; + +import base.GraphAlgorithm; +import base.Edge; +import base.Graph; +import game.Player; +import game.map.Castle; +import gui.components.MapPanel; + +import java.util.List; + +public class PathFinding extends GraphAlgorithm { + + private MapPanel.Action action; + private Player currentPlayer; + + public PathFinding(Graph graph, Castle sourceCastle, MapPanel.Action action, Player currentPlayer) { + super(graph, graph.getNode(sourceCastle)); + this.action = action; + this.currentPlayer = currentPlayer; + } + + @Override + protected double getValue(Edge edge) { + Castle castleA = edge.getNodeA().getValue(); + Castle castleB = edge.getNodeB().getValue(); + return castleA.distance(castleB); + } + + @Override + protected boolean isPassable(Edge edge) { + + Castle castleA = edge.getNodeA().getValue(); + Castle castleB = edge.getNodeB().getValue(); + + // One of the regions should belong to the current player + if(castleA.getOwner() != currentPlayer && castleB.getOwner() != currentPlayer) + return false; + + if(action == MapPanel.Action.ATTACKING) { + return castleA.getOwner() != null && castleB.getOwner() != null; + } else if(action == MapPanel.Action.MOVING) { + + // One of the regions may be empty + if(castleA.getOwner() == null || castleB.getOwner() == null) + return true; + + // Else both regions should belong to the current player + return castleA.getOwner() == castleB.getOwner() && castleA.getOwner() == currentPlayer; + } else { + return false; + } + } + + public List> getPath(Castle targetCastle) { + return this.getPath(getGraph().getNode(targetCastle)); + } +} diff --git a/Projektgruppe_XXX/src/game/players/BasicAI.java b/Projektgruppe_XXX/src/game/players/BasicAI.java new file mode 100644 index 0000000..de989c7 --- /dev/null +++ b/Projektgruppe_XXX/src/game/players/BasicAI.java @@ -0,0 +1,103 @@ +package game.players; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import base.Edge; +import base.Graph; +import base.Node; +import game.AI; +import game.Game; +import game.map.Castle; +import gui.AttackThread; + +public class BasicAI extends AI { + + public BasicAI(String name, Color color) { + super(name, color); + } + + private Castle getCastleWithFewestTroops(List castles) { + Castle fewestTroops = castles.get(0); + for(Castle castle : castles) { + if(castle.getTroopCount() < fewestTroops.getTroopCount()) { + fewestTroops = castle; + } + } + + return fewestTroops; + } + + @Override + protected void actions(Game game) throws InterruptedException { + if(game.getRound() == 1) { + List availableCastles = game.getMap().getCastles().stream().filter(c -> c.getOwner() == null).collect(Collectors.toList()); + while(availableCastles.size() > 0 && getRemainingTroops() > 0) { + + sleep(1000); + + Castle randomCastle = availableCastles.remove(this.getRandom().nextInt(availableCastles.size())); + game.chooseCastle(randomCastle, this); + } + } else { + + // 1. Distribute remaining troops + Graph graph = game.getMap().getGraph(); + List castleNearEnemy = new ArrayList<>(); + for(Castle castle : this.getCastles(game)) { + Node node = graph.getNode(castle); + for(Edge edge : graph.getEdges(node)) { + Castle otherCastle = edge.getOtherNode(node).getValue(); + if(otherCastle.getOwner() != this) { + castleNearEnemy.add(castle); + break; + } + } + } + + while(this.getRemainingTroops() > 0) { + Castle fewestTroops = getCastleWithFewestTroops(castleNearEnemy); + sleep(500); + game.addTroops(this, fewestTroops, 1); + } + + boolean attackWon; + + do { + // 2. Move troops from inside to border + for (Castle castle : this.getCastles(game)) { + if (!castleNearEnemy.contains(castle) && castle.getTroopCount() > 1) { + Castle fewestTroops = getCastleWithFewestTroops(castleNearEnemy); + game.moveTroops(castle, fewestTroops, castle.getTroopCount() - 1); + } + } + + // 3. attack! + attackWon = false; + for (Castle castle : castleNearEnemy) { + if(castle.getTroopCount() < 2) + continue; + + Node node = graph.getNode(castle); + for (Edge edge : graph.getEdges(node)) { + Castle otherCastle = edge.getOtherNode(node).getValue(); + if (otherCastle.getOwner() != this && castle.getTroopCount() >= otherCastle.getTroopCount()) { + AttackThread attackThread = game.startAttack(castle, otherCastle, castle.getTroopCount()); + if(fastForward) + attackThread.fastForward(); + + attackThread.join(); + attackWon = attackThread.getWinner() == this; + break; + } + } + + if(attackWon) + break; + } + } while(attackWon); + } + } +} diff --git a/Projektgruppe_XXX/src/game/players/Human.java b/Projektgruppe_XXX/src/game/players/Human.java new file mode 100644 index 0000000..48b3332 --- /dev/null +++ b/Projektgruppe_XXX/src/game/players/Human.java @@ -0,0 +1,11 @@ +package game.players; + +import java.awt.Color; + +import game.Player; + +public class Human extends Player { + public Human(String name, Color color) { + super(name, color); + } +} diff --git a/Projektgruppe_XXX/src/gui/AttackThread.java b/Projektgruppe_XXX/src/gui/AttackThread.java new file mode 100644 index 0000000..6affbca --- /dev/null +++ b/Projektgruppe_XXX/src/gui/AttackThread.java @@ -0,0 +1,76 @@ +package gui; + +import game.Game; +import game.Player; +import game.map.Castle; + +public class AttackThread extends Thread { + + private Castle attackerCastle, defenderCastle; + private Player attacker, defender; + private int troopAttackCount; + private Game game; + private boolean fastForward; + private Player winner; + + public AttackThread(Game game, Castle attackerCastle, Castle defenderCastle, int troopAttackCount) { + this.attackerCastle = attackerCastle; + this.defenderCastle = defenderCastle; + this.attacker = attackerCastle.getOwner(); + this.defender = defenderCastle.getOwner(); + this.winner = defender; + this.troopAttackCount = troopAttackCount; + this.game = game; + this.fastForward = false; + } + + public void fastForward() { + fastForward = true; + } + + private void sleep(int ms) throws InterruptedException { + long end = System.currentTimeMillis() + ms; + while(System.currentTimeMillis() < end && !fastForward) { + Thread.sleep(10); + } + } + + @Override + public void run() { + + int attackUntil = Math.max(1, attackerCastle.getTroopCount() - troopAttackCount); + + try { + sleep(1500); + + while(attackerCastle.getTroopCount() > attackUntil) { + + // Attacker dices: at maximum 3 and not more than actual troop count + int attackerCount = Math.min(troopAttackCount, Math.min(attackerCastle.getTroopCount(), 3)); + int attackerDice[] = game.roll(attacker, attackerCount, fastForward); + + sleep(1500); + + // Defender dices: at maximum 2 + int defenderCount = Math.min(2, defenderCastle.getTroopCount()); + int defenderDice[] = game.roll(defender, defenderCount, fastForward); + + game.doAttack(attackerCastle, defenderCastle, attackerDice, defenderDice); + if(defenderCastle.getOwner() == attacker) { + winner = attacker; + break; + } + + sleep(1500); + } + } catch(InterruptedException ex) { + ex.printStackTrace(); + } + + game.stopAttack(); + } + + public Player getWinner() { + return winner; + } +} \ No newline at end of file diff --git a/Projektgruppe_XXX/src/gui/GameWindow.java b/Projektgruppe_XXX/src/gui/GameWindow.java new file mode 100644 index 0000000..dab7abe --- /dev/null +++ b/Projektgruppe_XXX/src/gui/GameWindow.java @@ -0,0 +1,71 @@ +package gui; + +import gui.views.StartScreen; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +/** + * Diese Klasse bietet das Fenster wo die grafische Oberfläche angezeigt wird. + * + * @author Roman Hergenreder + */ +public class GameWindow extends JFrame { + + private View activeView; + private Resources resources; + + private GameWindow(Resources resources) { + this.resources = resources; + this.initWindow(); + this.setView(new StartScreen(this)); + this.setVisible(true); + } + + private void initWindow() { + + LookAndFeel laf = UIManager.getLookAndFeel(); + if(laf.getSupportsWindowDecorations()) { + UIManager.getCrossPlatformLookAndFeelClassName(); + } + + this.setTitle("Game of Castles - FOP Projekt WiSe 18/19"); + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setSize(800, 500); + this.setMinimumSize(new Dimension(600, 400)); + this.setLocationRelativeTo(null); // Center Window + this.addWindowStateListener(windowStateListener); // Resize Event + } + + public void setView(View view) { + this.activeView = view; + this.activeView.setSize(getContentPane().getSize()); + this.setContentPane(view); + this.requestFocus(); + } + + private WindowStateListener windowStateListener = new WindowStateListener() { + @Override + public void windowStateChanged(WindowEvent windowEvent) { + if ((windowEvent.getNewState() & JFrame.MAXIMIZED_BOTH) != JFrame.MAXIMIZED_BOTH) { + activeView.onResize(); + } + } + }; + + public static void main(String[] args) { + + Resources resources = Resources.getInstance(); + if(!resources.load()) + return; + + new GameWindow(resources); + + Runtime.getRuntime().addShutdownHook(new Thread(resources::save)); + } + + public Resources getResources() { + return this.resources; + } +} diff --git a/Projektgruppe_XXX/src/gui/Resources.java b/Projektgruppe_XXX/src/gui/Resources.java new file mode 100644 index 0000000..83a7491 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/Resources.java @@ -0,0 +1,315 @@ +package gui; + +import game.ScoreEntry; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.WritableRaster; +import java.io.*; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.List; + +/** + * Diese Klasse verwaltet die Resourcen des Spiels, darunter beispielsweise Bilder, Icons und Schriftarten. + */ +public class Resources { + + private static final int NUM_CASTLES = 6; + private static Resources instance; + private Map castlesColored; + private BufferedImage dices[]; + private BufferedImage check; + private BufferedImage unit; + private BufferedImage arrow, arrowDeactivated, plus, plusDeactivated, swords; + private BufferedImage soldiers[]; + private List castleNames; + private Font celticFont; + + private List scoreEntries; + + private boolean resourcesLoaded; + + /** + * Privater Konstruktor, dieser wird normalerweise nur einmal aufgerufen + */ + private Resources() { + this.resourcesLoaded = false; + this.scoreEntries = new LinkedList<>(); + } + + /** + * Gibt die Instanz des Resourcen Managers zurück oder erzeugt eine neue + * @return Resourcen Manager + */ + public static Resources getInstance() { + if(instance == null) { + instance = new Resources(); + instance.load(); + } + + return instance; + } + + /** + * Lädt ein Bild aus den Resourcen + * @param name der Name der Datei + * @return das Bild als {@link BufferedImage}-Objekt + * @throws IOException Eine IOException wird geworfen, falls das Bild nicht gefunden wurde oder andere Probleme beim Laden auftreten + */ + private BufferedImage loadImage(String name) throws IOException { + URL res = Resources.class.getClassLoader().getResource(name); + if(res == null) + throw new IOException("Resource not found: " + name); + + return ImageIO.read(res); + } + + /** + * Lädt alle Resourcen + * @return true, wenn alle Resourcen erfolgreich geladen wurden + */ + public boolean load() { + if(resourcesLoaded) + return true; + + try { + // Load Castle + castlesColored = new HashMap<>(); + BufferedImage castles[] = new BufferedImage[NUM_CASTLES]; + for(int i = 0; i < NUM_CASTLES; i++) + castles[i] = loadImage("castle" + (i + 1) + ".png"); + castlesColored.put(Color.WHITE, castles); + + // Load Dices + dices = loadDices(); + + // Load Icons + soldiers = new BufferedImage[] { loadImage("soldier1.png"), loadImage("soldier2.png") }; + check = loadImage("check.png"); + unit = loadImage("unit.png"); + arrow = loadImage("arrow.png"); + swords = loadImage("swords.png"); + plus = loadImage("plus.png"); + plusDeactivated = loadImage("plus_deactivated.png"); + arrowDeactivated = loadImage("arrow_deactivated.png"); + + // Load Castle names + castleNames = loadRegionNames(); + + // Load font + celticFont = loadFont("celtic.ttf"); + + // Load score entries + loadScoreEntries(); + + resourcesLoaded = true; + return true; + } catch(Exception ex) { + JOptionPane.showMessageDialog(null, "Konnte Resourcen nicht laden: " + ex.getMessage(), "Fehler beim Laden der Resourcen", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + return false; + } + } + + /** + * Speichert bestimmte Resourcen, zurzeit nur den Highscore-Table + * @return gibt true zurück, wenn erfolgreich gespeichert wurde + */ + public boolean save() { + try { + saveScoreEntries(); + return true; + } catch(IOException ex) { + ex.printStackTrace(); + return false; + } + } + + /** + * Diese Methode speichert alle Objekte des Typs {@link ScoreEntry} in der Textdatei "highscores.txt" + * Jede Zeile stellt dabei einen ScoreEntry dar. Sollten Probleme auftreten, muss eine {@link IOException} geworfen werden. + * Die Einträge sind in der Liste {@link #scoreEntries} zu finden. + * @see ScoreEntry#write(PrintWriter) + * @throws IOException Eine IOException wird geworfen, wenn Probleme beim Schreiben auftreten. + */ + private void saveScoreEntries() throws IOException { + // TODO: Resources#saveScoreEntries() + } + + /** + * Lädt den Highscore-Table aus der Datei "highscores.txt". + * Dabei wird die Liste {@link #scoreEntries} neu erzeugt und befüllt. + * Beachte dabei, dass die Liste nach dem Einlen absteigend nach den Punktzahlen sortiert sein muss. + * Sollte eine Exception auftreten, kann diese ausgegeben werden, sie sollte aber nicht weitergegeben werden, + * da sonst das Laden der restlichen Resourcen abgebrochen wird ({@link #load()}). + * @see ScoreEntry#read(String) + * @see #addScoreEntry(ScoreEntry) + */ + private void loadScoreEntries() { + // TODO: Resources#loadScoreEntries() + } + + /** + * Fügt ein {@link ScoreEntry}-Objekt der Liste von Einträgen hinzu. + * Beachte: Nach dem Einfügen muss die Liste nach den Punktzahlen absteigend sortiert bleiben. + * @param scoreEntry Der einzufügende Eintrag + * @see ScoreEntry#compareTo(ScoreEntry) + */ + public void addScoreEntry(ScoreEntry scoreEntry) { + int i = scoreEntries.size() - 1; + for(; i >= 0; i--) { + if(scoreEntry.compareTo(scoreEntries.get(i)) < 0) { + break; + } + } + + scoreEntries.add(i + 1, scoreEntry); + } + + public List getScoreEntries() { + return scoreEntries; + } + + private Font loadFont(String name) throws IOException, FontFormatException { + InputStream is = Resources.class.getClassLoader().getResourceAsStream(name); + Font f = Font.createFont(Font.TRUETYPE_FONT, is); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + ge.registerFont(f); + return f.deriveFont(20f); + } + + private BufferedImage[] loadDices() throws IOException { + BufferedImage[] dices = new BufferedImage[6]; + BufferedImage diceImage = loadImage("dices.png"); + int diceSizeW = diceImage.getWidth() / 3; + int diceSizeH = diceImage.getHeight() / 2; + if(diceSizeW != diceSizeH) + System.out.println("Invalid dice dimensions for resource: dice.png. Expected 3x2 dices, got dimensions: " + diceImage.getWidth() + "x" + diceImage.getHeight()); + + int diceSize = diceSizeH; + int num = 0; + + for (int x = 0; x < 2; x++) { + for (int y = 0; y < 3; y++) { + //Initialize the image array with image chunks + dices[num] = new BufferedImage(diceSize, diceSize, diceImage.getType()); + + // draws the image chunk + Graphics2D gr = dices[num++].createGraphics(); + gr.drawImage(diceImage, 0, 0, diceSize, diceSize, diceSize * y, diceSize * x, diceSize * y + diceSize, diceSize * x + diceSize, null); + gr.dispose(); + } + } + + return dices; + } + + private List loadRegionNames() throws IOException { + + List regionNames = new LinkedList<>(); + InputStream is = Resources.class.getClassLoader().getResourceAsStream("castles.txt"); + InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(isr); + + String line; + while((line = br.readLine()) != null) { + line = line.trim(); + if(line.length() > 0 && !line.startsWith("#")) { + regionNames.add(line); + } + } + + br.close(); + return regionNames; + } + + private BufferedImage colorImage(BufferedImage original, Color color) { + ColorModel cm = original.getColorModel(); + boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); + WritableRaster raster = original.copyData(null); + BufferedImage newImage = new BufferedImage(cm, raster, isAlphaPremultiplied, null); + + for(int x = 0; x < original.getWidth(); x++) { + for(int y = 0; y < original.getHeight(); y++) { + Color oldColor = new Color(original.getRGB(x, y), true); + if(oldColor.getRed() == 255 && oldColor.getGreen() == 255 && oldColor.getBlue() == 255 && oldColor.getAlpha() == 255) { + newImage.setRGB(x, y, color.getRGB()); + } + } + } + + return newImage; + } + + public BufferedImage getCastle(Color color, int index) { + if(!resourcesLoaded) + return null; + + index = index % NUM_CASTLES; + BufferedImage images[]; + if(castlesColored.containsKey(color)) + images = castlesColored.get(color); + else + images = new BufferedImage[NUM_CASTLES]; + + if(images[index] != null) + return images[index]; + + BufferedImage castleGeneric = castlesColored.get(Color.WHITE)[index]; + images[index] = colorImage(castleGeneric, color); + castlesColored.put(color, images); + return images[index]; + } + + public BufferedImage getDice(int value) { + if(!resourcesLoaded) + return null; + + return dices[value % dices.length]; + } + + public BufferedImage getCheckIcon() { + return this.check; + } + + public BufferedImage getUnitIcon() { + return this.unit; + } + + public BufferedImage getPlusIcon() { + return this.plus; + } + + public BufferedImage getArrowIcon() { + return this.arrow; + } + + public BufferedImage getSwordsIcon() { + return this.swords; + } + + public BufferedImage getArrowIconDeactivated() { + return this.arrowDeactivated; + } + + public BufferedImage[] getSoldiers() { + return this.soldiers; + } + + public List getCastleNames() { + return castleNames; + } + + public BufferedImage getPlusIconDeactivated() { + return this.plusDeactivated; + } + + public Font getCelticFont() { + return this.celticFont; + } +} diff --git a/Projektgruppe_XXX/src/gui/View.java b/Projektgruppe_XXX/src/gui/View.java new file mode 100644 index 0000000..a7d5132 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/View.java @@ -0,0 +1,135 @@ +package gui; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; + +import java.awt.*; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.geom.AffineTransform; +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +/** + * @author Roman Hergenreder + */ +public abstract class View extends Container implements ActionListener { + + private GameWindow gameWindow; + protected static final Dimension BUTTON_SIZE = new Dimension(125, 40); + + public View(GameWindow gameWindow) { + this.gameWindow = gameWindow; + this.onInit(); + this.setSize(gameWindow.getSize()); + this.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent componentEvent) { + onResize(); + } + }); + } + + public abstract void onResize(); + protected abstract void onInit(); + + public static Font createFont(int Size) { + return new Font("Times New Roman", Font.PLAIN, Size); + } + + public static Font createCelticFont(float Size) { + Resources resources = Resources.getInstance(); + return resources.getCelticFont().deriveFont(Size); + } + + protected GameWindow getWindow() { + return this.gameWindow; + } + + protected JButton createButton(String text) { + JButton button = new JButton(text); + button.setSize(BUTTON_SIZE); + button.setFont(createFont(16)); + button.addActionListener(this); + button.setBackground(new Color(54, 103, 53)); + button.setForeground(Color.WHITE); + button.setFocusable(false); + button.setFont(gameWindow.getResources().getCelticFont()); + this.add(button); + return button; + } + + public static Dimension calculateTextSize(String text, Font font) { + AffineTransform affinetransform = new AffineTransform(); + FontRenderContext frc = new FontRenderContext(affinetransform,true,true); + int width = (int)(font.getStringBounds(text, frc).getWidth()); + int height = (int)(font.getStringBounds(text, frc).getHeight()); + return new Dimension(width, height); + } + + private Dimension calculateLabelSize(JLabel label) { + return calculateTextSize(label.getText(), label.getFont()); + } + + protected JLabel createLabel(String text, int fontSize) { + return createLabel(text, fontSize, false); + } + + protected JLabel createLabel(String text, int fontSize, boolean underline) { + JLabel label = new JLabel(text); + label.setFont(createFont(fontSize)); + label.setSize(calculateLabelSize(label)); + + if(underline) { + Font font = label.getFont(); + Map attributes = new HashMap<>(font.getAttributes()); + attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + label.setFont(font.deriveFont(attributes)); + } + + add(label); + return label; + } + + + protected JComboBox createCombobox(Vector values, int selectedIndex) { + JComboBox comboBox = new JComboBox<>(values); + comboBox.setSelectedIndex(selectedIndex); + comboBox.setSize(200, 25); + add(comboBox); + return comboBox; + } + + protected JTextPane createTextPane() { + JTextPane textPane = new JTextPane(); + textPane.setEditable(false); + textPane.setBorder(BorderFactory.createCompoundBorder( + new LineBorder(Color.BLACK), + new EmptyBorder(3,3,3,3) + )); + return textPane; + } + + protected JTextArea createTextArea(String text, boolean readonly) { + JTextArea textArea = new JTextArea(text); + textArea.setWrapStyleWord(true); + textArea.setLineWrap(true); + + if(readonly) { + textArea.setEditable(false); + textArea.setBackground(this.getBackground()); + } + + add(textArea); + return textArea; + } + + protected void showErrorMessage(String text, String title) { + JOptionPane.showMessageDialog(this, text, title, JOptionPane.ERROR_MESSAGE); + } +} diff --git a/Projektgruppe_XXX/src/gui/components/ColorChooserButton.java b/Projektgruppe_XXX/src/gui/components/ColorChooserButton.java new file mode 100644 index 0000000..39e7a41 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/components/ColorChooserButton.java @@ -0,0 +1,42 @@ +package gui.components; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class ColorChooserButton extends JButton { + + private Color current; + + public ColorChooserButton(Color c) { + setSelectedColor(c); + addActionListener(event -> { + Color newColor = JColorChooser.showDialog(null, "Choose a color", current); + setSelectedColor(newColor); + }); + } + + public Color getSelectedColor() { + return current; + } + + public void setSelectedColor(Color newColor) { + if (newColor == null) + return; + + current = newColor; + setIcon(createIcon(current, 16, 16)); + repaint(); + } + + public static ImageIcon createIcon(Color main, int width, int height) { + BufferedImage image = new BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + graphics.setColor(main); + graphics.fillRect(0, 0, width, height); + graphics.setXORMode(Color.DARK_GRAY); + graphics.drawRect(0, 0, width-1, height-1); + image.flush(); + return new ImageIcon(image); + } +} diff --git a/Projektgruppe_XXX/src/gui/components/DicePanel.java b/Projektgruppe_XXX/src/gui/components/DicePanel.java new file mode 100644 index 0000000..3ef0781 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/components/DicePanel.java @@ -0,0 +1,79 @@ +package gui.components; + +import gui.Resources; + +import javax.swing.*; +import java.awt.*; +import java.util.Arrays; +import java.util.Random; + +public class DicePanel extends JPanel { + + private int diceValues[]; + private Random random; + private Resources resources; + private int numDices; + + public DicePanel(Resources resources) { + this.diceValues = new int[3]; + this.resources = resources; + this.random = new Random(); + this.generateRandom(3); + } + + public int[] generateRandom(int numDices) { + this.numDices = numDices; + int[] result = new int[Math.min(numDices, diceValues.length)]; + + for (int i = 0; i < Math.min(numDices, diceValues.length); i++) { + diceValues[i] = random.nextInt(6) + 1; + result[i] = diceValues[i]; + } + + repaint(); + return result; + } + + public int[] generateRandom(int numDices, boolean animate) throws InterruptedException { + if(animate) { + long duration = 1500; + long start = System.currentTimeMillis(); + long end = start + duration; + long lastTick = 0; + + while(System.currentTimeMillis() < end) { + long tick = System.currentTimeMillis() - start; + double progress = (double)tick / (double) duration; + long waitTime = (long) (200 * Math.pow(progress, 3) - 800 * Math.pow(progress, 2) + 850 * progress + 20); + if(lastTick == 0 || (waitTime > 0 && (tick - lastTick) >= waitTime)) { + lastTick = tick; + generateRandom(numDices); + } else { + Thread.sleep(10); + } + } + } + + return generateRandom(numDices); + } + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + int width = getWidth(); + int height = getHeight(); + int margin = 10; + int diceCount = Math.min(diceValues.length, numDices); + + int diceSize = Math.min(height - 2 * margin, (width - (diceCount + 1) * margin) / diceCount); + int offsetX = (width - 2 * margin - (diceCount * diceSize)) / 2; + + for(int i = 0; i < diceCount; i++) { + int x = offsetX + i * (margin + diceSize); + int y = margin; + int value = (diceValues[i] - 1) % 6; + g.drawImage(resources.getDice(value), x, y, diceSize, diceSize, null); + } + } +} diff --git a/Projektgruppe_XXX/src/gui/components/MapPanel.java b/Projektgruppe_XXX/src/gui/components/MapPanel.java new file mode 100644 index 0000000..32ca60e --- /dev/null +++ b/Projektgruppe_XXX/src/gui/components/MapPanel.java @@ -0,0 +1,527 @@ +package gui.components; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Line2D; +import java.awt.image.BufferedImage; +import java.util.List; + +import javax.swing.*; +import javax.swing.border.LineBorder; + +import base.Edge; +import game.AI; +import game.Game; +import game.map.PathFinding; +import game.Player; +import game.map.Castle; +import game.map.GameMap; +import game.players.Human; +import gui.Resources; +import gui.View; +import gui.views.GameView; + +public class MapPanel extends JScrollPane { + + public enum Action { + NONE, + MOVING, + ATTACKING + } + + private static final int CASTLE_SIZE = 50; + private static final int ICON_SIZE = 20; + private final GameView gameView; + + private ImagePanel imagePanel; + private GameMap map; + private Point mousePos, oldView; + private Castle selectedCastle; + private boolean showConnections; + private Resources resources; + private Game game; + private Action currentAction; + private PathFinding pathFinding; + private List> highlightedEdges; + private Castle targetCastle; + + public MapPanel(GameView gameView, Resources resources) { + super(); + this.gameView = gameView; + this.setBorder(new LineBorder(Color.BLACK)); + this.setViewportView(this.imagePanel = new ImagePanel()); + this.addMouseListener(onMouseInput); + this.addMouseMotionListener(onMouseInput); + this.showConnections = false; + this.setAutoscrolls(true); + this.resources = resources; + this.currentAction = Action.NONE; + + this.getActionMap().put("Escape", new AbstractAction("Escape") { + @Override + public void actionPerformed(ActionEvent actionEvent) { + if(currentAction != Action.NONE) { + currentAction = Action.NONE; + targetCastle = null; + highlightedEdges = null; + repaint(); + } else if(selectedCastle != null) { + selectedCastle = null; + repaint(); + } + } + }); + this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Escape"); + } + + private Castle getRegion(Point p) { + if (map == null) + return null; + + for (Castle castle : map.getCastles()) { + Point location = castle.getLocationOnMap(); + Rectangle rect = new Rectangle(location.x, location.y, CASTLE_SIZE, CASTLE_SIZE); + if (rect.contains(p)) + return castle; + } + + return null; + } + + private boolean canPerformAction() { + if(game.getCurrentPlayer() instanceof AI) + return false; + + if(game.isOver()) + return false; + + return game.getAttackThread() == null; + } + + private MouseAdapter onMouseInput = new MouseAdapter() { + + @Override + public void mousePressed(MouseEvent mouseEvent) { + super.mousePressed(mouseEvent); + oldView = getViewport().getViewPosition(); + mousePos = mouseEvent.getPoint(); + } + + @Override + public void mouseReleased(MouseEvent mouseEvent) { + super.mousePressed(mouseEvent); + + if(getCursor().getType() == Cursor.MOVE_CURSOR) + setCursor(Cursor.getDefaultCursor()); + } + + @Override + public void mouseDragged(MouseEvent e) { + super.mouseDragged(e); + + if (oldView != null && mousePos != null && !mousePos.equals(e.getPoint())) { + JViewport vp = getViewport(); + + + if (getWidth() < imagePanel.getWidth() || getHeight() < imagePanel.getHeight()) { + int newX = (int) Math.max(0, oldView.getX() - (e.getX() - mousePos.getX())); + int newY = (int) Math.max(0, oldView.getY() - (e.getY() - mousePos.getY())); + + // don't update view position, if you cannot move in this direction (e.g. vp.w >= img.w) + if(getWidth() >= imagePanel.getWidth()) + newX = vp.getViewPosition().x; + + if(getHeight() >= imagePanel.getHeight()) + newY = vp.getViewPosition().y; + + if(currentAction == Action.ATTACKING || currentAction == Action.MOVING) + setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); + else + setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + + Point newPoint = new Point(newX, newY); + vp.setViewPosition(newPoint); + } + } + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point mousePos = cursorToMapLocation(e.getPoint()); + Player currentPlayer = game.getCurrentPlayer(); + boolean selectNew = true; + Action lastAction = currentAction; + + if (selectedCastle != null && canPerformAction()) { + Point castlePos = selectedCastle.getLocationOnMap(); + + if(canChooseCastle()) { + Rectangle iconCheck = getBoundsIconCheck(castlePos); + if (iconCheck.contains(mousePos)) { + game.chooseCastle(selectedCastle, currentPlayer); + gameView.updateStats(); + setCursor(Cursor.getDefaultCursor()); + } + } else if(selectedCastle.getOwner() == currentPlayer && game.getRound() > 1) { + Rectangle iconPlus = getBoundsPlusIcon(castlePos); + Rectangle iconArrow = getBoundsArrowIcon(castlePos); + Rectangle iconSwords = getBoundsSwordsIcon(castlePos); + selectNew = false; + + if(iconPlus.contains(mousePos)) { + if(currentPlayer.getRemainingTroops() > 0) { + game.addTroops(currentPlayer, selectedCastle, 1); + gameView.updateStats(); + } + } else if (iconArrow.contains(mousePos)) { + if(selectedCastle.getTroopCount() > 1) { + currentAction = (currentAction == Action.MOVING ? Action.NONE : Action.MOVING); + } + } else if (iconSwords.contains(mousePos)) { + if(canAttack()) { + currentAction = (currentAction == Action.ATTACKING ? Action.NONE : Action.ATTACKING); + } + } else { + selectNew = true; + } + } + + if(currentAction != Action.NONE) { + if(lastAction != currentAction) { + pathFinding = new PathFinding(game.getMap().getGraph(), selectedCastle, currentAction, currentPlayer); + pathFinding.run(); + } + + setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); + } + } + + if(selectNew) { + Castle nextCastle = getRegion(mousePos); + if(nextCastle == null || nextCastle == selectedCastle || currentAction == Action.NONE) { + currentAction = Action.NONE; + selectedCastle = nextCastle; + setCursor(Cursor.getDefaultCursor()); + } else if(currentAction == Action.MOVING) { + NumberDialog nd = new NumberDialog("Wie viele Truppen möchtest du verschieben?", 1, selectedCastle.getTroopCount() - 1, 1); + if(nd.showDialog(MapPanel.this)) { + selectedCastle.moveTroops(nextCastle, nd.getValue()); + currentAction = Action.NONE; + selectedCastle = null; + highlightedEdges = null; + targetCastle = null; + setCursor(Cursor.getDefaultCursor()); + gameView.updateStats(); + } + } else if(currentAction == Action.ATTACKING) { + NumberDialog nd = new NumberDialog("Mit wie vielen Truppen möchtest du angreifen?", 1, selectedCastle.getTroopCount(), selectedCastle.getTroopCount()); + if(nd.showDialog(MapPanel.this)) { + game.startAttack(selectedCastle, targetCastle, nd.getValue()); + currentAction = Action.NONE; + } + } + } + + repaint(); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + Point mousePos = cursorToMapLocation(e.getPoint()); + + if (selectedCastle != null && getCursor().getType() != Cursor.MOVE_CURSOR) { + Point castlePos = selectedCastle.getLocationOnMap(); + + if (canChooseCastle()) { + Rectangle iconCheck = getBoundsIconCheck(castlePos); + if (iconCheck.contains(mousePos)) { + setToolTipText("Diese Burg besetzen"); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return; + } + } else if(selectedCastle.getOwner() == game.getCurrentPlayer() && game.getRound() > 1) { + Rectangle iconPlus = getBoundsPlusIcon(castlePos); + Rectangle iconArrow = getBoundsArrowIcon(castlePos); + Rectangle iconSwords = getBoundsSwordsIcon(castlePos); + Rectangle bounds[] = { iconPlus, iconArrow, iconSwords }; + String tooltips[] = { "Truppen hinzufügen", "Truppen bewegen", "Burg angreifen" }; + + for(int i = 0; i < 3; i++) { + if (bounds[i].contains(mousePos)) { + setToolTipText(tooltips[i]); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return; + } + } + } + + if(currentAction == Action.MOVING || currentAction == Action.ATTACKING) { + targetCastle = getRegion(mousePos); + if(targetCastle != null) { + if(currentAction != Action.ATTACKING || targetCastle.getOwner() != selectedCastle.getOwner()) { + highlightedEdges = pathFinding.getPath(targetCastle); + repaint(); + } else { + targetCastle = null; + } + } else if(highlightedEdges != null) { + highlightedEdges = null; + targetCastle = null; + repaint(); + } + + setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); + setToolTipText(null); + return; + } + + + setCursor(Cursor.getDefaultCursor()); + setToolTipText(null); + } + } + }; + + private boolean canChooseCastle() { + if (selectedCastle == null) + return false; + + return game.getCurrentPlayer() instanceof Human && + game.getCurrentPlayer().getRemainingTroops() > 0 && + game.getRound() == 1 && + selectedCastle.getOwner() == null; + } + + private boolean canAttack() { + if(selectedCastle == null) + return false; + + return game.getCurrentPlayer() instanceof Human && + selectedCastle.getOwner() == game.getCurrentPlayer() && + selectedCastle.getTroopCount() > 1; + } + + private Rectangle getBoundsIconCheck(Point castlePos) { + int x = (CASTLE_SIZE + 10 - ICON_SIZE) / 2 + castlePos.x - 5; + int y = (castlePos.y - 5 - ICON_SIZE); + + return new Rectangle(x, y, ICON_SIZE, ICON_SIZE); + } + + private Rectangle getBoundsPlusIcon(Point castlePos) { + int totalWidth = 3 * (ICON_SIZE + 2); + int x = castlePos.x - 5 + (CASTLE_SIZE + 10 - totalWidth) / 2; + int y = castlePos.y - 6 - ICON_SIZE; + return new Rectangle(x, y, ICON_SIZE, ICON_SIZE); + } + + private Rectangle getBoundsArrowIcon(Point castlePos) { + Rectangle plusIcon = getBoundsPlusIcon(castlePos); + plusIcon.x += ICON_SIZE + 2; + return plusIcon; + } + + private Rectangle getBoundsSwordsIcon(Point castlePos) { + Rectangle arrowIcon = getBoundsArrowIcon(castlePos); + arrowIcon.x += ICON_SIZE + 2; + return arrowIcon; + } + + public void showConnections(boolean showConnections) { + this.showConnections = showConnections; + repaint(); + } + + // (0|0) -> (0 + offsetX|0 + offsetY) + private Point translate(Point p) { + int offsetX = 0; + int offsetY = 0; + + if (getSize().getWidth() > map.getBackgroundImage().getWidth()) + offsetX = (int) ((getSize().getWidth() - map.getBackgroundImage().getWidth()) / 2); + + if (getSize().getHeight() > map.getBackgroundImage().getHeight()) + offsetY = (int) ((getSize().getHeight() - map.getBackgroundImage().getHeight()) / 2); + + return new Point(p.x + offsetX, p.y + offsetY); + } + + private Point cursorToMapLocation(Point p) { + int offsetX = 0; + int offsetY = 0; + + if (getSize().getWidth() > map.getBackgroundImage().getWidth()) + offsetX = (int) ((getSize().getWidth() - map.getBackgroundImage().getWidth()) / 2); + + if (getSize().getHeight() > map.getBackgroundImage().getHeight()) + offsetY = (int) ((getSize().getHeight() - map.getBackgroundImage().getHeight()) / 2); + + JViewport jp = this.getViewport(); + return new Point(p.x - offsetX + jp.getViewPosition().x, p.y - offsetY + jp.getViewPosition().y); + } + + public void setGame(Game game) { + this.game = game; + this.map = game.getMap(); + this.imagePanel.setSize(map.getSize()); + this.repaint(); + } + + class ImagePanel extends JPanel { + + @Override + public Dimension getPreferredSize() { + return map != null ? map.getSize() : new Dimension(); + } + + @Override + public void paintComponent(Graphics g) { + + Graphics2D g2 = (Graphics2D) g; + Point offset = translate(new Point(0, 0)); + + if (map != null) { + g.drawImage(map.getBackgroundImage(), offset.x, offset.y, null); + + if (showConnections) { + for (Edge edge : map.getEdges()) { + Point p1 = translate(edge.getNodeA().getValue().getLocationOnMap()); + Point p2 = translate(edge.getNodeB().getValue().getLocationOnMap()); + + if(highlightedEdges != null && highlightedEdges.contains(edge)) { + g2.setStroke(new BasicStroke(3)); + g.setColor(Color.RED); + } else { + g2.setStroke(new BasicStroke(1)); + g.setColor(Color.WHITE); + } + + g2.draw(new Line2D.Float(p1.x + CASTLE_SIZE / 2.0f, p1.y + CASTLE_SIZE / 2.0f, p2.x + CASTLE_SIZE / 2.0f, p2.y + CASTLE_SIZE / 2.0f)); + g2.setStroke(new BasicStroke(1)); + } + } + + for (Castle region : map.getCastles()) { + Color color = region.getOwner() == null ? Color.WHITE : region.getOwner().getColor(); + Point location = translate(region.getLocationOnMap()); + BufferedImage castle = resources.getCastle(color, region.getType()); + g.drawImage(castle, location.x, location.y, null); + + // Draw troop count + if(region.getTroopCount() > 0) { + BufferedImage unitIcon = resources.getUnitIcon(); + String str = String.valueOf(region.getTroopCount()); + Dimension strDimensions = View.calculateTextSize(str, g.getFont()); + Font troopCountFont = new Font(g.getFont().getName(), Font.BOLD, 15); + FontMetrics fm = g.getFontMetrics(troopCountFont); + + int totalWidth = strDimensions.width + 2 + unitIcon.getWidth(); + int textX = location.x + (castle.getWidth() - totalWidth) / 2; + int textY = location.y + castle.getHeight(); + + g.setColor(Color.WHITE); + g.fillRoundRect(textX - 2, textY - 2, totalWidth + 4, unitIcon.getHeight() + 4, 5, 5); + g.setColor(Color.BLACK); + g.setFont(troopCountFont); + g.drawString(str, textX, textY + fm.getAscent()); + g.drawImage(unitIcon, textX + 2 + strDimensions.width, textY, null); + } + } + + // Draw overlay icon if highlighted + if(currentAction != Action.NONE && targetCastle != null && highlightedEdges != null && canPerformAction()) { + BufferedImage icon = (currentAction == Action.ATTACKING ? resources.getSwordsIcon() : resources.getArrowIcon()); + Point targetLocation = translate(targetCastle.getLocationOnMap()); + int x = targetLocation.x + (CASTLE_SIZE - ICON_SIZE) / 2; + int y = targetLocation.y + (CASTLE_SIZE - ICON_SIZE) / 2; + g.drawImage(icon, x, y, ICON_SIZE, ICON_SIZE, null); + } + + // HUD + if (selectedCastle != null) { + + Point location = translate(selectedCastle.getLocationOnMap()); + g.setColor(selectedCastle.getOwner() == null ? Color.WHITE : selectedCastle.getOwner().getColor()); + g.drawRect(location.x - 5, location.y - 5, CASTLE_SIZE + 10, CASTLE_SIZE + 10); + + if(canPerformAction()) { + if (canChooseCastle()) { + BufferedImage icon = resources.getCheckIcon(); + Rectangle bounds = getBoundsIconCheck(location); + g.drawImage(icon, bounds.x, bounds.y, ICON_SIZE, ICON_SIZE, null); + } else if (selectedCastle.getOwner() == game.getCurrentPlayer() && game.getRound() > 1) { + boolean hasTroops = game.getCurrentPlayer().getRemainingTroops() > 0; + boolean canMove = selectedCastle.getTroopCount() > 1; + + BufferedImage plusIcon = hasTroops ? resources.getPlusIcon() : resources.getPlusIconDeactivated(); + BufferedImage swordsIcon = resources.getSwordsIcon(); + BufferedImage arrowIcon = canMove ? resources.getArrowIcon() : resources.getArrowIconDeactivated(); + + int totalWidth = 3 * (ICON_SIZE + 2); + int iconsX = location.x - 5 + (CASTLE_SIZE + 10 - totalWidth) / 2; + int iconsY = location.y - 6 - ICON_SIZE; + + BufferedImage icons[] = {plusIcon, arrowIcon, swordsIcon}; + for (int i = 0; i < icons.length; i++) + g.drawImage(icons[i], iconsX + (ICON_SIZE + 2) * i, iconsY, ICON_SIZE, ICON_SIZE, null); + } + } + } + } + } + } + + @Override + public void paint(Graphics g) { + super.paint(g); + + if(selectedCastle != null) { + + String titleText; + if(currentAction == Action.NONE) { + StringBuilder text = new StringBuilder(); + text.append(selectedCastle.getName()); + if (selectedCastle.getOwner() != null) + text.append(" - Besitzer: ").append(selectedCastle.getOwner().getName()); + if (selectedCastle.getTroopCount() > 0) + text.append(" - Truppen: ").append(selectedCastle.getTroopCount()); + + titleText = text.toString(); + } else if(currentAction == Action.MOVING) { + titleText = "Truppen verschieben"; + } else if(currentAction == Action.ATTACKING) { + titleText = "Eine Burg angreifen"; + } else { + return; + } + + Font font = View.createFont(20); + g.setFont(font); + Dimension titleSize = View.calculateTextSize(titleText, font); + titleSize.width += 6; + titleSize.height += 3; + + Point textPos = (new Point((MapPanel.this.getWidth() - titleSize.width) / 2, -5)); + g.setColor(Color.WHITE); + g.fillRect(textPos.x, textPos.y , titleSize.width, titleSize.height); + g.setColor(Color.BLACK); + g.drawString(titleText, textPos.x + 3, textPos.y + titleSize.height - 5); + } + } + + public void clearSelection() { + this.selectedCastle = null; + repaint(); + } + + public void reset() { + currentAction = MapPanel.Action.NONE; + selectedCastle = null; + highlightedEdges = null; + targetCastle = null; + setCursor(Cursor.getDefaultCursor()); + repaint(); + } +} diff --git a/Projektgruppe_XXX/src/gui/components/NumberChooser.java b/Projektgruppe_XXX/src/gui/components/NumberChooser.java new file mode 100644 index 0000000..132478b --- /dev/null +++ b/Projektgruppe_XXX/src/gui/components/NumberChooser.java @@ -0,0 +1,129 @@ +package gui.components; + +import gui.View; + +import java.awt.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.*; + +public class NumberChooser extends JComponent implements MouseListener { + + private int min, max, value; + private boolean minClicked, maxClicked; + private List valueListeners; + + public NumberChooser(int min, int max, int val) { + this.min = min; + this.max = max; + this.setValue(val); + this.valueListeners = new LinkedList<>(); + this.addMouseListener(this); + } + + private void setValue(int val) { + int prev = this.value; + this.value = clamp(val, min, max); + repaint(); + if(prev != this.value && this.valueListeners != null) + this.valueListeners.forEach(l -> l.valueChanged(prev, value)); + } + + public int getValue() { + return this.value; + } + + public void decrement() { + this.setValue(this.getValue() - 1); + } + + public void increment() { + this.setValue(this.getValue() + 1); + } + + public void addValueListener(ValueListener valueListener) { + this.valueListeners.add(valueListener); + } + + private static int clamp(int val, int min, int max) { + return Math.max(Math.min(val, max), min); + } + + private int getButtonWidth() { + return Math.max(25, (int) ((getWidth() - 1) * 0.1)); + } + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + int w = getWidth() - 1; + int h = getHeight() - 1; + int buttonWidth = getButtonWidth(); + + // Borders + g.setColor(Color.DARK_GRAY); + g.drawRect(0, 0, w, h); + g.drawRect(0, 0, buttonWidth, h); + g.drawRect(w - buttonWidth, 0, buttonWidth, h); + + // Background Colors + g.setColor(minClicked ? Color.GRAY : Color.LIGHT_GRAY); + g.fillRect(1, 1, buttonWidth - 1, h - 1); + g.setColor(maxClicked ? Color.GRAY : Color.LIGHT_GRAY); + g.fillRect(w - buttonWidth + 1, 1, buttonWidth - 1, h - 1); + + // Text + Font font = View.createFont(16); + FontMetrics fm = g.getFontMetrics(font); + g.setColor(Color.BLACK); + g.setFont(font); + + Dimension dim1 = View.calculateTextSize("-", font); + Dimension dim2 = View.calculateTextSize("+", font); + Dimension dim3 = View.calculateTextSize(String.valueOf(value), font); + + g.drawString("-", (int) ((buttonWidth - dim1.getWidth()) / 2) - 1, (int)((h - dim1.getHeight()) / 2 + fm.getAscent()) - 1); + g.drawString("+", w - buttonWidth + (int) ((buttonWidth - dim2.getWidth()) / 2), (int)((h - dim2.getHeight()) / 2 + fm.getAscent()) - 1); + g.drawString(String.valueOf(value), buttonWidth + (int) ((w - 2 * buttonWidth - dim3.getWidth()) / 2) - 1, (int)((h - dim3.getHeight()) / 2 + fm.getAscent()) - 1); + } + + @Override + public void mouseClicked(MouseEvent mouseEvent) { + if(mouseEvent.getButton() == MouseEvent.BUTTON1) { + int x = mouseEvent.getX(); + + if(x < getButtonWidth()) + decrement(); + else if(x >= getWidth() - getButtonWidth()) + increment(); + } + } + + @Override + public void mousePressed(MouseEvent mouseEvent) { + if(mouseEvent.getButton() == MouseEvent.BUTTON1) { + int x = mouseEvent.getX(); + minClicked = x < getButtonWidth(); + maxClicked = x >= getWidth() - getButtonWidth(); + repaint(); + } + } + + @Override + public void mouseReleased(MouseEvent mouseEvent) { + if (mouseEvent.getButton() == MouseEvent.BUTTON1) { + minClicked = false; + maxClicked = false; + repaint(); + } + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) { } + @Override + public void mouseExited(MouseEvent mouseEvent) { } +} diff --git a/Projektgruppe_XXX/src/gui/components/NumberDialog.java b/Projektgruppe_XXX/src/gui/components/NumberDialog.java new file mode 100644 index 0000000..d0d738d --- /dev/null +++ b/Projektgruppe_XXX/src/gui/components/NumberDialog.java @@ -0,0 +1,39 @@ +package gui.components; + +import javax.swing.*; +import java.util.InputMismatchException; + +public class NumberDialog { + + private String text; + private int min, max, value; + + public NumberDialog(String text, int min, int max, int initial) { + this.text = text; + this.min = min; + this.max = max; + this.value = initial; + } + + boolean showDialog(JComponent parent) { + String result = JOptionPane.showInputDialog(parent, text, String.valueOf(value)); + + if(result == null) + return false; + + result = result.trim(); + try { + value = Integer.parseInt(result); + if(value >= min && value <= max) + return true; + } catch(NumberFormatException | InputMismatchException ignored) { + } + + JOptionPane.showMessageDialog(parent, "Bitte gib eine gültige Zahl ein.", "Ungültige Eingabe", JOptionPane.ERROR_MESSAGE); + return false; + } + + public int getValue() { + return this.value; + } +} diff --git a/Projektgruppe_XXX/src/gui/components/ValueListener.java b/Projektgruppe_XXX/src/gui/components/ValueListener.java new file mode 100644 index 0000000..f1da28d --- /dev/null +++ b/Projektgruppe_XXX/src/gui/components/ValueListener.java @@ -0,0 +1,6 @@ +package gui.components; + +public interface ValueListener { + + void valueChanged(Object oldValue, Object newValue); +} diff --git a/Projektgruppe_XXX/src/gui/views/GameMenu.java b/Projektgruppe_XXX/src/gui/views/GameMenu.java new file mode 100644 index 0000000..45bfe0c --- /dev/null +++ b/Projektgruppe_XXX/src/gui/views/GameMenu.java @@ -0,0 +1,220 @@ +package gui.views; + +import game.*; +import game.map.MapSize; +import gui.GameWindow; +import gui.View; +import gui.components.ColorChooserButton; +import gui.components.NumberChooser; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.font.TextAttribute; +import java.util.*; + +import javax.swing.*; + +public class GameMenu extends View { + + private JLabel lblTitle; + private JLabel lblPlayerCount; + private JLabel lblMapSize; + private JLabel lblGoal; + private JTextArea lblGoalDescription; + + private NumberChooser playerCount; + private JComboBox mapSize; + private JComboBox goal; + private JComponent[][] playerConfig; + private JButton btnStart, btnBack; + + // map size, type? + // goal? + + public GameMenu(GameWindow gameWindow) { + super(gameWindow); + } + + @Override + public void onResize() { + + int offsetY = 25; + int offsetX = 25; + + lblTitle.setLocation(offsetX, offsetY); + offsetY += 50; + + int columnWidth = Math.max(300, (getWidth() - 75) / 2); + + // Column 1 + offsetX = (getWidth() - 2*columnWidth - 25) / 2 + (columnWidth - 350) / 2; + lblPlayerCount.setLocation(offsetX, offsetY + 2); + playerCount.setLocation(offsetX + lblPlayerCount.getWidth() + 10, offsetY); + offsetY += 50; + + for(int i = 0; i < GameConstants.MAX_PLAYERS; i++) { + int tempOffsetX = offsetX; + for(JComponent c : playerConfig[i]) { + c.setLocation(tempOffsetX, offsetY); + tempOffsetX += c.getWidth() + 10; + c.setEnabled(i < playerCount.getValue()); + } + + offsetY += 40; + } + + // Column 2 + offsetY = 125 - lblMapSize.getHeight(); + offsetX = (getWidth() - 2*columnWidth - 25) / 2 + columnWidth + 25 + (columnWidth - mapSize.getWidth()) / 2; + lblMapSize.setLocation(offsetX, offsetY); offsetY += lblMapSize.getHeight(); + mapSize.setLocation(offsetX, offsetY); offsetY += mapSize.getHeight() + 10; + lblGoal.setLocation(offsetX, offsetY); offsetY += lblGoal.getHeight(); + goal.setLocation(offsetX, offsetY); offsetY += goal.getHeight(); + lblGoalDescription.setLocation(offsetX, offsetY); + lblGoalDescription.setSize(goal.getWidth() + 25, getHeight() - offsetY - BUTTON_SIZE.height - 50); + + // Button bar + offsetY = this.getHeight() - BUTTON_SIZE.height - 25; + offsetX = (this.getWidth() - 2*BUTTON_SIZE.width - 25) / 2; + btnBack.setLocation(offsetX, offsetY); + btnStart.setLocation(offsetX + BUTTON_SIZE.width + 25, offsetY); + } + + @Override + protected void onInit() { + + // Title + lblTitle = createLabel("Neues Spiel starten", 25, true); + + // Player Count + lblPlayerCount = createLabel("Anzahl Spieler:", 16); + playerCount = new NumberChooser(2, GameConstants.MAX_PLAYERS, 2); + playerCount.setSize(125, 25); + playerCount.addValueListener((oldValue, newValue) -> onResize()); + add(playerCount); + + // Player rows: + // [Number] [Color] [Name] [Human/AI] (Team?) + Vector playerTypes = new Vector<>(); + for(Class c : GameConstants.PLAYER_TYPES) + playerTypes.add(c.getSimpleName()); + + playerConfig = new JComponent[GameConstants.MAX_PLAYERS][]; + for(int i = 0; i < GameConstants.MAX_PLAYERS; i++) { + playerConfig[i] = new JComponent[] { + createLabel(String.format("%d.", i + 1),16), + new ColorChooserButton(GameConstants.PLAYER_COLORS[i]), + new JTextField(String.format("Spieler %d", i + 1)), + new JComboBox<>(playerTypes) + }; + + playerConfig[i][1].setSize(25, 25); + playerConfig[i][2].setSize(200, 25); + playerConfig[i][3].setSize(125, 25); + + for(JComponent c : playerConfig[i]) + add(c); + } + + // GameMap config + lblMapSize = createLabel("Kartengröße", 16); + mapSize = createCombobox(MapSize.getMapSizes(), MapSize.MEDIUM.ordinal()); + + // Goals + Vector goalNames = new Vector<>(); + for(Goal goal : GameConstants.GAME_GOALS) + goalNames.add(goal.getName()); + + lblGoal = createLabel("Mission", 16); + lblGoalDescription = createTextArea(GameConstants.GAME_GOALS[0].getDescription(), true); + + goal = createCombobox(goalNames, 0); + goal.addItemListener(itemEvent -> { + int i = goal.getSelectedIndex(); + if(i < 0 || i >= GameConstants.GAME_GOALS.length) + lblGoalDescription.setText(""); + else + lblGoalDescription.setText(GameConstants.GAME_GOALS[i].getDescription()); + }); + + // Buttons + btnBack = createButton("Zurück"); + btnStart = createButton("Starten"); + + getWindow().setSize(750, 450); + getWindow().setMinimumSize(new Dimension(750, 450)); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + if(actionEvent.getSource() == btnBack) + getWindow().setView(new StartScreen(getWindow())); + else if(actionEvent.getSource() == btnStart) { + + try { + + // Check Inputs + int playerCount = this.playerCount.getValue(); + int mapSize = this.mapSize.getSelectedIndex(); + int goalIndex = this.goal.getSelectedIndex(); + + // Should never happen + if (playerCount < 2 || playerCount > GameConstants.MAX_PLAYERS) { + showErrorMessage("Bitte geben Sie eine gültige Spielerzahl an.", "Ungültige Eingaben"); + return; + } + + // Should never happen + if (mapSize < 0 || mapSize >= MapSize.values().length) { + showErrorMessage("Bitte geben Sie eine gültige Kartengröße an.", "Ungültige Eingaben"); + return; + } + + // Should never happen + if (goalIndex < 0 || goalIndex >= GameConstants.GAME_GOALS.length) { + showErrorMessage("Bitte geben Sie ein gültiges Spielziel an.", "Ungültige Eingaben"); + return; + } + + // Create Players + Game game = new Game(); + for (int i = 0; i < playerCount; i++) { + String name = ((JTextField) playerConfig[i][2]).getText().replaceAll(";", "").trim(); + if (name.isEmpty()) { + showErrorMessage(String.format("Bitte geben Sie einen gültigen Namen für Spieler %d an.", i + 1), "Ungültige Eingaben"); + return; + } + + Color color = ((ColorChooserButton) playerConfig[i][1]).getSelectedColor(); + int playerType = ((JComboBox) playerConfig[i][3]).getSelectedIndex(); + + if (playerType < 0 || playerType >= GameConstants.PLAYER_TYPES.length) { + showErrorMessage(String.format("Bitte geben Sie einen gültigen Spielertyp für Spieler %d an.", i + 1), "Ungültige Eingaben"); + return; + } + + Player player = Player.createPlayer(GameConstants.PLAYER_TYPES[playerType], name, color); + if (player == null) { + showErrorMessage(String.format("Fehler beim Erstellen von Spieler %d", i + 1), "Unbekannter Fehler"); + return; + } + + game.addPlayer(player); + } + + // Set Goal + Goal goal = GameConstants.GAME_GOALS[goalIndex]; + GameView gameView = new GameView(getWindow(), game); + game.setMapSize(MapSize.values()[mapSize]); + game.setGoal(goal); + game.start(gameView); + getWindow().setView(gameView); + } catch(IllegalArgumentException ex) { + ex.printStackTrace(); + showErrorMessage("Fehler beim Erstellen des Spiels: " + ex.getMessage(), "Interner Fehler"); + } + } + } +} diff --git a/Projektgruppe_XXX/src/gui/views/GameView.java b/Projektgruppe_XXX/src/gui/views/GameView.java new file mode 100644 index 0000000..79e0746 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/views/GameView.java @@ -0,0 +1,318 @@ +package gui.views; + +import java.awt.*; +import java.awt.event.ActionEvent; + +import javax.swing.*; +import javax.swing.border.LineBorder; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +import game.AI; +import game.Game; +import game.GameInterface; +import game.Player; +import game.map.Castle; +import gui.GameWindow; +import gui.View; +import gui.components.DicePanel; +import gui.components.MapPanel; + +public class GameView extends View implements GameInterface { + + private MapPanel map; + private JScrollPane scrollLog; + private JTextPane txtStats; + private DicePanel dices; + private JTextPane gameLog; + private JButton button; + private Game game; + + GameView(GameWindow gameWindow, Game game) { + super(gameWindow); + this.game = game; + } + + private int sidebarWidth() { + return (int) Math.max(getWidth() * 0.15, 300); + } + + private Dimension mapPanelSize() { + int w = getWidth(); + int h = getHeight(); + return new Dimension(w - sidebarWidth() - 40, h - 20); + } + + @Override + public void onResize() { + + int w = getWidth(); + int h = getHeight(); + + int sidebarWidth = sidebarWidth(); + Dimension mapPanelSize = mapPanelSize(); + this.map.setBounds(10, 10, mapPanelSize.width, mapPanelSize.height); + this.map.revalidate(); + this.map.repaint(); + + int x = w - sidebarWidth - 20; + int y = 10; + + txtStats.setSize(sidebarWidth, 50 + 20 * game.getPlayers().size()); + dices.setSize(sidebarWidth, 50); + scrollLog.setSize(sidebarWidth, h - txtStats.getHeight() - dices.getHeight() - 50 - BUTTON_SIZE.height); + scrollLog.revalidate(); + scrollLog.repaint(); + + button.setSize(sidebarWidth, BUTTON_SIZE.height); + + JComponent components[] = { txtStats, dices, scrollLog, button}; + for(JComponent component : components) { + component.setLocation(x, y); + y += 10 + component.getHeight(); + } + } + + @Override + protected void onInit() { + + this.add(this.map = new MapPanel(this, getWindow().getResources())); + this.map.showConnections(true); + + this.txtStats = createTextPane(); + this.txtStats.addStyle("PlayerColors", null); + this.add(txtStats); + this.dices = new DicePanel(getWindow().getResources()); + this.dices.setBorder(new LineBorder(Color.BLACK)); + this.add(dices); + this.gameLog = createTextPane(); + this.gameLog.addStyle("PlayerColor", null); + this.scrollLog = new JScrollPane(gameLog); + this.add(scrollLog); + this.button = createButton("Nächste Runde"); + + getWindow().setSize(1080, 780); + getWindow().setMinimumSize(new Dimension(750, 450)); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + if(actionEvent.getSource() == button) { + + switch (button.getText()) { + case "Nächste Runde": + + if (game.getCurrentPlayer() instanceof AI) + return; + + if (game.getCurrentPlayer().getRemainingTroops() > 0) { + if (game.getRound() == 1) { + int troops = game.getCurrentPlayer().getRemainingTroops(); + JOptionPane.showMessageDialog(this, String.format("Du musst noch %s auswählen.", + troops == 1 ? "eine Burg" : troops + " Burgen"), + "Burgen auswählen", JOptionPane.WARNING_MESSAGE); + return; + } else { + int choice = JOptionPane.showOptionDialog(this, "Du hast noch unverteilte Truppen.\nBist du dir sicher, dass du die Runde beenden möchtest?", + "Runde beenden?", + JOptionPane.YES_NO_OPTION, + JOptionPane.INFORMATION_MESSAGE, + null, new String[]{"Runde beenden", "Abbrechen"}, "Runde beenden"); + + if (choice == 1) + return; + } + } + + game.nextTurn(); + break; + + case "Beenden": + getWindow().setView(new StartScreen(getWindow())); + break; + + case "Überspringen": + if(game.getAttackThread() != null) + game.getAttackThread().fastForward(); + else if(game.getCurrentPlayer() instanceof AI) { + ((AI)game.getCurrentPlayer()).fastForward(); + } + + break; + } + } + } + + public void updateStats() { + txtStats.setText(""); + StyledDocument doc = txtStats.getStyledDocument(); + Style style = doc.getStyle("PlayerColors"); + + try { + doc.insertString(doc.getLength(), String.format("Runde: %d", game.getRound()), null); + if (game.getCurrentPlayer() != null) { + doc.insertString(doc.getLength(), ", Am Zug: ", null); + StyleConstants.setForeground(style, game.getCurrentPlayer().getColor()); + doc.insertString(doc.getLength(), game.getCurrentPlayer().getName(), style); + } + + doc.insertString(doc.getLength(), "\n\nName\tPunkte\tBurgen\tTruppen\n", null); + for (Player p : game.getPlayers()) { + StyleConstants.setForeground(style, p.getColor()); + doc.insertString(doc.getLength(), p.getName(), style); + doc.insertString(doc.getLength(), String.format(":\t%d\t%d\t%d\n", p.getPoints(), p.getNumRegions(game), p.getRemainingTroops()), null); + } + } catch(BadLocationException ex) { + ex.printStackTrace(); + } + } + + private void logText(String text) { + Style style = this.gameLog.getStyle("PlayerColor"); + StyleConstants.setForeground(style, Color.BLACK); + StyledDocument doc = this.gameLog.getStyledDocument(); + + try { doc.insertString(doc.getLength(), text, style); } + catch (BadLocationException ignored) {} + } + + private void logLine(String line) { + this.logText(line + "\n"); + gameLog.setCaretPosition(gameLog.getDocument().getLength()); + } + + private void logLine(String line, Player... playerFormat) { + + StyledDocument doc = this.gameLog.getStyledDocument(); + Style style = this.gameLog.getStyle("PlayerColor"); + + int num = 0; + int index; + while(!line.isEmpty() && (index = line.indexOf("%PLAYER%")) != -1) { + + if(index > 0) + this.logText(line.substring(0, index)); + + if(num < playerFormat.length) { + Player insert = playerFormat[num++]; + StyleConstants.setForeground(style, insert.getColor()); + try { doc.insertString(doc.getLength(), insert.getName(), style); } + catch (BadLocationException ignored) {} + } + + line = line.substring(index + ("%PLAYER%").length()); + } + + logLine(line); + } + + @Override + public void onCastleChosen(Castle castle, Player player) { + logLine("%PLAYER% wählt " + castle.getName() + ".", player); + updateStats(); + map.repaint(); + } + + @Override + public void onNextTurn(Player currentPlayer, int troopsGot, boolean human) { + this.logLine("%PLAYER% ist am Zug.", currentPlayer); + + if(game.getRound() == 1) + this.logLine("%PLAYER% muss " + troopsGot + " Burgen auswählen.", currentPlayer); + else + this.logLine("%PLAYER% erhält " + troopsGot + " Truppen.", currentPlayer); + + map.clearSelection(); + updateStats(); + + button.setText(human ? "Nächste Runde" : "Überspringen"); + } + + @Override + public void onNewRound(int round) { + this.logLine(String.format("Runde %d.", round)); + } + + @Override + public void onGameOver(Player winner) { + if(winner == null) { + this.logLine("Spiel vorbei - Unentschieden."); + } else { + this.logLine("Spiel vorbei - %PLAYER% gewinnt das Spiel.", winner); + } + + button.setText("Beenden"); + updateStats(); + } + + @Override + public void onGameStarted(Game game) { + this.map.setGame(game); + this.gameLog.setText(""); + this.logLine("Neues Spiel gestartet."); + this.updateStats(); + + Dimension mapSize = game.getMap().getSize(); + Dimension panelSize = mapPanelSize(); + if(mapSize.getWidth() > panelSize.getWidth() || mapSize.getHeight() > panelSize.getHeight()) { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + int newWidth = Math.min(screenSize.width, mapSize.width) + getWidth() - panelSize.width + 50; + int newHeight = Math.min(screenSize.height, mapSize.height) + getHeight() - panelSize.height + 50; + + getWindow().setSize(newWidth, newHeight); + getWindow().setLocationRelativeTo(null); + } + } + + @Override + public void onConquer(Castle castle, Player player) { + logLine("%PLAYER% erobert " + castle.getName(), player); + updateStats(); + map.repaint(); + } + + @Override + public void onUpdate() { + updateStats(); + map.repaint(); + } + + @Override + public void onAddScore(Player player, int score) { + updateStats(); + } + + @Override + public int[] onRoll(Player player, int dices, boolean fastForward) { + try { + int[] roll = this.dices.generateRandom(dices, !fastForward); + StringBuilder rolls = new StringBuilder(); + rolls.append("%PLAYER% würfelt: "); + for(int i = 0; i < roll.length; i++) { + rolls.append(i == 0 ? " " : ", "); + rolls.append(roll[i]); + } + + logLine(rolls.toString(), player); + return roll; + } catch(InterruptedException ex) { + ex.printStackTrace(); + return new int[0]; + } + } + + @Override + public void onAttackStarted(Castle source, Castle target, int troopCount) { + button.setText("Überspringen"); + logLine("%PLAYER% greift " + target.getName() + " mit " + troopCount + " Truppen an.", source.getOwner()); + } + + @Override + public void onAttackStopped() { + map.reset(); + updateStats(); + button.setText("Nächste Runde"); + } +} diff --git a/Projektgruppe_XXX/src/gui/views/HighscoreView.java b/Projektgruppe_XXX/src/gui/views/HighscoreView.java new file mode 100644 index 0000000..c2ca523 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/views/HighscoreView.java @@ -0,0 +1,67 @@ +package gui.views; + +import game.ScoreEntry; +import gui.GameWindow; +import gui.Resources; +import gui.View; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.text.SimpleDateFormat; + +public class HighscoreView extends View { + + private JButton btnBack; + // private JTextPane txtScores; + private JTable scoreTable; + private JLabel lblTitle; + private JScrollPane scrollPane; + + public HighscoreView(GameWindow gameWindow) { + super(gameWindow); + } + + @Override + public void onResize() { + int offsetY = 25; + lblTitle.setLocation((getWidth() - lblTitle.getWidth()) / 2, offsetY); offsetY += lblTitle.getSize().height + 25; + // txtScores.setLocation(25, offsetY); + // txtScores.setSize(getWidth() - 50, getHeight() - 50 - BUTTON_SIZE.height - offsetY); + scrollPane.setLocation(25, offsetY); + scrollPane.setSize(getWidth() - 50, getHeight() - 50 - BUTTON_SIZE.height - offsetY); + + btnBack.setLocation((getWidth() - BUTTON_SIZE.width) / 2, getHeight() - BUTTON_SIZE.height - 25); + } + + @Override + protected void onInit() { + btnBack = createButton("Zurück"); + lblTitle = createLabel("Highscores", 25, true); + + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); + Resources resources = Resources.getInstance(); + String[] columns = { "Datum", "Name", "Spielmodus", "Punkte" }; + Object[][] data = new Object[resources.getScoreEntries().size()][]; + for(int i = 0; i < resources.getScoreEntries().size(); i++) { + ScoreEntry scoreEntry = resources.getScoreEntries().get(i); + data[i] = new Object[] { + simpleDateFormat.format(scoreEntry.getDate()), + scoreEntry.getName(), + scoreEntry.getMode(), + scoreEntry.getScore() + }; + } + + scoreTable = new JTable(data, columns); + scoreTable.setBackground(this.getBackground()); + scoreTable.setFillsViewportHeight(true); + + scrollPane = new JScrollPane(scoreTable); + add(scrollPane); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + getWindow().setView(new StartScreen(getWindow())); + } +} diff --git a/Projektgruppe_XXX/src/gui/views/InfoView.java b/Projektgruppe_XXX/src/gui/views/InfoView.java new file mode 100644 index 0000000..d6ea227 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/views/InfoView.java @@ -0,0 +1,67 @@ +package gui.views; + +import gui.GameWindow; +import gui.View; + +import javax.swing.*; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; +import java.awt.event.ActionEvent; + +public class InfoView extends View { + + private static final String ABOUT_TEXT = + "~ Game of Castles ~\nFOP-Projekt WiSe 18/19\n" + + "Hauptautor: Roman Hergenreder\n" + + "Mitwirkende: Philipp Imperatori, Nils Nedderhut, Louis Neumann\n" + + "Icons/Bilder: Smashicons, Freepick, Retinaicons, Skyclick (www.flaticon.com)\n" + + "Keine Haftung für Bugs, Systemabstürze, Datenverlust und rauchende Grafikkarten\n\n" + + "HowTo:\n" + + "Bevor ein neues Spiel gestartet werden kann, müssen Sie 2-4 Spieler sowie die Kartengröße und die Spielmission festlegen. " + + "Es ist auch möglich, ein Programm als Spieler einzustellen (z.B. BasicAI). " + + "Anschließend wird eine Karte generiert. In der ersten Runde müssen abwechselnd 3 Burgen ausgewählt werden. Nachdem alle Burgen " + + "verteilt wurden, beginnt das eigentliche Spiel. Sie haben die Möglichkeit neue Truppen auf Ihre Burgen aufzuteilen, Truppen zwischen " + + "Ihren Burgen zu bewegen sowie andere Burgen anzugreifen. Bei der Standardmission 'Eroberung' gewinnt der Spieler, der zuerst alle Burgen " + + "eingenommen hat."; + + private JButton btnBack; + private JTextPane txtInfo; + private JLabel lblTitle; + + public InfoView(GameWindow gameWindow) { + super(gameWindow); + } + + @Override + public void onResize() { + + int offsetY = 25; + lblTitle.setLocation((getWidth() - lblTitle.getWidth()) / 2, offsetY); offsetY += lblTitle.getSize().height + 25; + txtInfo.setLocation(25, offsetY); + txtInfo.setSize(getWidth() - 50, getHeight() - 50 - BUTTON_SIZE.height - offsetY); + + btnBack.setLocation((getWidth() - BUTTON_SIZE.width) / 2, getHeight() - BUTTON_SIZE.height - 25); + } + + @Override + protected void onInit() { + btnBack = createButton("Zurück"); + lblTitle = createLabel("Über", 25, true); + txtInfo = createTextPane(); + txtInfo.setText(ABOUT_TEXT); + txtInfo.setBorder(null); + txtInfo.setBackground(this.getBackground()); + add(txtInfo); + + StyledDocument doc = txtInfo.getStyledDocument(); + SimpleAttributeSet center = new SimpleAttributeSet(); + StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER); + doc.setParagraphAttributes(0, doc.getLength(), center, false); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + getWindow().setView(new StartScreen(getWindow())); + } +} diff --git a/Projektgruppe_XXX/src/gui/views/StartScreen.java b/Projektgruppe_XXX/src/gui/views/StartScreen.java new file mode 100644 index 0000000..fcfda93 --- /dev/null +++ b/Projektgruppe_XXX/src/gui/views/StartScreen.java @@ -0,0 +1,79 @@ +package gui.views; + +import gui.GameWindow; +import gui.View; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.image.BufferedImage; + +public class StartScreen extends View { + + private JButton btnStart, btnStats, btnInfo, btnQuit; + private JLabel lblTitle; + + public StartScreen(GameWindow gameWindow) { + super(gameWindow); + } + + @Override + public void onResize() { + int width = getWidth(); + int height = getHeight(); + int labelHeight = 40; + + int offsetY = (height - 4 * (BUTTON_SIZE.height + 15) - labelHeight) / 3; + + lblTitle.setSize(width, labelHeight); + lblTitle.setLocation(0, offsetY); + + offsetY += labelHeight + 50; + + int offsetX = (width - BUTTON_SIZE.width) / 2; + JButton[] buttons = { btnStart, btnStats, btnInfo, btnQuit }; + for (JButton button : buttons) { + button.setLocation(offsetX, offsetY); + offsetY += BUTTON_SIZE.height + 15; + } + } + + @Override + protected void onInit() { + this.lblTitle = createLabel("Game of Castles", 25); + this.lblTitle.setHorizontalAlignment(SwingConstants.CENTER); + this.lblTitle.setFont(View.createCelticFont(25)); + this.btnStart = createButton("Start"); + this.btnStats = createButton("Punkte"); + this.btnInfo = createButton("Info"); + this.btnQuit = createButton("Beenden"); + + getWindow().setSize(750, 450); + getWindow().setMinimumSize(new Dimension(600, 400)); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + if(actionEvent.getSource() == btnQuit) { + getWindow().dispose(); + } else if(actionEvent.getSource() == btnStart) { + getWindow().setView(new GameMenu(getWindow())); + } else if(actionEvent.getSource() == btnInfo) { + getWindow().setView(new InfoView(getWindow())); + } else if(actionEvent.getSource() == btnStats) { + getWindow().setView(new HighscoreView(getWindow())); + } + } + + @Override + public void paint(Graphics g) { + super.paint(g); + + BufferedImage soldiers[] = getWindow().getResources().getSoldiers(); + int width = 200; + int height = (int) (((double)width / soldiers[0].getWidth()) * soldiers[0].getHeight()); + + g.drawImage(soldiers[0], 25, 100, width, height, null); + g.drawImage(soldiers[1], getWidth() - 25 - width, 100, width, height, null); + } +} diff --git a/Projektgruppe_XXX/src/tests/student/GraphConnectionTest.java b/Projektgruppe_XXX/src/tests/student/GraphConnectionTest.java new file mode 100644 index 0000000..6c3f261 --- /dev/null +++ b/Projektgruppe_XXX/src/tests/student/GraphConnectionTest.java @@ -0,0 +1,5 @@ +package tests.student; + +public class GraphConnectionTest { + +}