diff --git a/doc/Coordinates-Cheat-Sheet.svg b/doc/Coordinates-Cheat-Sheet.svg new file mode 100644 index 0000000..379786b --- /dev/null +++ b/doc/Coordinates-Cheat-Sheet.svg @@ -0,0 +1,5333 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + 1 + 2 + 3 + + + + 4 + 5 + 6 + + + + 7 + 8 + 9 + + + + 10 + 11 + 12 + + + + 13 + 14 + 0 + + + + + + + + + + + + + + + + + + 0 + 1 + 3 + 4 + 6 + 7 + 9 + 10 + 12 + 13 + 15 + 16 + 18 + 19 + 21 + 22 + + + + + + + + 2 + 5 + 8 + 11 + 14 + 17 + 20 + + diff --git a/lib/slf4j-api-2.0.0-alpha1.jar b/lib/slf4j-api-2.0.0-alpha1.jar new file mode 100644 index 0000000..c38dbb5 Binary files /dev/null and b/lib/slf4j-api-2.0.0-alpha1.jar differ diff --git a/lib/text-io-3.4.0.jar b/lib/text-io-3.4.0.jar new file mode 100644 index 0000000..2d0806a Binary files /dev/null and b/lib/text-io-3.4.0.jar differ diff --git a/src/ch/zhaw/catan/Config.java b/src/ch/zhaw/catan/Config.java new file mode 100644 index 0000000..2be09db --- /dev/null +++ b/src/ch/zhaw/catan/Config.java @@ -0,0 +1,254 @@ +package ch.zhaw.catan; + +import java.awt.Point; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * This class specifies the most important and basic parameters of the game + * Catan. + *

+ * The class provides definitions such as for the type and number of resource + * cards or the number of available road elements per player. Furthermore, it + * provides a dice number to field and a field to land type mapping for the + * standard setup detailed here + *

+ * @author tebe + * + */ +public class Config { + // Minimum number of players + // Note: The max. number is equal to the number of factions (see Faction enum) + public static final int MIN_NUMBER_OF_PLAYERS = 2; + + // Initial thief position (on the desert field) + public static final Point INITIAL_THIEF_POSITION = new Point(7, 11); + + // Available factions + public enum Faction { + RED("rr"), BLUE("bb"), GREEN("gg"), YELLOW("yy"); + + private String name; + + private Faction(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + // RESOURCE CARD DECK + public static final Map INITIAL_RESOURCE_CARDS_BANK = Map.of(Resource.LUMBER, 19, + Resource.BRICK, 19, Resource.WOOL, 19, Resource.GRAIN, 19, Resource.ORE, 19); + + // SPECIFICATION OF AVAILABLE RESOURCE TYPES + /** + * This {@link Enum} specifies the available resource types in the game. + * + * @author tebe + */ + public enum Resource { + GRAIN("GR"), WOOL("WL"), LUMBER("LU"), ORE("OR"), BRICK("BR"); + + private String name; + + private Resource(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + // SPECIFICATION OF AVAILABLE LAND TYPES + /** + * This {@link Enum} specifies the available lands in the game. Some land types + * produce resources (e.g., {@link Land#FOREST}, others do not (e.g., + * {@link Land#WATER}. + * + * @author tebe + */ + public enum Land { + FOREST(Resource.LUMBER), PASTURE(Resource.WOOL), FIELDS(Resource.GRAIN), + MOUNTAIN(Resource.ORE), HILLS(Resource.BRICK), WATER("~~"), DESERT("--"); + + private Resource resource = null; + private String name; + + private Land(Resource resource) { + this(resource.toString()); + this.resource = resource; + } + + private Land(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + + /** + * Returns the {@link Resource} that this land provides or null, + * if it does not provide any. + * + * @return the {@link Resource} or null + */ + public Resource getResource() { + return resource; + } + } + + // STRUCTURES (with costs) + private static final int NUMBER_OF_ROADS_PER_PLAYER = 15; + private static final int NUMBER_OF_SETTLEMENTS_PER_PLAYER = 5; + private static final int NUMBER_OF_CITIES_PER_PLAYER = 4; + public static final int MAX_CARDS_IN_HAND_NO_DROP = 7; + + /** + * This enum models the different structures that can be built. + *

+ * The enum provides information about the cost of a structure and how many of + * these structures are available per player. + *

+ */ + public enum Structure { + SETTLEMENT(List.of(Resource.LUMBER, Resource.BRICK, Resource.WOOL, Resource.GRAIN), + NUMBER_OF_SETTLEMENTS_PER_PLAYER), + CITY(List.of(Resource.ORE, Resource.ORE, Resource.ORE, Resource.GRAIN, Resource.GRAIN), + NUMBER_OF_CITIES_PER_PLAYER), + ROAD(List.of(Resource.LUMBER, Resource.BRICK), NUMBER_OF_ROADS_PER_PLAYER); + + private List costs; + private int stockPerPlayer; + + private Structure(List costs, int stockPerPlayer) { + this.costs = costs; + this.stockPerPlayer = stockPerPlayer; + } + + /** + * Returns the build costs of this structure. + *

+ * Each list entry represents a resource card. The value of an entry (e.g., {@link Resource#LUMBER}) + * identifies the resource type of the card. + *

+ * @return the build costs + */ + public List getCosts() { + return costs; + } + + /** + * Returns the build costs of this structure. + * + * @return the build costs in terms of the number of resource cards per resource type + */ + public Map getCostsAsMap() { + return costs.stream() + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + } + + /** + * Returns the number of pieces that are available of a certain structure (per + * player). For example, there are {@link Config#NUMBER_OF_ROADS_PER_PLAYER} + * pieces of the structure {@link Structure#ROAD} per player. + * + * + * @return the stock per player + */ + public int getStockPerPlayer() { + return stockPerPlayer; + } + } + + // STANDARD FIXED DICE NUMBER TO FIELD SETUP + /** + * Returns a mapping of the dice values per field. + * + * @return the dice values per field + */ + public static final Map getStandardDiceNumberPlacement() { + Map assignment = new HashMap<>(); + assignment.put(new Point(4, 8), 2); + assignment.put(new Point(7, 5), 3); + assignment.put(new Point(8, 14), 3); + assignment.put(new Point(6, 8), 4); + assignment.put(new Point(7, 17), 4); + + assignment.put(new Point(3, 11), 5); + assignment.put(new Point(8, 8), 5); + assignment.put(new Point(5, 5), 6); + assignment.put(new Point(9, 11), 6); + + assignment.put(new Point(7, 11), 7); + assignment.put(new Point(9, 5), 8); + assignment.put(new Point(5, 17), 8); + assignment.put(new Point(5, 11), 9); + assignment.put(new Point(11, 11), 9); + assignment.put(new Point(4, 14), 10); + assignment.put(new Point(10, 8), 10); + assignment.put(new Point(6, 14), 11); + assignment.put(new Point(9, 17), 11); + assignment.put(new Point(10, 14), 12); + return Collections.unmodifiableMap(assignment); + } + + // STANDARD FIXED LAND SETUP + /** + * Returns the field (coordinate) to {@link Land} mapping for the standard + * setup of the game Catan.. + * + * @return the field to {@link Land} mapping for the standard setup + */ + public static final Map getStandardLandPlacement() { + Map assignment = new HashMap<>(); + Point[] water = { new Point(4, 2), new Point(6, 2), new Point(8, 2), new Point(10, 2), + new Point(3, 5), new Point(11, 5), new Point(2, 8), new Point(12, 8), new Point(1, 11), + new Point(13, 11), new Point(2, 14), new Point(12, 14), new Point(3, 17), new Point(11, 17), + new Point(4, 20), new Point(6, 20), new Point(8, 20), new Point(10, 20) }; + + for (Point p : water) { + assignment.put(p, Land.WATER); + } + + assignment.put(new Point(5, 5), Land.FOREST); + assignment.put(new Point(7, 5), Land.PASTURE); + assignment.put(new Point(9, 5), Land.PASTURE); + + assignment.put(new Point(4, 8), Land.FIELDS); + assignment.put(new Point(6, 8), Land.MOUNTAIN); + assignment.put(new Point(8, 8), Land.FIELDS); + assignment.put(new Point(10, 8), Land.FOREST); + + assignment.put(new Point(3, 11), Land.FOREST); + assignment.put(new Point(5, 11), Land.HILLS); + assignment.put(new Point(7, 11), Land.DESERT); + assignment.put(new Point(9, 11), Land.MOUNTAIN); + assignment.put(new Point(11, 11), Land.FIELDS); + + assignment.put(new Point(4, 14), Land.FIELDS); + assignment.put(new Point(6, 14), Land.MOUNTAIN); + assignment.put(new Point(8, 14), Land.FOREST); + assignment.put(new Point(10, 14), Land.PASTURE); + + assignment.put(new Point(5, 17), Land.PASTURE); + assignment.put(new Point(7, 17), Land.HILLS); + assignment.put(new Point(9, 17), Land.HILLS); + + return Collections.unmodifiableMap(assignment); + } + +} diff --git a/src/ch/zhaw/catan/Dummy.java b/src/ch/zhaw/catan/Dummy.java new file mode 100644 index 0000000..6f7f3f8 --- /dev/null +++ b/src/ch/zhaw/catan/Dummy.java @@ -0,0 +1,59 @@ +package ch.zhaw.catan; + +import ch.zhaw.catan.Config.Land; +import ch.zhaw.hexboard.Label; +import java.awt.Point; +import java.util.HashMap; +import java.util.Map; +import org.beryx.textio.TextIO; +import org.beryx.textio.TextIoFactory; +import org.beryx.textio.TextTerminal; + +public class Dummy { + + public enum Actions { + SHOW, QUIT + } + + private void run() { + TextIO textIO = TextIoFactory.getTextIO(); + TextTerminal textTerminal = textIO.getTextTerminal(); + + SiedlerBoard board = new SiedlerBoard(); + board.addField(new Point(2, 2), Land.FOREST); + board.setCorner(new Point(3, 3), "RR"); + board.setEdge(new Point(2, 0), new Point(3, 1), "r"); + board.addFieldAnnotation(new Point(2, 2), new Point(3, 1), "AA"); + + Map lowerFieldLabel = new HashMap<>(); + lowerFieldLabel.put(new Point(2, 2), new Label('0', '9')); + SiedlerBoardTextView view = new SiedlerBoardTextView(board); + + for (Map.Entry e : lowerFieldLabel.entrySet()) { + view.setLowerFieldLabel(e.getKey(), e.getValue()); + } + + boolean running = true; + while (running) { + switch (getEnumValue(textIO, Actions.class)) { + case SHOW: + textTerminal.println(view.toString()); + break; + case QUIT: + running = false; + break; + default: + throw new IllegalStateException("Internal error found - Command not implemented."); + } + } + textIO.dispose(); + } + + public static > T getEnumValue(TextIO textIO, Class commands) { + return textIO.newEnumInputReader(commands).read("What would you like to do?"); + } + + public static void main(String[] args) { + new Dummy().run(); + } +} diff --git a/src/ch/zhaw/catan/SiedlerBoard.java b/src/ch/zhaw/catan/SiedlerBoard.java new file mode 100644 index 0000000..9143967 --- /dev/null +++ b/src/ch/zhaw/catan/SiedlerBoard.java @@ -0,0 +1,37 @@ +package ch.zhaw.catan; + +import ch.zhaw.catan.Config.Land; +import ch.zhaw.hexboard.HexBoard; + +import java.awt.*; +import java.util.Collections; +import java.util.List; + +public class SiedlerBoard extends HexBoard { + + + //TODO: Add fields, constructors and methods as you see fit. Do NOT change the signature + // of the methods below. + + /** + * Returns the fields associated with the specified dice value. + * + * @param dice the dice value + * @return the fields associated with the dice value + */ + public List getFieldsForDiceValue(int dice) { + //TODO: Implement. + return Collections.emptyList(); + } + + /** + * Returns the {@link Land}s adjacent to the specified corner. + * + * @param corner the corner + * @return the list with the adjacent {@link Land}s + */ + public List getLandsForCorner(Point corner) { + //TODO: Implement. + return Collections.emptyList(); + } +} diff --git a/src/ch/zhaw/catan/SiedlerBoardTextView.java b/src/ch/zhaw/catan/SiedlerBoardTextView.java new file mode 100644 index 0000000..d8e3206 --- /dev/null +++ b/src/ch/zhaw/catan/SiedlerBoardTextView.java @@ -0,0 +1,12 @@ +package ch.zhaw.catan; + +import ch.zhaw.catan.Config.Land; +import ch.zhaw.hexboard.HexBoardTextView; + +public class SiedlerBoardTextView extends HexBoardTextView { + + public SiedlerBoardTextView(SiedlerBoard board) { + super(board); + } + +} diff --git a/src/ch/zhaw/catan/SiedlerGame.java b/src/ch/zhaw/catan/SiedlerGame.java new file mode 100644 index 0000000..a482921 --- /dev/null +++ b/src/ch/zhaw/catan/SiedlerGame.java @@ -0,0 +1,255 @@ +package ch.zhaw.catan; + +import ch.zhaw.catan.Config.Faction; +import ch.zhaw.catan.Config.Resource; +import java.awt.Point; +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +/** + * This class performs all actions related to modifying the game state. + * + * TODO: (your documentation) + * + * @author TODO + * + */ +public class SiedlerGame { + static final int FOUR_TO_ONE_TRADE_OFFER = 4; + static final int FOUR_TO_ONE_TRADE_WANT = 1; + + /** + * Constructs a SiedlerGame game state object. + * + * @param winPoints the number of points required to win the game + * @param numberOfPlayers the number of players + * + * @throws IllegalArgumentException if winPoints is lower than + * three or players is not between two and four + */ + public SiedlerGame(int winPoints, int numberOfPlayers) { + // TODO: Implement + } + + /** + * Switches to the next player in the defined sequence of players. + */ + public void switchToNextPlayer() { + // TODO: Implement + } + + /** + * Switches to the previous player in the defined sequence of players. + */ + public void switchToPreviousPlayer() { + // TODO: Implement + } + + /** + * Returns the {@link Faction}s of the active players. + * + *

The order of the player's factions in the list must + * correspond to the oder in which they play. + * Hence, the player that sets the first settlement must be + * at position 0 in the list etc. + * + * Important note: The list must contain the + * factions of active players only.

+ * + * @return the list with player's factions + */ + public List getPlayerFactions() { + // TODO: Implement + return Collections.emptyList(); + } + + + /** + * Returns the game board. + * + * @return the game board + */ + public SiedlerBoard getBoard() { + // TODO: Implement + return null; + } + + /** + * Returns the {@link Faction} of the current player. + * + * @return the faction of the current player + */ + public Faction getCurrentPlayerFaction() { + // TODO: Implement + return null; + } + + /** + * Returns how many resource cards of the specified type + * the current player owns. + * + * @param resource the resource type + * @return the number of resource cards of this type + */ + public int getCurrentPlayerResourceStock(Resource resource) { + // TODO: Implement + return 0; + } + + /** + * Places a settlement in the founder's phase (phase II) of the game. + * + *

The placement does not cost any resource cards. If payout is + * set to true, for each adjacent resource-producing field, a resource card of the + * type of the resource produced by the field is taken from the bank (if available) and added to + * the players' stock of resource cards.

+ * + * @param position the position of the settlement + * @param payout if true, the player gets one resource card per adjacent resource-producing field + * @return true, if the placement was successful + */ + public boolean placeInitialSettlement(Point position, boolean payout) { + // TODO: Implement + return false; + } + + /** + * Places a road in the founder's phase (phase II) of the game. + * The placement does not cost any resource cards. + * + * @param roadStart position of the start of the road + * @param roadEnd position of the end of the road + * @return true, if the placement was successful + */ + public boolean placeInitialRoad(Point roadStart, Point roadEnd) { + // TODO: Implement + return false; + } + + /** + * This method takes care of actions depending on the dice throw result. + * + * A key action is the payout of the resource cards to the players + * according to the payout rules of the game. This includes the + * "negative payout" in case a 7 is thrown and a player has more than + * {@link Config#MAX_CARDS_IN_HAND_NO_DROP} resource cards. + * + * If a player does not get resource cards, the list for this players' + * {@link Faction} is an empty list (not null)!. + * + *

+ * The payout rules of the game take into account factors such as, the number + * of resource cards currently available in the bank, settlement types + * (settlement or city), and the number of players that should get resource + * cards of a certain type (relevant if there are not enough left in the bank). + *

+ * + * @param dicethrow the resource cards that have been distributed to the players + * @return the resource cards added to the stock of the different players + */ + public Map> throwDice(int dicethrow) { + // TODO: Implement + return null; + } + + /** + * Builds a settlement at the specified position on the board. + * + *

The settlement can be built if: + *

    + *
  • the player possesses the required resource cards
  • + *
  • a settlement to place on the board
  • + *
  • the specified position meets the build rules for settlements
  • + *
+ * + * @param position the position of the settlement + * @return true, if the placement was successful + */ + public boolean buildSettlement(Point position) { + // TODO: Implement + return false; + } + + /** + * Builds a city at the specified position on the board. + * + *

The city can be built if: + *

    + *
  • the player possesses the required resource cards
  • + *
  • a city to place on the board
  • + *
  • the specified position meets the build rules for cities
  • + *
+ * + * @param position the position of the city + * @return true, if the placement was successful + */ + public boolean buildCity(Point position) { + // TODO: OPTIONAL task - Implement + return false; + } + + /** + * Builds a road at the specified position on the board. + * + *

The road can be built if: + *

    + *
  • the player possesses the required resource cards
  • + *
  • a road to place on the board
  • + *
  • the specified position meets the build rules for roads
  • + *
+ * + * @param roadStart the position of the start of the road + * @param roadEnd the position of the end of the road + * @return true, if the placement was successful + */ + public boolean buildRoad(Point roadStart, Point roadEnd) { + // TODO: Implement + return false; + } + + + /** + * Trades in {@link #FOUR_TO_ONE_TRADE_OFFER} resource cards of the + * offered type for {@link #FOUR_TO_ONE_TRADE_WANT} resource cards of the wanted type. + * + * The trade only works when bank and player possess the resource cards + * for the trade before the trade is executed. + * + * @param offer offered type + * @param want wanted type + * @return true, if the trade was successful + */ + public boolean tradeWithBankFourToOne(Resource offer, Resource want) { + // TODO: Implement + return false; + } + + /** + * Returns the winner of the game, if any. + * + * @return the winner of the game or null, if there is no winner (yet) + */ + public Faction getWinner() { + // TODO: Implement + return null; + } + + + /** + * Places the thief on the specified field and steals a random resource card (if + * the player has such cards) from a random player with a settlement at that + * field (if there is a settlement) and adds it to the resource cards of the + * current player. + * + * @param field the field on which to place the thief + * @return false, if the specified field is not a field or the thief cannot be + * placed there (e.g., on water) + */ + public boolean placeThiefAndStealCard(Point field) { + //TODO: Implement (or longest road functionality) + return false; + } + +} diff --git a/src/ch/zhaw/hexboard/Edge.java b/src/ch/zhaw/hexboard/Edge.java new file mode 100644 index 0000000..b83c810 --- /dev/null +++ b/src/ch/zhaw/hexboard/Edge.java @@ -0,0 +1,117 @@ +package ch.zhaw.hexboard; + +import java.awt.Point; + +/** + * This class models an edge on @see ch.zhaw.hexboard.HexBoard. + *

+ * Edges are non-directional and can be created by providing the two points that + * span an edge on the hex-grid defined by @see ch.zhaw.hexboard.HexBoard + *

+ * @author tebe + * + */ +final class Edge { + private Point start; + private Point end; + + /** + * Creates an edge between the two points. + * + * @param p1 first point + * @param p2 second point + * @throws IllegalArgumentException if the points are not non-null or not a + * valid point for an edge on the grid defined + * by @see ch.zhaw.hexboard.HexBoard + */ + public Edge(Point p1, Point p2) { + if (Edge.isEdge(p1, p2)) { + if (p1.x > p2.x || (p1.x == p2.x && p1.y > p2.y)) { + this.start = new Point(p2); + this.end = new Point(p1); + } else { + this.start = new Point(p1); + this.end = new Point(p2); + } + } else { + throw new IllegalArgumentException( + "Coordinates " + p1 + " and " + p2 + " are not coordinates of an edge."); + } + } + + static boolean isEdge(Point p1, Point p2) { + boolean isEdge = false; + if (p1 != null && p2 != null && HexBoard.isCornerCoordinate(p1) + && HexBoard.isCornerCoordinate(p2)) { + int xdistance = Math.abs(p1.x - p2.x); + int ydistance = Math.abs(p1.y - p2.y); + boolean isVerticalEdge = xdistance == 0 && ydistance == 2; + boolean isDiagonalEdge = xdistance == 1 && ydistance == 1; + isEdge = isVerticalEdge || isDiagonalEdge; + } + return isEdge; + } + + public boolean isEdgePoint(Point p1) { + return start.equals(p1) || end.equals(p1); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((end == null) ? 0 : end.hashCode()); + result = prime * result + ((start == null) ? 0 : start.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + + } + Edge other = (Edge) obj; + if (end == null) { + if (other.end != null) { + return false; + } + } else if (!end.equals(other.end)) { + return false; + } + if (start == null) { + if (other.start != null) { + return false; + } + } else if (!start.equals(other.start)) { + return false; + } + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Edge [start=" + start + ", end=" + end + "]"; + } +} \ No newline at end of file diff --git a/src/ch/zhaw/hexboard/FieldAnnotationPosition.java b/src/ch/zhaw/hexboard/FieldAnnotationPosition.java new file mode 100644 index 0000000..4f369f4 --- /dev/null +++ b/src/ch/zhaw/hexboard/FieldAnnotationPosition.java @@ -0,0 +1,114 @@ +package ch.zhaw.hexboard; + +import java.awt.Point; + +/** + * This class models an annotation for the hex-fields of the hex-grid defined + * by @see ch.zhaw.hexboard.HexBoard + * + * @author tebe + * + */ +final class FieldAnnotationPosition { + private Point field; + private Point corner; + + /** + * Creates a field annotation for the specified field. + * + * @param field the field to be annotated + * @param corner the location of the annotation + * @throws IllegalArgumentException if arguments are null or not valid + * field/corner coordinates (@see + * ch.zhaw.hexboard.HexBoard). + */ + public FieldAnnotationPosition(Point field, Point corner) { + if (HexBoard.isCorner(field, corner)) { + this.field = field; + this.corner = corner; + } else { + throw new IllegalArgumentException("" + field + " is not a field coordinate or " + corner + + " is not a corner of the field."); + } + } + + /** + * Checks whether the provided coordinate matches the position of the annotation + * within the field. + * + * @param p the corner coordinate + * @return true, if they match + */ + public boolean isCorner(Point p) { + return corner.equals(p); + } + + /** + * Checks whether the provided coordinate matches the field coordinate of this + * annotation. + * + * @param p a field coordinate + * @return true, if they match + */ + public boolean isField(Point p) { + return field.equals(p); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((field == null) ? 0 : field.hashCode()); + result = prime * result + ((corner == null) ? 0 : corner.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + FieldAnnotationPosition other = (FieldAnnotationPosition) obj; + if (field == null) { + if (other.field != null) { + return false; + } + } else if (!field.equals(other.field)) { + return false; + } + if (corner == null) { + if (other.corner != null) { + return false; + } + } else if (!corner.equals(other.corner)) { + return false; + } + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "FieldAnnotationPosition [field=" + field + ", corner=" + corner + "]"; + } +} diff --git a/src/ch/zhaw/hexboard/HexBoard.java b/src/ch/zhaw/hexboard/HexBoard.java new file mode 100644 index 0000000..79767f0 --- /dev/null +++ b/src/ch/zhaw/hexboard/HexBoard.java @@ -0,0 +1,541 @@ +package ch.zhaw.hexboard; + +import java.awt.Point; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/*** + *

+ * This class represents a simple generic hexagonal game board. + *

+ *

The game board uses a fixed coordinate system which is structured as follows:

+ * + *
+ *         0    1    2    3    4    5    6    7    8 
+ *         |    |    |    |    |    |    |    |    |   ...
+ * 
+ *  0----  C         C         C         C         C
+ *            \   /     \   /     \   /     \   /     \ 
+ *  1----       C         C         C         C         C
+ *  
+ *  2----  F    |    F    |    F    |    F    |    F    |   ...
+ *  
+ *  3----       C         C         C         C         C
+ *           /     \   /     \   /     \   /     \   / 
+ *  4----  C         C         C         C         C    
+ *              
+ *  5----  |    F    |    F    |    F    |    F    |    F   ...
+ *              
+ *  6----  C         C         C         C         C    
+ *           \     /   \     /   \     /   \     /   \     
+ *  7----       C         C         C         C         C    
+ *   
+ *    ...
+ * 
+ * + *

+ * Fields F and corners C can be retrieved + * using their coordinates ({@link java.awt.Point}) on the board. Edges can be + * retrieved using the coordinates of the two corners they connect. + *

+ * + *

+ * When created, the board is empty (no fields added). To add fields, the + * #{@link #addField(Point, Object)} function can be used. Edges and corners are + * automatically created when adding a field. They cannot be created/removed + * individually. When adding a field, edges and corners that were already + * created, e.g., because adding an adjacent field already created them, are + * left untouched. + *

+ * + *

+ * Fields, edges and corners can store an object of the type of the + * corresponding type parameter each. + *

+ * + *

+ * Furthermore, the hexagonal game board can store six additional objects, so + * called annotations, for each field. These objects are identified by the + * coordinates of the field and the corner. Hence, they can be thought of being + * located between the center and the respective corner. Or in other words, + * their positions correspond to the positions N, NW, SW, NE, NW, SE and NE in + * the below visualization of a field. + *

+ * + *
+ *       SW (C) SE
+ *    /      N      \
+ *  (C) NW       NE (C)
+ *   |       F       |
+ *   |               |
+ *  (C) SW       SE (C)
+ *    \      S      /
+ *       NW (C) NE
+ * 
+ * + * @param Data type for the field data objects + * @param Data type for the corner data objects + * @param Data type for the edge data objects + * @param Data type for the annotation data objects + * + * @author tebe + * + */ +public class HexBoard { + private int maxCoordinateX = 0; + private int maxCoordinateY = 0; + private final Map field; + private final Map corner; + private final Map edge; + private final Map annotation; + + /** + * Constructs an empty hexagonal board. + */ + public HexBoard() { + field = new HashMap<>(); + corner = new HashMap<>(); + edge = new HashMap<>(); + annotation = new HashMap<>(); + } + + /** + * Adds a field to the board and creates the surrounding (empty) corners and + * edges if they do not yet exist Note: Corners and edges of a field might + * already have been created while creating adjacent fields. + * + * @param center Coordinate of the center of a field on the unit grid + * @param element Data element to be stored for this field + * + * @throws IllegalArgumentException if center is not the center of a field, the + * field already exists or data is null + */ + public void addField(Point center, F element) { + if (isFieldCoordinate(center) && !field.containsKey(center)) { + field.put(center, element); + maxCoordinateX = Math.max(center.x + 1, maxCoordinateX); + maxCoordinateY = Math.max(center.y + 2, maxCoordinateY); + // add (empty) edge, if they do not yet exist + for (Edge e : constructEdgesOfField(center)) { + if (!edge.containsKey(e)) { + edge.put(e, null); + } + } + // add (empty) corners, if they do not yet exist + for (Point p : getCornerCoordinatesOfField(center)) { + if (!corner.containsKey(p)) { + corner.put(p, null); + } + } + } else { + throw new IllegalArgumentException( + "Coordinates are not the center of a field, the field already exists or data is null - (" + + center.x + ", " + center.y + ")"); + } + } + + /** + * Add an annotation for the specified field and corner. + * + * @param center the center of the field + * @param corner the corner of the field + * @param data the annotation + * @throws IllegalArgumentException if the field does not exists or when the + * annotation already exists + */ + public void addFieldAnnotation(Point center, Point corner, A data) { + FieldAnnotationPosition annotationPosition = new FieldAnnotationPosition(center, corner); + if (!annotation.containsKey(annotationPosition)) { + annotation.put(annotationPosition, data); + } else { + throw new IllegalArgumentException("Annotation: " + annotation + " already exists for field " + + center + " and position " + corner); + } + } + + /** + * Get an annotation for the specified field and corner. + * + * @param center the center of the field + * @param corner the corner of the field + * @return the annotation + * @throws IllegalArgumentException if coordinates are not a field and + * corresponding corner coordinate + */ + public A getFieldAnnotation(Point center, Point corner) { + return annotation.get(new FieldAnnotationPosition(center, corner)); + } + + /** + * Get field annotation whose position information includes the specified corner. + * + * @param corner the corner + * @return a list with the annotations that are not null + * @throws IllegalArgumentException if corner is not a corner + */ + public List getFieldAnnotationsForCorner(Point corner) { + List list = new LinkedList<>(); + for (Entry entry : annotation.entrySet()) { + if (entry.getKey().isCorner(corner) && entry.getValue() != null) { + list.add(entry.getValue()); + } + } + return list; + } + + /** + * Get all field annotation of the specified field. + * + * @param center the field + * @return a list with the annotations that are not null + * @throws IllegalArgumentException if center is not a field + */ + public List getFieldAnnotationsForField(Point center) { + List list = new LinkedList<>(); + for (Entry entry : annotation.entrySet()) { + if (entry.getKey().isField(center) && entry.getValue() != null) { + list.add(entry.getValue()); + } + } + return list; + } + + /** + * Determines whether the field at the specified position exists. + * + * @param center the field + * @return false, if the field does not exist or the position is not a field + */ + public boolean hasField(Point center) { + if (!HexBoard.isFieldCoordinate(center)) { + return false; + } + return field.containsKey(center); + } + + static boolean isFieldCoordinate(Point position) { + boolean isYFieldCoordinateEven = (position.y - 2) % 6 == 0; + boolean isYFieldCoordinateOdd = (position.y - 5) % 6 == 0; + boolean isXFieldCoordinateEven = position.x % 2 == 0; + boolean isXFieldCoordinateOdd = (position.x - 1) % 2 == 0; + + return (position.y >= 2 && position.x >= 1) + && (isYFieldCoordinateEven && isXFieldCoordinateEven) + || (isYFieldCoordinateOdd && isXFieldCoordinateOdd); + } + + static boolean isCornerCoordinate(Point p) { + // On the horizontal center lines, no edge points exist + boolean isOnFieldCenterLineHorizontal = (p.y - 2) % 3 == 0; + + // On the vertical center lines, edge points exist + boolean isOnFieldCenterLineVerticalOdd = (p.x - 1) % 3 == 0 && p.x % 2 == 0; + boolean isOnFieldCenterLineVerticalEven = (p.x - 1) % 3 == 0 && (p.x - 1) % 2 == 0; + boolean isNotAnEdgePointOnFieldCentralVerticalLine = isOnFieldCenterLineVerticalOdd + && !(p.y % 6 == 0 || (p.y + 2) % 6 == 0) + || isOnFieldCenterLineVerticalEven && !((p.y + 5) % 6 == 0 || (p.y + 3) % 6 == 0); + + return !(isOnFieldCenterLineHorizontal || isNotAnEdgePointOnFieldCentralVerticalLine); + } + + private List constructEdgesOfField(Point position) { + Edge[] e = new Edge[6]; + e[0] = new Edge(new Point(position.x, position.y - 2), + new Point(position.x + 1, position.y - 1)); + e[1] = new Edge(new Point(position.x + 1, position.y - 1), + new Point(position.x + 1, position.y + 1)); + e[2] = new Edge(new Point(position.x + 1, position.y + 1), + new Point(position.x, position.y + 2)); + e[3] = new Edge(new Point(position.x, position.y + 2), + new Point(position.x - 1, position.y + 1)); + e[4] = new Edge(new Point(position.x - 1, position.y + 1), + new Point(position.x - 1, position.y - 1)); + e[5] = new Edge(new Point(position.x - 1, position.y - 1), + new Point(position.x, position.y - 2)); + return Arrays.asList(e); + } + + private static List getCornerCoordinatesOfField(Point position) { + Point[] corner = new Point[6]; + corner[0] = new Point(position.x, position.y - 2); + corner[1] = new Point(position.x + 1, position.y - 1); + corner[2] = new Point(position.x + 1, position.y + 1); + corner[3] = new Point(position.x, position.y + 2); + corner[4] = new Point(position.x - 1, position.y - 1); + corner[5] = new Point(position.x - 1, position.y + 1); + return Collections.unmodifiableList(Arrays.asList(corner)); + } + + protected static List getAdjacentCorners(Point position) { + Point[] corner = new Point[3]; + if (position.y % 3 == 0) { + corner[0] = new Point(position.x, position.y - 2); + corner[1] = new Point(position.x + 1, position.y + 1); + corner[2] = new Point(position.x - 1, position.y + 1); + } else { + corner[0] = new Point(position.x, position.y + 2); + corner[1] = new Point(position.x + 1, position.y - 1); + corner[2] = new Point(position.x - 1, position.y - 1); + } + return Collections.unmodifiableList(Arrays.asList(corner)); + } + + /** + * Returns all non-null corner data elements. + * + * @return the non-null corner data elements + */ + public List getCorners() { + List result = new LinkedList<>(); + for (C c : this.corner.values()) { + if (c != null) { + result.add(c); + } + } + return Collections.unmodifiableList(result); + } + + protected Set getCornerCoordinates() { + return Collections.unmodifiableSet(this.corner.keySet()); + } + + private static List getAdjacentFields(Point corner) { + Point[] field = new Point[3]; + if (corner.y % 3 == 0) { + field[0] = new Point(corner.x, corner.y + 2); + field[1] = new Point(corner.x + 1, corner.y - 1); + field[2] = new Point(corner.x - 1, corner.y - 1); + } else { + field[0] = new Point(corner.x, corner.y - 2); + field[1] = new Point(corner.x + 1, corner.y + 1); + field[2] = new Point(corner.x - 1, corner.y + 1); + } + return Collections.unmodifiableList(Arrays.asList(field)); + } + + /** + * Returns the data for the field denoted by the point. + * + * @param center the location of the field + * @return the stored data (or null) + * @throws IllegalArgumentException if the requested field does not exist + */ + public F getField(Point center) { + if (field.containsKey(center)) { + return field.get(center); + } else { + throw new IllegalArgumentException("No field exists at these coordinates: " + center); + } + } + + /** + * Returns the fields with non-null data elements. + * + * @return the list with the (non-null) field data + */ + public List getFields() { + List result = new LinkedList<>(); + for (Entry e : field.entrySet()) { + if (e.getValue() != null) { + result.add(e.getKey()); + } + } + return Collections.unmodifiableList(result); + } + + /** + * Returns the field data of the fields that touch this corner. + *

+ * If the specified corner is not a corner or none of the fields that touch this + * corner have a non-null data element, an empty list is returned. + *

+ * @param corner the location of the corner + * @return the list with the (non-null) field data + */ + public List getFields(Point corner) { + List result = new LinkedList<>(); + if (isCornerCoordinate(corner)) { + for (Point f : getAdjacentFields(corner)) { + if (field.get(f) != null) { + result.add(field.get(f)); + } + } + } + return Collections.unmodifiableList(result); + } + + /** + * Returns the data for the edge denoted by the two points. + * + * @param p1 first point + * @param p2 second point + * @return the stored data (or null) + */ + public E getEdge(Point p1, Point p2) { + Edge e = new Edge(p1, p2); + if (edge.containsKey(e)) { + return edge.get(e); + } else { + return null; + } + } + + /** + * Stores the data for the edge denoted by the two points. + * + * @param p1 first point + * @param p2 second point + * @param data the data to be stored + * @throws IllegalArgumentException if the two points do not identify an + * EXISTING edge of the field + */ + public void setEdge(Point p1, Point p2, E data) { + Edge e = new Edge(p1, p2); + if (edge.containsKey(e)) { + edge.put(e, data); + } else { + throw new IllegalArgumentException("Edge does not exist => no data can be stored: " + e); + } + } + + /** + * Returns the data for the corner denoted by the point. + * + * @param location the location of the corner + * @return the data stored for this node (or null) + * @throws IllegalArgumentException if the requested corner does not exist + */ + public C getCorner(Point location) { + if (corner.containsKey(location)) { + return corner.get(location); + } else { + throw new IllegalArgumentException("No corner exists at the coordinates: " + location); + } + } + + /** + * Stores the data for the edge denoted by the two points. + * + * @param location the location of the corner + * @param data the data to be stored + * @return the old data entry (or null) + * @throws IllegalArgumentException if there is no corner at this location + */ + public C setCorner(Point location, C data) { + C old = corner.get(location); + if (corner.containsKey(location)) { + corner.put(location, data); + return old; + } else { + throw new IllegalArgumentException( + "Corner does not exist => no data can be stored: " + location); + } + } + + /** + * Returns the (non-null) corner data elements of the corners that are direct + * neighbors of the specified corner. + *

+ * Each corner has three direct neighbors, except corners that are located at + * the border of the game board. + *

+ * @param center the location of the corner for which to return the direct + * neighbors + * @return list with non-null corner data elements + */ + public List getNeighboursOfCorner(Point center) { + List result = new LinkedList<>(); + for (Point c : HexBoard.getAdjacentCorners(center)) { + C temp = corner.get(c); + if (temp != null) { + result.add(temp); + } + } + return result; + } + + /** + * Returns the (non-null) edge data elements of the edges that directly connect + * to that corner. + *

+ * Each corner has three edges connecting to it, except edges that are located + * at the border of the game board. + *

+ * @param corner corner for which to get the edges + * @return list with non-null edge data elements of edges connecting to the + * specified edge + */ + public List getAdjacentEdges(Point corner) { + List result = new LinkedList<>(); + for (Entry e : this.edge.entrySet()) { + if (e.getKey().isEdgePoint(corner) + && e.getValue() != null) { + result.add(e.getValue()); + } + } + return result; + } + + /** + * Returns the (non-null) data elements of the corners of the specified field. + * + * @param center the location of the field + * @return list with non-null corner data elements + */ + public List getCornersOfField(Point center) { + List result = new LinkedList<>(); + for (Point c : getCornerCoordinatesOfField(center)) { + C temp = getCorner(c); + if (temp != null) { + result.add(temp); + } + } + return result; + } + + int getMaxCoordinateX() { + return maxCoordinateX; + } + + int getMaxCoordinateY() { + return maxCoordinateY; + } + + /** + * Checks whether there is a corner at that specified location. + * @param location the location to check + * @return true, if there is a corner at this location + */ + public boolean hasCorner(Point location) { + if (!HexBoard.isCornerCoordinate(location)) { + return false; + } + return corner.containsKey(location); + } + + /** + * Checks whether there is an edge between the two points. + * @param p1 first point + * @param p2 second point + * @return true, if there is an edge between the two points + */ + public boolean hasEdge(Point p1, Point p2) { + if (Edge.isEdge(p1, p2)) { + return edge.containsKey(new Edge(p1, p2)); + } else { + return false; + } + } + + static boolean isCorner(Point field, Point corner) { + return HexBoard.isFieldCoordinate(field) + && HexBoard.getCornerCoordinatesOfField(field).contains(corner); + } + +} diff --git a/src/ch/zhaw/hexboard/HexBoardTextView.java b/src/ch/zhaw/hexboard/HexBoardTextView.java new file mode 100644 index 0000000..97c51af --- /dev/null +++ b/src/ch/zhaw/hexboard/HexBoardTextView.java @@ -0,0 +1,366 @@ +package ch.zhaw.hexboard; + +import java.awt.Point; +import java.util.HashMap; +import java.util.Map; + +/** + * This class can be used to get a textual representation of a hex-grid modeled + * by {@link ch.zhaw.hexboard.HexBoard}. + *

+ * It creates a textual representation of the {@link ch.zhaw.hexboard.HexBoard} + * that includes all defined fields, edges, corners and annotations. + *

+ * The generation of the textual representation is basically working on a line + * by line basis. Thereby, the two text lines needed for the diagonal edges are + * treated like "one line" in that they are created in one step together. + *

+ * The textual representation does not contain the hex-grid as such but only the + * fields that actually exist on the hex-board. Note that if a field exists, + * also its corners and edges exist and are therefore shown in the textual + * representation. + *

+ *

+ * This class defines how edges, corners and fields look like (their "label"). + * This is done as follows:

+ *
    + *
  • If there is no data object associated with an edge, corner or field, + * their default representation is used. Note that the default representation of + * an edge depends on its direction (see below).
  • + *
  • If there is a data object associated with an edge, corner or field, the + * {@link ch.zhaw.hexboard.Label} is determined by calling: + *
      + *
    • EL = {@link #getEdgeLabel(Object)}
    • + *
    • CL = {@link #getCornerLabel(Object)}
    • + *
    • UL = {@link #getFieldLabelUpper(Object)}
    • + *
    + *
  • + *
+ *

In addition to edges, corners and field labels, the hex-board's field + * annotations are included too. If an annotation exists for one of the corners + * (N, NW, SW, S, SE, NE), which means that an associated data object exists, it + * is turned into a {@link ch.zhaw.hexboard.Label} with + * {@link #getAnnotationLabel(Object)}.

+ *
+ *

Two examples of how that looks like are shown below. The first example shows + * a case with all edges, corners and the field with no data associated with + * them. The second one has all edges corner and the upper field label defined + * by calling the corresponding method for creating the Label for the associated + * data object.

+ * + *
+ *        DEFAULT                LABELS FROM DATA 
+ *                       
+ *          (  )                       (CL)
+ *        //    \\                   EL    EL
+ *    //            \\           EL     N      EL
+ * (  )              (  )     (CL) NW        NE (CL)   
+ *  ||                ||       EL       UL       EL    
+ *  ||                ||       EL                EL
+ * (  )              (  )     (CL) SW        SE (CL)
+ *    \\            //           EL     S      EL
+ *        \\    //                   EL    EL
+ *          (  )                       (CL)
+ * 
+ *
+ *

To override the default behavior, which creates a Label using the two first + * characters of the string returned by the toString() method of the + * edge/corner/field data object, you might override the respective methods. + *

+ *
+ *

+ * Finally, a field can be labeled with a lower label (LL) by providing a map of + * field coordinates and associated labels. An example of a representation with + * all field annotations, corner labels and field labels defined but default + * edges is the following:

+ * + *
+ *          (CL) 
+ *        // N  \\
+ *    //            \\
+ * (CL) NW        NE (CL)
+ *  ||       UL       ||
+ *  ||       LL       ||
+ * (CL) SW        SE (CL)
+ *    \\     S      //
+ *        \\    //
+ *          (CL)
+ * 
+ * + * @param See {@link ch.zhaw.hexboard.HexBoard} + * @param See {@link ch.zhaw.hexboard.HexBoard} + * @param See {@link ch.zhaw.hexboard.HexBoard} + * @param
See {@link ch.zhaw.hexboard.HexBoard} + * + * + * @author tebe + */ +public class HexBoardTextView { + + private static final String ONE_SPACE = " "; + private static final String TWO_SPACES = " "; + private static final String FOUR_SPACES = " "; + private static final String FIVE_SPACES = " "; + private static final String SIX_SPACES = " "; + private static final String SEVEN_SPACES = " "; + private static final String NINE_SPACES = " "; + private final HexBoard board; + private final Label emptyLabel = new Label(' ', ' '); + private final Label defaultDiagonalEdgeDownLabel = new Label('\\', '\\'); + private final Label defaultDiagonalEdgeUpLabel = new Label('/', '/'); + private final Label defaultVerticalEdgeLabel = new Label('|', '|'); + private Map fixedLowerFieldLabels; + + /** + * Creates a view for the specified board. + * + * @param board the board + */ + public HexBoardTextView(HexBoard board) { + this.fixedLowerFieldLabels = new HashMap<>(); + this.board = board; + } + + /** + * Sets the lower field label for the specified field. + * + * @param field the field + * @param label the label + * @throws IllegalArgumentException if arguments are null or if the field does + * not exist + */ + public void setLowerFieldLabel(Point field, Label label) { + if (field == null || label == null || !board.hasField(field)) { + throw new IllegalArgumentException("Argument(s) must not be null and field must exist."); + } + fixedLowerFieldLabels.put(field, label); + } + + /** + * Returns a label to be used as label for the edge. This method is called to + * determine the label for this edge. + * + * @param e edge data object + * @return the label + */ + protected Label getEdgeLabel(E e) { + return deriveLabelFromToStringRepresentation(e); + } + + /** + * Returns a label to be used as label for the corner. This method is called to + * determine the label for this corner. + * + * @param c corner data object + * @return the label + */ + protected Label getCornerLabel(C c) { + return deriveLabelFromToStringRepresentation(c); + } + + /** + * Returns a label to be used as upper label for the field. This method is + * called to determine the upper label for this field. + * + * @param f field data object + * @return the label + */ + protected Label getFieldLabelUpper(F f) { + return deriveLabelFromToStringRepresentation(f); + } + + /** + * Returns a label to be used as lower label for the field at this position. + * This method is called to determine the lower label for this field. + * + * @param p location of the field + * @return the label + */ + private Label getFieldLabelLower(Point p) { + Label l = this.fixedLowerFieldLabels.get(p); + l = l == null ? emptyLabel : l; + return l; + } + + private Label deriveLabelFromToStringRepresentation(Object o) { + Label label = emptyLabel; + if (o.toString().length() > 0) { + String s = o.toString(); + if (s.length() > 1) { + return new Label(s.charAt(0), s.charAt(1)); + } else { + return new Label(s.charAt(0), ' '); + } + } + return label; + } + + /** + *

+ * This method returns a single-line string with all corners and field + * annotations for a given y-coordinate. It produces the string by iterating + * over corner positions and appending per corner: + *

+ *

+ * "(CL) NE NW " for y%3==1 "(CL) SE SW " for y%3==0 + *

+ *

+ * Corners/labels that do not exist are replaced by spaces. + *

+ */ + private String printCornerLine(int y) { + StringBuilder cornerLine = new StringBuilder(""); + int offset = 0; + if (y % 2 != 0) { + cornerLine.append(NINE_SPACES); + offset = 1; + } + for (int x = offset; x <= board.getMaxCoordinateX(); x = x + 2) { + Point p = new Point(x, y); + Label cornerLabel; + + // handle corner labels for corners other than north and south corners + Point center; + Label first = null; + Label second = null; + switch (y % 3) { + case 0: + center = new Point(x + 1, y - 1); + first = this.getAnnotationLabel( + board.getFieldAnnotation(center, new Point(center.x - 1, center.y + 1))); + second = this.getAnnotationLabel( + board.getFieldAnnotation(center, new Point(center.x + 1, center.y + 1))); + break; + case 1: + center = new Point(x + 1, y + 1); + first = this.getAnnotationLabel( + board.getFieldAnnotation(center, new Point(center.x - 1, center.y - 1))); + second = this.getAnnotationLabel( + board.getFieldAnnotation(center, new Point(center.x + 1, center.y - 1))); + break; + default: + throw new IllegalArgumentException("Not a corner line"); + } + + if (board.hasCorner(p)) { + cornerLabel = board.getCorner(p) != null ? getCornerLabel(board.getCorner(p)) : emptyLabel; + cornerLine.append("(").append(cornerLabel.getFirst()).append(cornerLabel.getSecond()).append(")"); + } else { + cornerLine.append(FOUR_SPACES); + } + cornerLine.append(ONE_SPACE).append(first.getFirst()).append(first.getSecond()); + cornerLine.append(FIVE_SPACES).append(second.getFirst()).append(second.getSecond()).append(TWO_SPACES); + } + return cornerLine.toString(); + } + + private Label getAnnotationLabel(A annotation) { + if (annotation == null) { + return emptyLabel; + } else { + return deriveLabelFromToStringRepresentation(annotation); + } + } + + private String printMiddlePartOfField(int y) { + boolean isOffsetRow = (y - 2) % 6 == 0; + StringBuilder lower = new StringBuilder(isOffsetRow ? NINE_SPACES : ""); + StringBuilder upper = new StringBuilder(isOffsetRow ? NINE_SPACES : ""); + int xstart = isOffsetRow ? 2 : 1; + + for (int x = xstart; x <= board.getMaxCoordinateX() + 1; x = x + 2) { + Point edgeStart = new Point(x - 1, y - 1); + Point edgeEnd = new Point(x - 1, y + 1); + Label l = this.emptyLabel; + if (board.hasEdge(edgeStart, edgeEnd)) { + E edge = board.getEdge(edgeStart, edgeEnd); + if (edge != null) { + l = this.getEdgeLabel(edge); + } else { + l = this.defaultVerticalEdgeLabel; + } + } + Point center = new Point(x, y); + boolean hasFieldWithData = board.hasField(center) && board.getField(center) != null; + Label lowerFieldLabel = hasFieldWithData ? getFieldLabelLower(center) : emptyLabel; + Label upperFieldLabel = hasFieldWithData ? getFieldLabelUpper(board.getField(center)) + : emptyLabel; + lower.append(ONE_SPACE).append(l.getFirst()).append(l.getSecond()).append(SEVEN_SPACES); + lower.append(lowerFieldLabel.getFirst()).append(lowerFieldLabel.getSecond()).append(SIX_SPACES); + + upper.append(ONE_SPACE).append(l.getFirst()).append(l.getSecond()).append(SEVEN_SPACES); + upper.append(upperFieldLabel.getFirst()).append(upperFieldLabel.getSecond()).append(SIX_SPACES); + } + return upper + System.lineSeparator() + lower; + } + + private String printDiagonalEdges(int y) { + StringBuilder builder = new StringBuilder(); + Point edgeStart; + Point edgeEnd; + Label annotation = null; + Label l; + boolean isDown = y % 6 == 0; + + builder.append(" "); + for (int x = 0; x <= board.getMaxCoordinateX(); x = x + 1) { + if (isDown) { + edgeStart = new Point(x, y); + edgeEnd = new Point(x + 1, y + 1); + annotation = getAnnotationLabel(board.getFieldAnnotation(new Point(x + 1, y - 1), new Point(x + 1, y + 1))); + } else { + edgeStart = new Point(x, y + 1); + edgeEnd = new Point(x + 1, y); + annotation = getAnnotationLabel(board.getFieldAnnotation(new Point(x + 1, y + 2), new Point(x + 1, y))); + } + l = determineEdgeLabel(isDown, edgeStart, edgeEnd); + + if (!isDown) { + builder.append(TWO_SPACES + l.getFirst() + l.getSecond() + TWO_SPACES + annotation.getFirst() + annotation.getSecond()); + } else { + builder.append(TWO_SPACES + l.getFirst() + l.getSecond() + TWO_SPACES + annotation.getFirst() + annotation.getSecond()); + } + isDown = !isDown; + } + return builder.toString(); + } + + private Label determineEdgeLabel(boolean isDown, Point edgeStart, Point edgeEnd) { + Label l; + if (board.hasEdge(edgeStart, edgeEnd)) { + // does it have data associated with it? + if (board.getEdge(edgeStart, edgeEnd) != null) { + l = this.getEdgeLabel(board.getEdge(edgeStart, edgeEnd)); + } else { + // default visualization + l = isDown ? this.defaultDiagonalEdgeDownLabel : this.defaultDiagonalEdgeUpLabel; + } + } else { + l = this.emptyLabel; + } + return l; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int y = 0; y <= board.getMaxCoordinateY(); y = y + 3) { + sb.append(printCornerLine(y)); + sb.append(System.lineSeparator()); + sb.append(printDiagonalEdges(y)); + sb.append(System.lineSeparator()); + sb.append(printCornerLine(y + 1)); + sb.append(System.lineSeparator()); + sb.append(printMiddlePartOfField(y + 2)); + sb.append(System.lineSeparator()); + + } + return sb.toString(); + } + +} diff --git a/src/ch/zhaw/hexboard/Label.java b/src/ch/zhaw/hexboard/Label.java new file mode 100644 index 0000000..6e766ef --- /dev/null +++ b/src/ch/zhaw/hexboard/Label.java @@ -0,0 +1,50 @@ +package ch.zhaw.hexboard; + +/** + * This class defines a label composed of two characters. + * + * @author tebe + * + */ +public final class Label { + public static final char DEFAULT_CHARACTER = ' '; + private final char first; + private final char second; + + /** + * Creates a label from two characters. + * + * @param firstChar first character + * @param secondChar second character + */ + public Label(char firstChar, char secondChar) { + first = firstChar; + second = secondChar; + } + + /** + * Creates a label using the default character {@link #DEFAULT_CHARACTER}. + */ + public Label() { + first = ' '; + second = ' '; + } + + public char getFirst() { + return first; + } + + public char getSecond() { + return second; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + first + second; + } +} diff --git a/test/ch/zhaw/catan/SiedlerGameTest.java b/test/ch/zhaw/catan/SiedlerGameTest.java new file mode 100644 index 0000000..2b3d4d6 --- /dev/null +++ b/test/ch/zhaw/catan/SiedlerGameTest.java @@ -0,0 +1,21 @@ +package ch.zhaw.catan; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + + +/*** + * TODO Write your own tests in this class. + * + * Note: Have a look at {@link ch.zhaw.catan.games.ThreePlayerStandard}. It can be used + * to get several different game states. + * + */ +public class SiedlerGameTest { + + @Test + public void dummyTestMethod() { + assertTrue(false); + } + +} \ No newline at end of file diff --git a/test/ch/zhaw/catan/SiedlerGameTestBasic.java b/test/ch/zhaw/catan/SiedlerGameTestBasic.java new file mode 100644 index 0000000..4c6e6fe --- /dev/null +++ b/test/ch/zhaw/catan/SiedlerGameTestBasic.java @@ -0,0 +1,258 @@ +package ch.zhaw.catan; + +import ch.zhaw.catan.games.ThreePlayerStandard; + + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.awt.*; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This class contains some basic tests for the {@link SiedlerGame} class + *

+ *

DO NOT MODIFY THIS CLASS

+ * + * @author tebe + */ +public class SiedlerGameTestBasic { + private final static int DEFAULT_WINPOINTS = 5; + private final static int DEFAULT_NUMBER_OF_PLAYERS = 3; + + /** + * Tests whether the functionality for switching to the next/previous player + * works as expected for different numbers of players. + * + * @param numberOfPlayers the number of players + */ + @ParameterizedTest + @ValueSource(ints = {2, 3, 4}) + public void requirementPlayerSwitching(int numberOfPlayers) { + SiedlerGame model = new SiedlerGame(DEFAULT_WINPOINTS, numberOfPlayers); + assertTrue(numberOfPlayers == model.getPlayerFactions().size(), + "Wrong number of players returned by getPlayers()"); + //Switching forward + for (int i = 0; i < numberOfPlayers; i++) { + assertEquals(Config.Faction.values()[i], model.getCurrentPlayerFaction(), + "Player order does not match order of Faction.values()"); + model.switchToNextPlayer(); + } + assertEquals(Config.Faction.values()[0], model.getCurrentPlayerFaction(), + "Player wrap-around from last player to first player did not work."); + //Switching backward + for (int i = numberOfPlayers - 1; i >= 0; i--) { + model.switchToPreviousPlayer(); + assertEquals(Config.Faction.values()[i], model.getCurrentPlayerFaction(), + "Switching players in reverse order does not work as expected."); + } + } + + /** + * Tests whether the game board meets the required layout/land placement. + */ + @Test + public void requirementLandPlacementTest() { + SiedlerGame model = new SiedlerGame(DEFAULT_WINPOINTS, DEFAULT_NUMBER_OF_PLAYERS); + assertTrue(Config.getStandardLandPlacement().size() == model.getBoard().getFields().size(), + "Check if explicit init must be done (violates spec): " + + "modify initializeSiedlerGame accordingly."); + for (Map.Entry e : Config.getStandardLandPlacement().entrySet()) { + assertEquals(e.getValue(), model.getBoard().getField(e.getKey()), + "Land placement does not match default placement."); + } + } + + /** + * Tests whether the {@link ThreePlayerStandard#getAfterSetupPhase(int)}} game board is not empty (returns + * an object) at positions where settlements and roads have been placed. + */ + @Test + public void requirementSettlementAndRoadPositionsOccupiedThreePlayerStandard() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(DEFAULT_WINPOINTS); + assertEquals(DEFAULT_NUMBER_OF_PLAYERS, model.getPlayerFactions().size()); + for (Config.Faction f : model.getPlayerFactions()) { + assertTrue(model.getBoard().getCorner(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).first) != null); + assertTrue(model.getBoard().getCorner(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).second) != null); + assertTrue(model.getBoard().getEdge(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).first, ThreePlayerStandard.INITIAL_ROAD_ENDPOINTS.get(f).first) != null); + assertTrue(model.getBoard().getEdge(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).second, ThreePlayerStandard.INITIAL_ROAD_ENDPOINTS.get(f).second) != null); + } + } + + /** + * Checks that the resource card payout for different dice values matches + * the expected payout for the game state {@link ThreePlayerStandard#getAfterSetupPhase(int)}}. + * + * Note, that for the test to work, the {@link Map} returned by {@link SiedlerGame#throwDice(int)} + * must contain a {@link List} with resource cards (empty {@link List}, if the player gets none) + * for each of the players. + * + * @param diceValue the dice value + */ + @ParameterizedTest + @ValueSource(ints = {2, 3, 4, 5, 6, 8, 9, 10, 11, 12}) + public void requirementDiceThrowResourcePayoutThreePlayerStandardTest(int diceValue) { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(DEFAULT_WINPOINTS); + Map> expectd = ThreePlayerStandard.INITIAL_DICE_THROW_PAYOUT.get(diceValue); + Map> actual = model.throwDice(diceValue); + assertEquals(ThreePlayerStandard.INITIAL_DICE_THROW_PAYOUT.get(diceValue), model.throwDice(diceValue)); + } + + /** + * Tests whether the resource card stock of the players matches the expected stock + * for the game state {@link ThreePlayerStandard#getAfterSetupPhase(int)}}. + */ + @Test + public void requirementPlayerResourceCardStockAfterSetupPhase() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(DEFAULT_WINPOINTS); + assertPlayerResourceCardStockEquals(model, ThreePlayerStandard.INITIAL_PLAYER_CARD_STOCK); + } + + /** + * Tests whether the resource card stock of the players matches the expected stock + * for the game state {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}}. + */ + @Test + public void requirementPlayerResourceCardStockAfterSetupPhaseAlmostEmptyBank() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(DEFAULT_WINPOINTS); + assertPlayerResourceCardStockEquals(model, ThreePlayerStandard.BANK_ALMOST_EMPTY_RESOURCE_CARD_STOCK); + } + + /** + * Tests whether the resource card stock of the players matches the expected stock + * for the game state {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}}. + */ + @Test + public void requirementPlayerResourceCardStockPlayerOneReadyToBuildFifthSettlement() { + SiedlerGame model = ThreePlayerStandard.getPlayerOneReadyToBuildFifthSettlement(DEFAULT_WINPOINTS); + assertPlayerResourceCardStockEquals(model, ThreePlayerStandard.PLAYER_ONE_READY_TO_BUILD_FIFTH_SETTLEMENT_RESOURCE_CARD_STOCK); + } + + /** + * Throws each dice value except 7 once and tests whether the resource + * card stock of the players matches the expected stock. + */ + @Test + public void requirementDiceThrowPlayerResourceCardStockUpdateTest() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(DEFAULT_WINPOINTS); + for(int i : List.of(2, 3, 4, 5, 6, 8, 9, 10, 11, 12)) { + model.throwDice(i); + } + Map> expected = Map.of( + Config.Faction.values()[0], Map.of(Config.Resource.GRAIN, 1, Config.Resource.WOOL, 2, + Config.Resource.BRICK, 2, Config.Resource.ORE, 1, Config.Resource.LUMBER, 1), + Config.Faction.values()[1], + Map.of(Config.Resource.GRAIN, 1, Config.Resource.WOOL, 5, Config.Resource.BRICK, 0, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 0), + Config.Faction.values()[2], + Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 0, Config.Resource.BRICK, 2, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 1)); + + assertPlayerResourceCardStockEquals(model, expected); + } + + private void assertPlayerResourceCardStockEquals(SiedlerGame model, Map> expected) { + for (int i = 0; i < expected.keySet().size(); i++) { + Config.Faction f = model.getCurrentPlayerFaction(); + for (Config.Resource r : Config.Resource.values()) { + assertEquals(expected.get(f).get(r), model.getCurrentPlayerResourceStock(r), + "Resource card stock of player " + i + " [faction " + f + "] for resource type " + r + " does not match."); + } + model.switchToNextPlayer(); + } + } + + /** + * Tests whether player one can build two roads starting in game state + * {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}. + */ + @Test + public void requirementBuildRoad() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(DEFAULT_WINPOINTS); + assertTrue(model.buildRoad(new Point(6, 6), new Point(6, 4))); + assertTrue(model.buildRoad(new Point(6, 4), new Point(7, 3))); + } + + /** + * Tests whether player one can build a road and a settlement starting in game state + * {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}. + */ + @Test + public void requirementBuildSettlement() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(DEFAULT_WINPOINTS); + assertTrue(model.buildRoad(new Point(9, 15), new Point(9, 13))); + assertTrue(model.buildSettlement(new Point(9, 13))); + } + + /** + * Tests whether payout with multiple settlements of the same player at one field works + * {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}. + */ + @Test + public void requirementTwoSettlementsSamePlayerSameFieldResourceCardPayout() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(DEFAULT_WINPOINTS); + for(int diceValue : List.of(2, 6, 6, 11)){ + model.throwDice(diceValue); + } + assertTrue(model.buildRoad(new Point(6, 6), new Point(7, 7))); + assertTrue(model.buildSettlement(new Point(7, 7))); + assertEquals(List.of(Config.Resource.ORE, Config.Resource.ORE), model.throwDice(4).get(model.getCurrentPlayerFaction())); + } + + /** + * Tests whether player one can build a city starting in game state + * {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}. + */ + @Test + public void requirementBuildCity() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(DEFAULT_WINPOINTS); + assertTrue(model.buildCity(new Point(10, 16))); + } + + /** + * Tests whether player two can trade in resources with the bank and has the + * correct number of resource cards afterwards. The test starts from game state + * {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}. + */ + @Test + public void requirementCanTradeFourToOneWithBank() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(DEFAULT_WINPOINTS); + model.switchToNextPlayer(); + + Map expectedResourceCards = ThreePlayerStandard.BANK_ALMOST_EMPTY_RESOURCE_CARD_STOCK.get(model.getCurrentPlayerFaction()); + assertEquals(expectedResourceCards.get(Config.Resource.WOOL), model.getCurrentPlayerResourceStock(Config.Resource.WOOL)); + assertEquals(expectedResourceCards.get(Config.Resource.LUMBER), model.getCurrentPlayerResourceStock(Config.Resource.LUMBER)); + + model.tradeWithBankFourToOne(Config.Resource.WOOL, Config.Resource.LUMBER); + + int cardsOffered = 4; + int cardsReceived = 1; + assertEquals(expectedResourceCards.get(Config.Resource.WOOL)-cardsOffered, model.getCurrentPlayerResourceStock(Config.Resource.WOOL)); + assertEquals(expectedResourceCards.get(Config.Resource.LUMBER)+cardsReceived, model.getCurrentPlayerResourceStock(Config.Resource.LUMBER)); + } + + /*** + * This test is not actually a test and should be removed. However, + * we leave it in for you to have a quick and easy way to look at the + * game board produced by {@link ThreePlayerStandard#getAfterSetupPhase(int)}, + * augmented by annotations, which you won't need since we do not ask for + * more advanced trading functionality using harbours. + */ + @Disabled + @Test + public void print(){ + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(DEFAULT_WINPOINTS); + model.getBoard().addFieldAnnotation(new Point(6, 8),new Point(6, 6), "N "); + model.getBoard().addFieldAnnotation(new Point(6, 8),new Point(5, 7), "NE"); + model.getBoard().addFieldAnnotation(new Point(6, 8),new Point(5, 9), "SE"); + model.getBoard().addFieldAnnotation(new Point(6, 8),new Point(6, 10), "S "); + model.getBoard().addFieldAnnotation(new Point(6, 8),new Point(7, 7), "NW"); + model.getBoard().addFieldAnnotation(new Point(6, 8),new Point(7, 9), "SW"); + System.out.println(new SiedlerBoardTextView(model.getBoard()).toString()); + } +} diff --git a/test/ch/zhaw/catan/Tuple.java b/test/ch/zhaw/catan/Tuple.java new file mode 100644 index 0000000..9957113 --- /dev/null +++ b/test/ch/zhaw/catan/Tuple.java @@ -0,0 +1,42 @@ +package ch.zhaw.catan; + +public class Tuple { + public final X first; + public final Y second; + + public Tuple(X x, Y y) { + this.first = x; + this.second = y; + } + + @Override + public String toString() { + return "(" + first + "," + second + ")"; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Tuple)) { + return false; + } + + @SuppressWarnings("unchecked") + Tuple otherCasted = (Tuple) other; + + // null is not a valid value for first and second tuple element + return otherCasted.first.equals(this.first) && otherCasted.second.equals(this.second); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((first == null) ? 0 : first.hashCode()); + result = prime * result + ((second == null) ? 0 : second.hashCode()); + return result; + } +} diff --git a/test/ch/zhaw/catan/games/ThreePlayerStandard.java b/test/ch/zhaw/catan/games/ThreePlayerStandard.java new file mode 100644 index 0000000..a0d525e --- /dev/null +++ b/test/ch/zhaw/catan/games/ThreePlayerStandard.java @@ -0,0 +1,480 @@ +package ch.zhaw.catan.games; + +import ch.zhaw.catan.Config; +import ch.zhaw.catan.Config.Resource; +import ch.zhaw.catan.Tuple; +import ch.zhaw.catan.SiedlerGame; + +import java.awt.*; +import java.util.List; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * This class can be used to prepare some predefined siedler game situations and, for some + * of the situations, it provides information about the expected game state, + * for example the number of resource cards in each player's stock or the expected resource + * card payout when the dices are thrown (for each dice value). + *
+ * The basic game situations upon which all other situations that can be retrieved are based is + * the following: + *
+ *                                 (  )            (  )            (  )            (  )
+ *                              //      \\      //      \\      //      \\      //      \\
+ *                         (  )            (  )            (  )            (  )            (  )
+ *                          ||      ~~      ||      ~~      ||      ~~      ||      ~~      ||
+ *                          ||              ||              ||              ||              ||
+ *                         (  )            (  )            (  )            (  )            (  )
+ *                      //      \\      //      \\      //      \\      //      \\      //      \\
+ *                 (  )            (  )            (  )            (bb)            (  )            (  )
+ *                  ||      ~~      ||      LU      ||      WL      bb      WL      ||      ~~      ||
+ *                  ||              ||      06      ||      03      bb      08      ||              ||
+ *                 (  )            (  )            (  )            (  )            (  )            (  )
+ *              //      \\      //      \\      rr      \\      //      \\      //      \\      //      \\
+ *         (  )            (  )            (rr)            (  )            (  )            (  )            (  )
+ *          ||      ~~      ||      GR      ||      OR      ||      GR      ||      LU      ||      ~~      ||
+ *          ||              ||      02      ||      04      ||      05      ||      10      ||              ||
+ *         (  )            (  )            (  )            (  )            (  )            (  )            (  )
+ *      //      \\      //      \\      //      \\      //      \\      //      \\      //      \\      //      \\
+ * (  )            (  )            (  )            (  )            (  )            (  )            (  )            (  )
+ *  ||      ~~      gg      LU      ||      BR      ||      --      ||      OR      ||      GR      ||      ~~      ||
+ *  ||              gg      05      ||      09      ||      07      ||      06      ||      09      ||              ||
+ * (  )            (gg)            (  )            (  )            (  )            (  )            (  )            (  )
+ *      \\      //      \\      //      \\      //      \\      //      \\      //      \\      bb      \\      //
+ *         (  )            (  )            (  )            (  )            (  )            (bb)            (  )
+ *          ||      ~~      ||      GR      ||      OR      ||      LU      ||      WL      ||      ~~      ||
+ *          ||              ||      10      ||      11      ||      03      ||      12      ||              ||
+ *         (  )            (  )            (  )            (  )            (  )            (  )            (  )
+ *              \\      //      \\      //      \\      //      \\      //      rr      //      \\      //
+ *                 (  )            (  )            (  )            (  )            (rr)            (  )
+ *                  ||      ~~      ||      WL      ||      BR      ||      BR      ||      ~~      ||
+ *                  ||              ||      08      ||      04      ||      11      ||              ||
+ *                 (  )            (  )            (  )            (  )            (  )            (  )
+ *                      \\      //      \\      //      \\      gg      \\      //      \\      //
+ *                         (  )            (  )            (gg)            (  )            (  )
+ *                          ||      ~~      ||      ~~      ||      ~~      ||      ~~      ||
+ *                          ||              ||              ||              ||              ||
+ *                         (  )            (  )            (  )            (  )            (  )
+ *                              \\      //      \\      //      \\      //      \\      //
+ *                                 (  )            (  )            (  )            (  )
+ * 
+ * Resource cards after the setup phase: + *
    + *
  • Player 1: WOOL BRICK
  • + *
  • Player 2: WOOL WOOL
  • + *
  • Player 3: BRICK
  • + *
+ *

The main ideas for this setup were the following:

+ *
    + *
  • Player one has access to all resource types from the start so that any resource card can be acquired by + * throwing the corresponding dice value.
  • + *
  • The settlements are positioned in a way that for each dice value, there is only one resource card paid + * to one player, except for the dice values 4 and 12.
  • + *
  • There is a settlement next to water and the owner has access to resource types required to build roads
  • + *
  • The initial resource card stock of each player does not allow to build anything without getting + * additional resources first
  • + *
+ * + * @author tebe + */ +public class ThreePlayerStandard { + public final static int NUMBER_OF_PLAYERS = 3; + + public static final Map> INITIAL_SETTLEMENT_POSITIONS = + Map.of( Config.Faction.values()[0], new Tuple<>(new Point(5, 7), new Point(10, 16)), + Config.Faction.values()[1], new Tuple<>(new Point(11, 13), new Point(8, 4)), + Config.Faction.values()[2], new Tuple<>(new Point(2, 12), new Point(7, 19))); + + public static final Map> INITIAL_ROAD_ENDPOINTS = Map.of(Config.Faction.values()[0], + new Tuple<>(new Point(6, 6), new Point(9, 15)), Config.Faction.values()[1], + new Tuple<>(new Point(12, 12), new Point(8, 6)), Config.Faction.values()[2], + new Tuple<>(new Point(2, 10), new Point(8, 18))); + + public static final Map> INITIAL_PLAYER_CARD_STOCK = Map.of( + Config.Faction.values()[0], Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 1, + Config.Resource.BRICK, 1, Config.Resource.ORE, 0, Config.Resource.LUMBER, 0), + Config.Faction.values()[1], + Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 2, Config.Resource.BRICK, 0, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 0), + Config.Faction.values()[2], + Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 0, Config.Resource.BRICK, 1, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 0)); + + public static final Map> BANK_ALMOST_EMPTY_RESOURCE_CARD_STOCK = Map.of( + Config.Faction.values()[0], Map.of(Config.Resource.GRAIN, 8, Config.Resource.WOOL, 9, + Config.Resource.BRICK, 9, Config.Resource.ORE, 7, Config.Resource.LUMBER, 9), + Config.Faction.values()[1], + Map.of(Config.Resource.GRAIN, 8, Config.Resource.WOOL, 10, Config.Resource.BRICK, 0, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 0), + Config.Faction.values()[2], + Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 0, Config.Resource.BRICK, 8, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 9)); + + public static final Map> PLAYER_ONE_READY_TO_BUILD_FIFTH_SETTLEMENT_RESOURCE_CARD_STOCK = Map.of( + Config.Faction.values()[0], Map.of(Config.Resource.GRAIN, 2, Config.Resource.WOOL, 2, + Config.Resource.BRICK, 3, Config.Resource.ORE, 0, Config.Resource.LUMBER, 3), + Config.Faction.values()[1], + Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 5, Config.Resource.BRICK, 0, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 0), + Config.Faction.values()[2], + Map.of(Config.Resource.GRAIN, 0, Config.Resource.WOOL, 0, Config.Resource.BRICK, 1, + Config.Resource.ORE, 0, Config.Resource.LUMBER, 0)); + + public static final Map>> INITIAL_DICE_THROW_PAYOUT = Map.of( + 2, Map.of( + Config.Faction.values()[0], List.of(Resource.GRAIN), + Config.Faction.values()[1], List.of(), + Config.Faction.values()[2], List.of()), + 3, Map.of( + Config.Faction.values()[0], List.of(), + Config.Faction.values()[1], List.of(Resource.WOOL), + Config.Faction.values()[2], List.of()), + 4, Map.of( + Config.Faction.values()[0], List.of(Resource.ORE), + Config.Faction.values()[1], List.of(), + Config.Faction.values()[2], List.of(Resource.BRICK)), + 5, Map.of( + Config.Faction.values()[0], List.of(), + Config.Faction.values()[1], List.of(), + Config.Faction.values()[2], List.of(Resource.LUMBER)), + 6, Map.of( + Config.Faction.values()[0], List.of(Resource.LUMBER), + Config.Faction.values()[1], List.of(), + Config.Faction.values()[2], List.of()), + 8, Map.of( + Config.Faction.values()[0], List.of(), + Config.Faction.values()[1], List.of(Resource.WOOL), + Config.Faction.values()[2], List.of()), + 9, Map.of( + Config.Faction.values()[0], List.of(), + Config.Faction.values()[1], List.of(Resource.GRAIN), + Config.Faction.values()[2], List.of()), + 10, Map.of( + Config.Faction.values()[0], List.of(), + Config.Faction.values()[1], List.of(), + Config.Faction.values()[2], List.of()), + 11, Map.of( + Config.Faction.values()[0], List.of(Resource.BRICK), + Config.Faction.values()[1], List.of(), + Config.Faction.values()[2], List.of()), + 12, Map.of( + Config.Faction.values()[0], List.of(Resource.WOOL), + Config.Faction.values()[1], List.of(Resource.WOOL), + Config.Faction.values()[2], List.of())); + + public static final Map RESOURCE_CARDS_IN_BANK_AFTER_STARTUP_PHASE = Map.of(Resource.LUMBER, 19, + Resource.BRICK, 17, Resource.WOOL, 16, Resource.GRAIN, 19, Resource.ORE, 19); + + public static final Point PLAYER_ONE_READY_TO_BUILD_FIFTH_SETTLEMENT_FIFTH_SETTLEMENT_POSITION = new Point(9, 13); + public static final List playerOneReadyToBuildFifthSettlementAllSettlementPositions = + List.of(INITIAL_SETTLEMENT_POSITIONS.get(Config.Faction.values()[0]).first, + INITIAL_SETTLEMENT_POSITIONS.get(Config.Faction.values()[0]).second, + new Point(7, 7),new Point(6, 4), PLAYER_ONE_READY_TO_BUILD_FIFTH_SETTLEMENT_FIFTH_SETTLEMENT_POSITION); + + /** + * Returns a siedler game after the setup phase in the setup + * and with the initial resource card setup as described + * in {@link ThreePlayerStandard}. + * + * @param winpoints the number of points required to win the game + * @return the siedler game + */ + public static SiedlerGame getAfterSetupPhase(int winpoints) { + SiedlerGame model = new SiedlerGame(winpoints, NUMBER_OF_PLAYERS); + for (int i = 0; i < model.getPlayerFactions().size(); i++) { + Config.Faction f = model.getCurrentPlayerFaction(); + assertTrue(model.placeInitialSettlement(INITIAL_SETTLEMENT_POSITIONS.get(f).first, false)); + assertTrue(model.placeInitialRoad(INITIAL_SETTLEMENT_POSITIONS.get(f).first, INITIAL_ROAD_ENDPOINTS.get(f).first)); + model.switchToNextPlayer(); + } + for (int i = 0; i < model.getPlayerFactions().size(); i++) { + model.switchToPreviousPlayer(); + Config.Faction f = model.getCurrentPlayerFaction(); + assertTrue(model.placeInitialSettlement(INITIAL_SETTLEMENT_POSITIONS.get(f).second, true)); + assertTrue(model.placeInitialRoad(INITIAL_SETTLEMENT_POSITIONS.get(f).second, INITIAL_ROAD_ENDPOINTS.get(f).second)); + } + return model; + } + + /** + * Returns a siedler game after the setup phase in the setup + * described in {@link ThreePlayerStandard} and with the bank almost empty. + * + * The following resource cards should be in the stock of the bank: + *
    + *
  • LUMBER: 1
  • + *
  • BRICK: 2
  • + *
  • GRAIN: 3
  • + *
  • ORE: 13
  • + *
  • WOOL: 0
  • + *
+ * + * The stocks of the players should contain: + *
+ * Player 1: + *
    + *
  • LUMBER: 9
  • + *
  • BRICK: 9
  • + *
  • GRAIN: 8
  • + *
  • ORE: 7
  • + *
  • WOOL: 9
  • + *
+ * Player 2: + *
    + *
  • LUMBER: 0
  • + *
  • BRICK: 0
  • + *
  • GRAIN: 8
  • + *
  • ORE: 0
  • + *
  • WOOL: 10
  • + *
+ * Player 3: + *
    + *
  • LUMBER: 9
  • + *
  • BRICK: 8
  • + *
  • GRAIN: 0
  • + *
  • ORE: 0
  • + *
  • WOOL: 0
  • + *
+ * + * @param winpoints the number of points required to win the game + * @return the siedler game + */ + public static SiedlerGame getAfterSetupPhaseAlmostEmptyBank(int winpoints) { + SiedlerGame model = getAfterSetupPhase(winpoints); + throwDiceMultipleTimes(model, 6, 9); + throwDiceMultipleTimes(model, 11, 8); + throwDiceMultipleTimes(model, 2, 8); + throwDiceMultipleTimes(model, 4, 7); + throwDiceMultipleTimes(model, 12, 8); + throwDiceMultipleTimes(model, 5, 9); + throwDiceMultipleTimes(model, 9, 8); + return model; + } + + + /** + * Returns a {@link SiedlerGame} with several roads added but none longer than + * 4 elements. Hence, no player meets the longest road criteria yet. Furthermore, + * players one and three have enough resource cards to build additional roads and settlements. + * + *

+ *

The game board should look as follows: + *

+     *                                 (  )            (  )            (  )            (  )
+     *                              //      \\      //      \\      //      \\      //      \\
+     *                         (  )            (  )            (  )            (  )            (  )
+     *                          ||      ~~      ||      ~~      ||      ~~      ||      ~~      ||
+     *                          ||              ||              ||              ||              ||
+     *                         (  )            (  )            (  )            (  )            (  )
+     *                      //      \\      //      \\      //      \\      //      \\      //      \\
+     *                 (  )            (  )            (  )            (bb)            (  )            (  )
+     *                  ||      ~~      ||      LU      ||      WL      bb      WL      ||      ~~      ||
+     *                  ||              ||      06      ||      03      bb      08      ||              ||
+     *                 (  )            (  )            (  )            (  )            (  )            (  )
+     *              //      \\      //      \\      rr      \\      //      \\      //      \\      //      \\
+     *         (  )            (  )            (rr)            (  )            (  )            (  )            (  )
+     *          ||      ~~      gg      GR      rr      OR      ||      GR      ||      LU      ||      ~~      ||
+     *          ||              gg      02      rr      04      ||      05      ||      10      ||              ||
+     *         (  )            (  )            (  )            (  )            (  )            (  )            (  )
+     *      //      \\      gg      rr      rr      \\      //      \\      //      \\      //      \\      //      \\
+     * (  )            (  )            (  )            (  )            (  )            (  )            (  )            (  )
+     *  ||      ~~      gg      LU      ||      BR      ||      --      ||      OR      ||      GR      ||      ~~      ||
+     *  ||              gg      05      ||      09      ||      07      ||      06      ||      09      ||              ||
+     * (  )            (gg)            (  )            (  )            (  )            (  )            (  )            (  )
+     *      \\      //      gg      //      \\      //      \\      //      \\      rr      \\      bb      \\      //
+     *         (  )            (  )            (  )            (  )            (  )            (bb)            (  )
+     *          ||      ~~      ||      GR      ||      OR      ||      LU      rr      WL      ||      ~~      ||
+     *          ||              ||      10      ||      11      ||      03      rr      12      ||              ||
+     *         (  )            (  )            (  )            (  )            (  )            (  )            (  )
+     *              \\      //      \\      //      \\      //      \\      //      rr      rr      \\      //
+     *                 (  )            (  )            (  )            (  )            (rr)            (  )
+     *                  ||      ~~      ||      WL      gg      BR      gg      BR      rr      ~~      ||
+     *                  ||              ||      08      gg      04      gg      11      rr              ||
+     *                 (  )            (  )            (  )            (  )            (  )            (  )
+     *                      \\      //      \\      //      gg      gg      \\      //      \\      //
+     *                         (  )            (  )            (gg)            (  )            (  )
+     *                          ||      ~~      ||      ~~      ||      ~~      ||      ~~      ||
+     *                          ||              ||              ||              ||              ||
+     *                         (  )            (  )            (  )            (  )            (  )
+     *                              \\      //      \\      //      \\      //      \\      //
+     *                                 (  )            (  )            (  )            (  )
+     * 
+ *

+ * And the player resource card stocks: + *
+ * Player 1: + *

    + *
  • LUMBER: 6
  • + *
  • BRICK: 6
  • + *
  • GRAIN: 1
  • + *
  • ORE: 11
  • + *
  • WOOL: 1
  • + *
+ * Player 2: + *
    + *
  • LUMBER: 0
  • + *
  • BRICK: 0
  • + *
  • GRAIN: 0
  • + *
  • ORE: 0
  • + *
  • WOOL: 2
  • + *
+ * Player 3: + *
    + *
  • LUMBER: 6
  • + *
  • BRICK: 6
  • + *
  • GRAIN: 1
  • + *
  • ORE: 0
  • + *
  • WOOL: 1
  • + *
+ * + * @param winpoints the number of points required to win the game + * @return the siedler game + */ + public static SiedlerGame getAfterSetupPhaseSomeRoads(int winpoints) { + SiedlerGame model = getAfterSetupPhase(winpoints); + throwDiceMultipleTimes(model, 6, 7); + throwDiceMultipleTimes(model, 11, 6); + throwDiceMultipleTimes(model, 4, 5); + throwDiceMultipleTimes(model, 5, 6); + throwDiceMultipleTimes(model, 2, 1); + + model.switchToNextPlayer(); + model.switchToNextPlayer(); + model.buildRoad(new Point(2,12), new Point(3,13)); + buildRoad(model, List.of(new Point(2,10), new Point(3,9), new Point(3,7))); + model.buildRoad(new Point(8,18), new Point(8,16)); + buildRoad(model, List.of(new Point(7,19), new Point(6,18), new Point(6,16))); + model.switchToNextPlayer(); + model.buildRoad(new Point(10,16), new Point(11,15)); + model.buildRoad(new Point(10,16), new Point(10,18)); + buildRoad(model, List.of(new Point(9,15), new Point(9,13), new Point(10,12))); + buildRoad(model, List.of(new Point(5,7), new Point(5,9), new Point(4,10), new Point(3, 9))); + + throwDiceMultipleTimes(model, 6, 6); + throwDiceMultipleTimes(model, 11, 6); + throwDiceMultipleTimes(model, 4, 6); + throwDiceMultipleTimes(model, 5, 6); + + model.switchToNextPlayer(); + model.switchToNextPlayer(); + throwDiceMultipleTimes(model, 5, 4); + model.tradeWithBankFourToOne(Resource.LUMBER, Resource.GRAIN); + throwDiceMultipleTimes(model, 5, 4); + model.tradeWithBankFourToOne(Resource.LUMBER, Resource.WOOL); + model.switchToNextPlayer(); + return model; + } + + + + private static SiedlerGame throwDiceMultipleTimes(SiedlerGame model, int diceValue, int numberOfTimes) { + for(int i=0; i

+ *

The game board should look as follows: + *

+     *                                 (  )            (  )            (  )            (  )
+     *                              //      \\      //      \\      //      \\      //      \\
+     *                         (  )            (  )            (  )            (  )            (  )
+     *                          ||      ~~      ||      ~~      ||      ~~      ||      ~~      ||
+     *                          ||              ||              ||              ||              ||
+     *                         (  )            (  )            (  )            (  )            (  )
+     *                      //      \\      //      \\      //      \\      //      \\      //      \\
+     *                 (  )            (  )            (rr)            (bb)            (  )            (  )
+     *                  ||      ~~      ||      LU      rr      WL      bb      WL      ||      ~~      ||
+     *                  ||              ||      06      rr      03      bb      08      ||              ||
+     *                 (  )            (  )            (  )            (  )            (  )            (  )
+     *              //      \\      //      \\      rr      rr      //      \\      //      \\      //      \\
+     *         (  )            (  )            (rr)            (rr)            (  )            (  )            (  )
+     *          ||      ~~      ||      GR      ||      OR      ||      GR      ||      LU      ||      ~~      ||
+     *          ||              ||      02      ||      04      ||      05      ||      10      ||              ||
+     *         (  )            (  )            (  )            (  )            (  )            (  )            (  )
+     *      //      \\      //      \\      //      \\      //      \\      //      \\      //      \\      //      \\
+     * (  )            (  )            (  )            (  )            (  )            (  )            (  )            (  )
+     *  ||      ~~      gg      LU      ||      BR      ||      --      ||      OR      ||      GR      ||      ~~      ||
+     *  ||              gg      05      ||      09      ||      07      ||      06      ||      09      ||              ||
+     * (  )            (gg)            (  )            (  )            (  )            (  )            (  )            (  )
+     *      \\      //      \\      //      \\      //      \\      //      \\      //      \\      bb      \\      //
+     *         (  )            (  )            (  )            (  )            (  )            (bb)            (  )
+     *          ||      ~~      ||      GR      ||      OR      ||      LU      rr      WL      ||      ~~      ||
+     *          ||              ||      10      ||      11      ||      03      rr      12      ||              ||
+     *         (  )            (  )            (  )            (  )            (  )            (  )            (  )
+     *              \\      //      \\      //      \\      //      \\      //      rr      //      \\      //
+     *                 (  )            (  )            (  )            (  )            (rr)            (  )
+     *                  ||      ~~      ||      WL      ||      BR      ||      BR      ||      ~~      ||
+     *                  ||              ||      08      ||      04      ||      11      ||              ||
+     *                 (  )            (  )            (  )            (  )            (  )            (  )
+     *                      \\      //      \\      //      \\      gg      \\      //      \\      //
+     *                         (  )            (  )            (gg)            (  )            (  )
+     *                          ||      ~~      ||      ~~      ||      ~~      ||      ~~      ||
+     *                          ||              ||              ||              ||              ||
+     *                         (  )            (  )            (  )            (  )            (  )
+     *                              \\      //      \\      //      \\      //      \\      //
+     *                                 (  )            (  )            (  )            (  )
+     *
+     * 
+ *
+ *

And the player resource card stocks:

+ *
+ * Player 1: + *
    + *
  • LUMBER: 3
  • + *
  • BRICK: 3
  • + *
  • GRAIN: 2
  • + *
  • ORE: 0
  • + *
  • WOOL: 2
  • + *
+ * Player 2: + *
    + *
  • LUMBER: 0
  • + *
  • BRICK: 0
  • + *
  • GRAIN: 0
  • + *
  • ORE: 0
  • + *
  • WOOL: 5
  • + *
+ * Player 3: + *
    + *
  • LUMBER: 0
  • + *
  • BRICK: 1
  • + *
  • GRAIN: 0
  • + *
  • ORE: 0
  • + *
  • WOOL: 0
  • + *
+ * + * @param winpoints the number of points required to win the game + * @return the siedler game + */ + public static SiedlerGame getPlayerOneReadyToBuildFifthSettlement(int winpoints) { + SiedlerGame model = getAfterSetupPhase(winpoints); + //generate resources to build four roads and four settlements. + throwDiceMultipleTimes(model, 6, 8); + throwDiceMultipleTimes(model, 11, 7); + throwDiceMultipleTimes(model, 2, 4); + throwDiceMultipleTimes(model, 12, 3); + model.buildRoad(new Point(6,6), new Point(7,7)); + model.buildRoad(new Point(6,6), new Point(6,4)); + model.buildRoad(new Point(9,15), new Point(9,13)); + model.buildSettlement(playerOneReadyToBuildFifthSettlementAllSettlementPositions.get(2)); + model.buildSettlement(playerOneReadyToBuildFifthSettlementAllSettlementPositions.get(3)); + return model; + } + private static void buildSettlement(SiedlerGame model, Point position, List roads) { + buildRoad(model, roads); + assertTrue(model.buildSettlement(position)); + } + + private static void buildRoad(SiedlerGame model, List roads) { + for (int i = 0; i < roads.size()-1; i++) { + assertTrue(model.buildRoad(roads.get(i), roads.get(i + 1))); + } + } +} diff --git a/test/ch/zhaw/hexboard/EdgeTest.java b/test/ch/zhaw/hexboard/EdgeTest.java new file mode 100644 index 0000000..e07ff73 --- /dev/null +++ b/test/ch/zhaw/hexboard/EdgeTest.java @@ -0,0 +1,106 @@ +package ch.zhaw.hexboard; + +import java.awt.Point; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/*** + *

+ * This class performs tests for the class {@link Edge}. + *

+ * + * @author tebe + * + **/ +public class EdgeTest { + + private Point[] hexagon22 = { new Point(2, 0), new Point(3, 1), new Point(3, 3), new Point(2, 4), + new Point(1, 3), new Point(1, 1) }; + private Point[] hexagon75 = { new Point(7, 3), new Point(8, 4), new Point(8, 6), new Point(7, 7), + new Point(6, 6), new Point(6, 4) }; + + @Test + public void createValidEdge() { + new Edge(new Point(0, 0), new Point(1, 1)); + } + + @Test + public void edgeEqualityStartEndPointReversed() { + for (int i = 0; i < hexagon22.length - 1; i++) { + assertEquals(new Edge(hexagon22[i], hexagon22[i + 1]), + new Edge(hexagon22[i + 1], hexagon22[i])); + } + for (int i = 0; i < hexagon75.length - 1; i++) { + assertEquals(new Edge(hexagon75[i], hexagon75[i + 1]), + new Edge(hexagon75[i + 1], hexagon75[i])); + } + } + + @Test + public void notEquals() { + assertNotEquals(new Edge(hexagon22[0], hexagon22[1]), + new Edge(hexagon22[1], hexagon22[2])); + } + + @Test + public void createWithBothArgumentsNull() { + assertThrows(IllegalArgumentException.class, () -> new Edge(null, null)); + } + + @Test + public void createWithFirstArgumentNull() { + assertThrows(IllegalArgumentException.class, () -> new Edge(null, new Point(1, 0))); + } + + @Test + public void createWithSecondArgumentNull() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(1, 0), null)); + } + + @Test + public void createWithStartAndEndpointIdentical() { + assertThrows(IllegalArgumentException.class, () -> new Edge(hexagon22[0], hexagon22[0])); + } + + @Test + public void notAnEdgeHorizontalOddTop() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(5, 7), new Point(7, 7))); + } + + @Test + public void notAnEdgeHorizontalOddMiddle() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(3, 2), new Point(5, 2))); + } + + @Test + public void notAnEdgeHorizontalOddBottom() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(5, 3), new Point(7, 3))); + } + + @Test + public void notAnEdgeHorizontalEvenTop() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(4, 4), new Point(6, 4))); + } + + @Test + public void notAnEdgeHorizontalEvenMiddle() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(2, 5), new Point(4, 5))); + } + + @Test + public void notAnEdgeHorizontalEvenBottom() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(4, 6), new Point(6, 6))); + } + + @Test + public void notAnEdgeVerticalEven() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(7, 7), new Point(7, 3))); + } + + @Test + public void notAnEdgeVerticalOdd() { + assertThrows(IllegalArgumentException.class, () -> new Edge(new Point(6, 4), new Point(6, 0))); + } + +} diff --git a/test/ch/zhaw/hexboard/HexBoardTest.java b/test/ch/zhaw/hexboard/HexBoardTest.java new file mode 100644 index 0000000..2e3e078 --- /dev/null +++ b/test/ch/zhaw/hexboard/HexBoardTest.java @@ -0,0 +1,121 @@ +package ch.zhaw.hexboard; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.Point; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/*** + *

+ * Tests for the class {@link HexBoard}. + *

+ * @author tebe + */ +public class HexBoardTest { + private HexBoard board; + private Point[] corner; + + /** + * Setup for a test - Instantiates a board and adds one field at (7,5). + * + *
+   *         0    1    2    3    4    5    6    7    8 
+   *         |    |    |    |    |    |    |    |    |   ...
+   * 
+   *  0----  
+   *          
+   *  1----  
+   *  
+   *  2----  
+   *  
+   *  3----                                     C         
+   *                                         /     \    
+   *  4----                                C         C   
+   *              
+   *  5----                                |    F    |        ...
+   *              
+   *  6----                                C         C    
+   *                                         \     /        
+   *  7----                                     C
+   * 
+ */ + @BeforeEach + public void setUp() { + board = new HexBoard<>(); + board.addField(new Point(7, 5), "00"); + Point[] singleField = { new Point(7, 3), new Point(8, 4), new Point(8, 6), new Point(7, 7), + new Point(6, 6), new Point(6, 4) }; + this.corner = singleField; + } + + // Edge retrieval + @Test + public void edgeTest() { + for (int i = 0; i < corner.length - 1; i++) { + assertNull(board.getEdge(corner[i], corner[i + 1])); + board.setEdge(corner[i], corner[i + 1], Integer.toString(i)); + assertEquals(board.getEdge(corner[i], corner[i + 1]), Integer.toString(i)); + } + } + + @Test + public void noEdgeCoordinatesTest() { + assertThrows(IllegalArgumentException.class, + () -> board.getEdge(new Point(2, 2), new Point(0, 2))); + } + + @Test + public void edgeDoesNotExistTest() { + assertThrows(IllegalArgumentException.class, + () -> board.getEdge(new Point(0, 2), new Point(3, 1))); + } + + // Corner retrieval + @Test + public void cornerTest() { + for (Point p : corner) { + assertNull(board.getCorner(p)); + board.setCorner(p, p.toString()); + assertEquals(board.getCorner(p), p.toString()); + } + } + + @Test + public void noCornerCoordinateTest() { + assertThrows(IllegalArgumentException.class, () -> board.getCorner(new Point(2, 2))); + } + + @Test + public void cornerDoesNotExistTest() { + assertThrows(IllegalArgumentException.class, () -> board.getCorner(new Point(2, 2))); + } + + // Field addition/retrieval + @Test + public void fieldAreadyExistsErrorTest() { + board.addField(new Point(2, 2), "22"); + assertThrows(IllegalArgumentException.class, () -> board.addField(new Point(2, 2), "22")); + } + + @Test + public void fieldRetrievalTest() { + Point field = new Point(2, 2); + board.addField(field, new String("22")); + assertTrue(board.hasField(field)); + assertEquals(board.getField(field), "22"); + } + + @Test + public void fieldRetrievalWrongCoordinatesOutsideTest() { + assertThrows(IllegalArgumentException.class, () -> board.getField(new Point(10, 10))); + } + + @Test + public void fieldRetrievalWrongCoordinatesInsideTest() { + assertThrows(IllegalArgumentException.class, () -> board.getField(new Point(2, 2))); + } +}