diff --git a/.github/execution.svg b/.github/execution.svg new file mode 100644 index 0000000..ad6aa6b --- /dev/null +++ b/.github/execution.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b62755b --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# IntelliJ Files +out/ +.idea/ +.idea_modules/ +*.iml +*.ipr +*.iws + +# macOS +.DS_Store diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6aab65b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +Copyright © 2023 Boostvolt (Jan). All rights reserved. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..17398f1 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Catan + +Catan is a board game that simulates the building and development of a settlement on a fictional island. + +## Getting started + +### Clone the repository + +1. Open the repository in the IntelliJ IDE and navigate to the `App.java` file. +2. Click ![Execution Symbol](.github/execution.svg) `Run` next to the `Current File` option on the toolbar. +3. The program is compiled and started accordingly. After that the [commands](#available-commands) listed below can be + used in the console. + +## Available commands + +| Command | Description | +| :----------------------- | :-------------------------------------------------------------------------------- | +| DISPLAY_BOARD | Print the current state of the game board to the console. | +| DISPLAY_PLAYER_RESOURCES | Shows the current resource amounts for a given player. | +| DISPLAY_BANK_RESOURCES | Displays the current number of each resource type in the game bank. | +| DISPLAY_STRUCTURE_COSTS | Shows the cost of building each type of structure in the game. | +| DISPLAY_SCORES | Displays the current scores of all players in the game. | +| TRADE | Allows players to exchange resources with the bank. | +| BUILD_ROAD | Allows a player to build a road at a specified location on the game board. | +| BUILD_SETTLEMENT | Allows a player to build a settlement on an empty intersection on the game board. | +| BUILD_CITY | Allows a player to build a city on an existing settlement on the game board. | +| NEXT_TURN | Advances the game to the next player's turn. | +| QUIT | Allows the user to exit the game. | + +## Features + +- Basic functionality +- Hexboard with coordinate system +- Winning animation +- Cities extension +- Robber extension + +## Classdiagram + +### Without dependencies + +![classdiagram](doc/class_diagram.png) + +### With dependencies + +![classdiagram](doc/class_diagram_with_dependencies.png) diff --git a/doc/class_diagram.png b/doc/class_diagram.png new file mode 100644 index 0000000..a6761a2 Binary files /dev/null and b/doc/class_diagram.png differ diff --git a/doc/class_diagram_with_dependencies.png b/doc/class_diagram_with_dependencies.png new file mode 100644 index 0000000..1300a44 Binary files /dev/null and b/doc/class_diagram_with_dependencies.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6a090e7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + groupId + catan + 1.0-SNAPSHOT + + + + org.junit.jupiter + junit-jupiter-engine + 5.9.1 + test + + + org.junit.jupiter + junit-jupiter-params + 5.9.1 + test + + + org.beryx + text-io + 3.4.1 + + + + + 17 + 17 + UTF-8 + + + \ No newline at end of file diff --git a/src/main/java/ch/zhaw/catan/board/Field.java b/src/main/java/ch/zhaw/catan/board/Field.java new file mode 100644 index 0000000..051ebeb --- /dev/null +++ b/src/main/java/ch/zhaw/catan/board/Field.java @@ -0,0 +1,80 @@ +package ch.zhaw.catan.board; + +import ch.zhaw.catan.game.Config.Land; + +import java.awt.Point; + +import static ch.zhaw.catan.game.Config.INITIAL_THIEF_POSITION; +import static java.util.Objects.requireNonNull; + +/** + * A class representing a Field on the game board of Siedler. + */ +public class Field { + + static final String THIEF_IDENTIFIER = "XX"; + + private final Land land; + private final Point position; + private boolean occupiedByThief; + + /** + * Constructs a new Field with the given {@link Land} type and position. + * + * @param land the type of {@link Land} for this Field + * @param position the position of this Field on the game board + */ + public Field(final Land land, final Point position) { + this.land = requireNonNull(land, "land must not be null"); + this.position = requireNonNull(position, "position must not be null"); + occupiedByThief = position.equals(INITIAL_THIEF_POSITION); + } + + /** + * Returns the type of {@link Land} for this Field. + * + * @return the {@link Land} type for this Field + */ + public Land getLand() { + return land; + } + + /** + * Returns the position of this Field on the game board. + * + * @return the position of this Field on the game board + */ + public Point getPosition() { + return position; + } + + /** + * Returns whether this Field is occupied by a thief. + * + * @return true if this Field is occupied by a thief, false otherwise + */ + public boolean isOccupiedByThief() { + return occupiedByThief; + } + + /** + * Sets whether this Field is occupied by a thief. + * + * @param occupiedByThief true if this Field is occupied by a thief, false otherwise + */ + public void setOccupiedByThief(final boolean occupiedByThief) { + this.occupiedByThief = occupiedByThief; + } + + /** + * Returns a {@link String} representation of this Field. If this Field is occupied by a thief, the THIEF_IDENTIFIER is returned. + * Otherwise, the toString() method of the {@link Land} type is returned. + * + * @return a {@link String} representation of this Field + */ + @Override + public String toString() { + return isOccupiedByThief() ? THIEF_IDENTIFIER : getLand().toString(); + } + +} diff --git a/src/main/java/ch/zhaw/catan/board/SiedlerBoard.java b/src/main/java/ch/zhaw/catan/board/SiedlerBoard.java new file mode 100644 index 0000000..a00d7a5 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/board/SiedlerBoard.java @@ -0,0 +1,283 @@ +package ch.zhaw.catan.board; + +import ch.zhaw.catan.game.Config; +import ch.zhaw.catan.game.Config.Faction; +import ch.zhaw.catan.game.Config.Land; +import ch.zhaw.catan.game.Player; +import ch.zhaw.catan.structure.City; +import ch.zhaw.catan.structure.Road; +import ch.zhaw.catan.structure.Settlement; +import ch.zhaw.catan.structure.Structure; +import ch.zhaw.hexboard.HexBoard; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static ch.zhaw.catan.game.Config.INITIAL_THIEF_POSITION; +import static ch.zhaw.catan.game.Config.Land.WATER; +import static ch.zhaw.catan.game.Config.Structure.SETTLEMENT; +import static ch.zhaw.catan.game.Config.getStandardDiceNumberPlacement; +import static ch.zhaw.catan.game.Config.getStandardLandPlacement; + +/** + * Class representing the Board of the game. Class holds information of the board and provides methods to fetch + * data about the various fields, corners and edges and to assert if certain structures can be built. + */ +public class SiedlerBoard extends HexBoard { + + public static final int MIN_COORDINATE = 0; + public static final int MAX_X_COORDINATE = 14; + public static final int MAX_Y_COORDINATE = 22; + + private final SiedlerBoardTextView view; + + private Field currentThiefField; + + /** + * Constructs a new SiedlerBoard with the standard {@link Land} placement. + * The initial thief field is also set to the initial thief position. + */ + public SiedlerBoard() { + for (Map.Entry entry : getStandardLandPlacement().entrySet()) { + super.addField(entry.getKey(), new Field(entry.getValue(), entry.getKey())); + } + setCurrentThiefField(getField(INITIAL_THIEF_POSITION)); + view = new SiedlerBoardTextView(this); + } + + /** + * Returns the {@link Field}s associated with the specified dice value. + * + * @param dice the dice value + * @return the fields associated with the dice value + */ + public List getFieldsForDiceValue(final int dice) { + final List rolledFields = new ArrayList<>(); + + for (Map.Entry diceValuePerPoint : getStandardDiceNumberPlacement().entrySet()) { + if (diceValuePerPoint.getValue() == dice) { + rolledFields.add(getField(diceValuePerPoint.getKey())); + } + } + + return rolledFields; + } + + + /** + * 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(final Point corner) { + final List landList = new ArrayList<>(); + + if (hasCorner(corner)) { + final List fields = getFields(corner); + + for (Field field : fields) { + landList.add(field.getLand()); + } + + landList.removeIf(land -> land.getResource() == null); + } + + return landList; + } + + /** + * Returns the current thief {@link Field}. + * + * @return the current thief {@link Field} + */ + public Field getCurrentThiefField() { + return currentThiefField; + } + + /** + * Sets the current thief {@link Field}. + * + * @param currentThiefField the current thief {@link Field} + */ + public void setCurrentThiefField(final Field currentThiefField) { + this.currentThiefField = currentThiefField; + } + + /** + * Returns the text view of the game board. + * + * @return the text view of the game board + */ + public SiedlerBoardTextView getView() { + return view; + } + + /** + * Returns whether the thief can be placed on the specified position on the game board. + * + * @param position the position to check + * @return true if the thief can be placed on the position, false otherwise + */ + public boolean canPlaceThiefOnPosition(final Point position) { + return hasField(position) + && getField(position) != null + && getField(position).getLand() != WATER; + } + + /** + * Moves the thief to the specified {@link Field} on the game board. + * + * @param fieldPosition the position of the {@link Field} to move the thief to + */ + public void switchThiefPosition(final Point fieldPosition) { + getCurrentThiefField().setOccupiedByThief(false); + final Field field = getField(fieldPosition); + field.setOccupiedByThief(true); + setCurrentThiefField(field); + } + + + /** + * Returns a set of {@link Faction}s of the {@link Player}s who have {@link Structure}s on the corners of the current thief {@link Field} that + * belong to a different {@link Faction} than the current {@link Player}'s {@link Faction}. + * + * @param currentPlayerFaction the {@link Faction} of the current {@link Player} + * @return a {@link Set} of {@link Faction}s of the {@link Player}s on the corners of the current thief {@link Field} + */ + public Set getOtherFieldCornerFactions(final Faction currentPlayerFaction) { + final Set factions = new HashSet<>(); + + for (Structure structure : getCornersOfField(getCurrentThiefField().getPosition())) { + if (structure.getFaction() != currentPlayerFaction) { + factions.add(structure.getFaction()); + } + } + + return factions; + } + + /** + * Checks if the current {@link Point} on {@link SiedlerBoard} + * can hold a {@link Settlement} and has no surrounding occupied corners and is not occupied itself. + * + * @param position position on {@link SiedlerBoard} as {@link Point} + * @param initiationPhase boolean for set up phase, removes adjacent {@link Road}s requirement + * @return true if {@link Settlement} can be placed + */ + public boolean canPlaceSettlementOnPosition(final Point position, final boolean initiationPhase, + final Faction currentPlayerFaction) { + if (!hasCorner(position)) { + return false; + } + + final Structure corner = getCorner(position); + final boolean isCornerFree = !getLandsForCorner(position).isEmpty() + && corner == null; + final boolean hasNoNeighbour = hasNoNeighbour(position); + return initiationPhase + ? isCornerFree && hasNoNeighbour + : isCornerFree && hasNoNeighbour && isOwnRoadAdjacent(position, currentPlayerFaction); + } + + /** + * Checks if a {@link Settlement} of the current {@link Player} + * can be upgraded into a {@link City} on position + * + * @param position position on {@link SiedlerBoard} as {@link Point} + * @return true if {@link City} can be placed + */ + public boolean canPlaceCityOnPosition(final Point position, final Faction currentPlayerFaction) { + if (!hasCorner(position)) { + return false; + } + + return isSpecificOwnStructureAdjacent(position, SETTLEMENT, currentPlayerFaction); + } + + /** + * Checks if the Start- and End-{@link Point} on {@link SiedlerBoard} + * can hold a {@link Road} and has a {@link Structure} of the current {@link Player}s {@link Faction} + * + * @param startPosition position on {@link SiedlerBoard} as {@link Point} + * @param endPosition position on {@link SiedlerBoard} as {@link Point} + * @return true if {@link Road} can be placed, false otherwise + */ + public boolean canPlaceRoadOnPosition(final Point startPosition, final Point endPosition, + final Faction currentPlayerFaction) { + if (!hasEdge(startPosition, endPosition) + || getEdge(startPosition, endPosition) != null) { + return false; + } + + if (getLandsForCorner(startPosition).isEmpty() && getLandsForCorner(endPosition).isEmpty()) { + return false; + } + + return isOwnStructureAdjacent(startPosition, currentPlayerFaction) + || isOwnStructureAdjacent(endPosition, currentPlayerFaction) + || isOwnRoadAdjacent(startPosition, currentPlayerFaction) + || isOwnRoadAdjacent(endPosition, currentPlayerFaction); + } + + /** + * Returns whether the given position has no neighbours with a {@link Faction}. + * + * @param position the position to check + * @return true if the position has no neighbours with a {@link Faction}, false otherwise + */ + private boolean hasNoNeighbour(final Point position) { + return getNeighboursOfCorner(position).isEmpty(); + } + + /** + * Determines if any of the {@link Road}s adjacent to the given position are owned by the current {@link Player}. + * + * @param position the position to check + * @param currentPlayerFaction the {@link Faction} of the current {@link Player} + * @return true if at least one adjacent {@link Road} is owned by the current {@link Player}, false otherwise + */ + private boolean isOwnRoadAdjacent(final Point position, final Faction currentPlayerFaction) { + for (Road road : getAdjacentEdges(position)) { + if (road != null && road.getFaction() == currentPlayerFaction) { + return true; + } + } + return false; + } + + + /** + * Returns whether any {@link Structure} owned by the current {@link Player} is adjacent to the specified position. + * + * @param position the position to check + * @param currentPlayerFaction the {@link Faction} of the current {@link Player} + * @return true if a {@link Structure} owned by the current {@link Player} is adjacent to the specified position, false otherwise + */ + private boolean isOwnStructureAdjacent(final Point position, final Faction currentPlayerFaction) { + return isSpecificOwnStructureAdjacent(position, null, currentPlayerFaction); + } + + /** + * Returns whether a specific type of {@link Structure} owned by the current {@link Player} is adjacent to the given position. + * + * @param position the position to check + * @param structureType the type of {@link Structure} to check, or null if any type is acceptable + * @param currentPlayerFaction the {@link Faction} of the current {@link Player} + * @return true if a specific type of {@link Structure} owned by the current {@link Player} is adjacent to the given position, + * false otherwise + */ + private boolean isSpecificOwnStructureAdjacent(final Point position, final Config.Structure structureType, + final Faction currentPlayerFaction) { + final Structure corner = getCorner(position); + final boolean isOwnStructureAdjacent = corner != null && corner.getFaction() == currentPlayerFaction; + return structureType == null + ? isOwnStructureAdjacent + : isOwnStructureAdjacent && corner.getStructureType() == structureType; + } + +} diff --git a/src/main/java/ch/zhaw/catan/board/SiedlerBoardTextView.java b/src/main/java/ch/zhaw/catan/board/SiedlerBoardTextView.java new file mode 100644 index 0000000..2e26976 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/board/SiedlerBoardTextView.java @@ -0,0 +1,128 @@ +package ch.zhaw.catan.board; + +import ch.zhaw.catan.structure.Road; +import ch.zhaw.catan.structure.Structure; +import ch.zhaw.hexboard.HexBoardTextView; +import ch.zhaw.hexboard.Label; + +import java.awt.Point; +import java.util.Map; + +import static ch.zhaw.catan.board.SiedlerBoard.MAX_X_COORDINATE; +import static ch.zhaw.catan.board.SiedlerBoard.MAX_Y_COORDINATE; +import static ch.zhaw.catan.game.Config.getStandardDiceNumberPlacement; +import static java.lang.String.format; +import static java.lang.System.lineSeparator; + +/** + * Represents a class responsible to print out the game board created in {@link SiedlerBoard}. + */ +public class SiedlerBoardTextView extends HexBoardTextView { + + private static final String WHITE_SPACE = " "; + private static final int ZERO_DIGIT_CHANGE = 10; + + /** + * Creates a new SiedlerBoardTextView object and initializes it with a {@link SiedlerBoard} object. + * It also sets the lower field labels of the board to the corresponding dice numbers. + * For example, if a field has a dice number of 7, the lower field label will be "07". + * + * @param board The {@link SiedlerBoard} object to be used for the SiedlerBoardTextView. + */ + public SiedlerBoardTextView(final SiedlerBoard board) { + super(board); + for (Map.Entry entry : getStandardDiceNumberPlacement().entrySet()) { + String label = format("%02d", entry.getValue()); + setLowerFieldLabel(entry.getKey(), new Label(label.charAt(0), label.charAt(1))); + } + } + + /** + * Returns a string representation of the board with coordinate system + * + * @return a string representation of the board + */ + @Override + public String toString() { + return getBoardWithCoordinateSystem(super.toString()); + } + + /** + * This method takes a String representing a board and adds a coordinate system to it. + * It adds the x and y coordinates to the edges of the board, with the x coordinates + * at the top and the y coordinates at the left. + * + * @param board String representation of the board + * @return String representation of the board with the coordinate system + */ + private String getBoardWithCoordinateSystem(final String board) { + final StringBuilder sb = new StringBuilder(); + + sb.append(lineSeparator()); + sb.append(WHITE_SPACE.repeat(4)); + appendXCoordinates(sb); + sb.append(lineSeparator().repeat(2)); + + final String[] lines = board.split(lineSeparator()); + appendYCoordinates(sb, lines); + + return sb.toString(); + } + + /** + * Appends the y-coordinates to the given {@link StringBuilder} for each line in the given array of lines. + * + * @param sb the {@link StringBuilder} to append the y-coordinates to + * @param lines the array of lines to append y-coordinates for + */ + private void appendYCoordinates(final StringBuilder sb, final String[] lines) { + int currentBoardLine = 0; + int y = 0; + while (currentBoardLine < lines.length) { + if (y < ZERO_DIGIT_CHANGE) { + sb.append(WHITE_SPACE); + } + + if (currentBoardLine % 5 == 0) { + sb.append(y++); + sb.append(WHITE_SPACE); + sb.append(lines[currentBoardLine]); + + currentBoardLine++; + + sb.append(lineSeparator()); + sb.append(WHITE_SPACE.repeat(2)); + } else if (y <= MAX_Y_COORDINATE) { + sb.append(currentBoardLine % 5 == 3 ? y : y++); + } else { + sb.append(WHITE_SPACE.repeat(2)); + } + + sb.append(WHITE_SPACE); + sb.append(lines[currentBoardLine]); + sb.append(lineSeparator()); + + currentBoardLine++; + } + } + + /** + * Appends the X coordinates to the given {@link StringBuilder}. + * + * @param sb the {@link StringBuilder} to append the coordinates to + */ + private void appendXCoordinates(final StringBuilder sb) { + for (int x = 0; x <= MAX_X_COORDINATE; x++) { + if (x < ZERO_DIGIT_CHANGE) { + sb.append(WHITE_SPACE); + } + + sb.append(x); + + if (x < MAX_X_COORDINATE) { + sb.append(WHITE_SPACE.repeat(6)); + } + } + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/Activity.java b/src/main/java/ch/zhaw/catan/game/Activity.java new file mode 100644 index 0000000..1f05ece --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/Activity.java @@ -0,0 +1,64 @@ +package ch.zhaw.catan.game; + +/** + * Represents all available in-game activities. + */ + +public enum Activity { + + /** + * Displays the current state of the {@link ch.zhaw.catan.board.SiedlerBoard}. + */ + DISPLAY_BOARD, + + /** + * Displays the {@link ch.zhaw.catan.game.Config.Resource} of the current {@link Player}. + */ + DISPLAY_PLAYER_RESOURCES, + + /** + * Displays the costs for all the structures defined in {@link ch.zhaw.catan.game.Config.Structure} + */ + DISPLAY_BANK_RESOURCES, + + /** + * Displays the costs for all the structures defined in {@link ch.zhaw.catan.game.Config.Structure} + */ + DISPLAY_STRUCTURE_COSTS, + + /** + * Displays the scores of all participating {@link Player}s + */ + DISPLAY_SCORES, + + /** + * Trades {@link ch.zhaw.catan.game.Config.Resource} from current {@link Player} with {@link Bank}, with the ratio 4:1 + */ + TRADE, + + /** + * Builds a {@link ch.zhaw.catan.structure.Road} between two corners. + */ + BUILD_ROAD, + + /** + * Builds a {@link ch.zhaw.catan.structure.Settlement} on a corner. + */ + BUILD_SETTLEMENT, + + /** + *Replaces a {@link ch.zhaw.catan.structure.Settlement} with a {@link ch.zhaw.catan.structure.City}. + */ + BUILD_CITY, + + /** + *Swaps to the next {@link Player} and starts a new turn. + */ + NEXT_TURN, + + /** + * Ends the Game. + */ + QUIT + +} diff --git a/src/main/java/ch/zhaw/catan/game/App.java b/src/main/java/ch/zhaw/catan/game/App.java new file mode 100644 index 0000000..e0cfa14 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/App.java @@ -0,0 +1,442 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Structure; +import ch.zhaw.catan.structure.City; +import ch.zhaw.catan.structure.Road; +import ch.zhaw.catan.structure.Settlement; + +import java.awt.Point; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static ch.zhaw.catan.board.SiedlerBoard.MAX_X_COORDINATE; +import static ch.zhaw.catan.board.SiedlerBoard.MAX_Y_COORDINATE; +import static ch.zhaw.catan.board.SiedlerBoard.MIN_COORDINATE; +import static ch.zhaw.catan.game.Config.Faction; +import static ch.zhaw.catan.game.Config.MAX_CARDS_IN_HAND_NO_DROP; +import static ch.zhaw.catan.game.Config.MIN_NUMBER_OF_PLAYERS; +import static ch.zhaw.catan.game.Config.Resource; +import static ch.zhaw.catan.game.Console.QUIT_SHORTCUT; +import static ch.zhaw.catan.game.SiedlerGame.FOUR_TO_ONE_TRADE_OFFER; +import static ch.zhaw.catan.game.SiedlerGame.FOUR_TO_ONE_TRADE_WANT; +import static java.lang.String.format; +import static java.lang.Thread.sleep; +import static java.util.EnumSet.allOf; + +/** + * App class to start a new game of Siedler. It contains the main game flow. + */ +public class App { + + static final int REQUIRED_WINNING_SCORE = 7; + static final Random RANDOM = new Random(); + + private static final String SETTLEMENT_NAME = Settlement.class.getSimpleName(); + private static final String CITY_NAME = City.class.getSimpleName(); + private static final String ROAD_NAME = Road.class.getSimpleName(); + private static final String THIEF_NAME = "Thief"; + + private final Console console; + private final Dice dice; + + private SiedlerGame game; + + /** + * Constructs a new App object. + */ + public App() { + console = new Console("Catan"); + dice = new Dice(); + } + + /** + * Main method to start a new game of Siedler. + * + * @param args command line arguments + * @throws InterruptedException if thread is interrupted while sleeping + */ + public static void main(final String[] args) throws InterruptedException { + new App().runGame(); + } + + /** + * Runs the game of Siedler. + * + * @throws InterruptedException if thread is interrupted while sleeping + */ + public void runGame() throws InterruptedException { + printIntro(); + + final int numberOfPlayers = console.readInteger("Please enter the number of players", MIN_NUMBER_OF_PLAYERS, allOf(Faction.class).size()); + game = new SiedlerGame(REQUIRED_WINNING_SCORE, numberOfPlayers); + + printBoardView(); + runInitiationPhase(numberOfPlayers); + runGameTurns(); + } + + /** + * Prints the introduction message for the game. + */ + private void printIntro() { + console.printLine("Welcome to a new game of Settlers of Catan!"); + console.print("You can exit the game anytime by pressing the key " + QUIT_SHORTCUT + "."); + console.printEmptyLine(); + } + + /** + * This method prints the current board view to the console. + */ + private void printBoardView() { + console.printLine(game.getBoard().getView().toString()); + } + + /** + * Runs the initiation phase of the game, where players place their initial {@link Settlement}s and {@link Road}s. + * + * @param numberOfPlayers the number of {@link Player}s in the game + */ + private void runInitiationPhase(final int numberOfPlayers) { + for (int i = 0; i < numberOfPlayers; i++) { + placeInitialStructures(false); + game.switchToNextPlayer(); + } + + for (int i = 0; i < numberOfPlayers; i++) { + game.switchToPreviousPlayer(); + placeInitialStructures(true); + } + + console.printLine("Initiation phase ended. Let the game begin!"); + console.printEmptyLine(); + } + + /** + * Places a {@link Settlement} and a {@link Road} for the current {@link Player}. + * + * @param withPayout if true, the {@link Player} will receive {@link Resource}s for placing the {@link Structure}s + */ + private void placeInitialStructures(final boolean withPayout) { + placeInitialSettlement(withPayout); + placeInitialRoad(withPayout); + } + + /** + * Lets the current {@link Player} place an initial {@link Settlement} and handles the placement. + * + * @param withPayout if true, the {@link Player} will receive {@link Resource}s for placing the {@link Settlement} + */ + private void placeInitialSettlement(final boolean withPayout) { + console.printLine(game.getCurrentPlayerFaction() + " please place your " + (withPayout ? "second " : "first ") + SETTLEMENT_NAME + "."); + boolean isPlaced = false; + while (!isPlaced) { + final Point point = readCoordinates("for your " + SETTLEMENT_NAME); + if (game.placeInitialSettlement(point, withPayout)) { + isPlaced = true; + printBoardView(); + printValidPlacement(SETTLEMENT_NAME); + } else { + printInvalidPlacement(SETTLEMENT_NAME); + } + } + } + + /** + * Lets the current {@link Player} place an initial {@link Road} and handles the placement. + * + * @param isSecondRoad if true, the print message will be adapted to the second {@link Road} + */ + private void placeInitialRoad(final boolean isSecondRoad) { + console.printLine(game.getCurrentPlayerFaction() + " please place your " + (isSecondRoad ? "second " : "first ") + ROAD_NAME + "."); + boolean isPlaced = false; + while (!isPlaced) { + final Point startPoint = readCoordinates("for the " + ROAD_NAME + " start point"); + final Point endPoint = readCoordinates("for the " + ROAD_NAME + " end point"); + + if (game.placeInitialRoad(startPoint, endPoint)) { + isPlaced = true; + printBoardView(); + printValidPlacement(ROAD_NAME); + } else { + printInvalidPlacement(ROAD_NAME); + } + } + } + + /** + * Reads a coordinate from the user and validates it. + * + * @param prompt the type of {@link Structure} ({@link Settlement}, {@link City}, or {@link Road}) + * @return the coordinate of the {@link Structure} + */ + private Point readCoordinates(final String prompt) { + final int xCoordinate = console.readInteger(game.getCurrentPlayerFaction() + " please enter the x-coordinate " + + prompt, MIN_COORDINATE, MAX_X_COORDINATE); + final int yCoordinate = console.readInteger(game.getCurrentPlayerFaction() + " please enter the y-coordinate " + + prompt, MIN_COORDINATE, MAX_Y_COORDINATE); + return new Point(xCoordinate, yCoordinate); + } + + /** + * Runs the game turns, where {@link Player}s take their actions in turn until the game ends. + * + * @throws InterruptedException if thread is interrupted while sleeping + */ + private void runGameTurns() throws InterruptedException { + boolean isGameFinished = false; + processDiceRoll(dice.roll()); + while (!isGameFinished) { + console.printEmptyLine(); + final Activity chosenActivity = console.readEnum(game.getCurrentPlayerFaction() + + " choose any of the following activities", Activity.class); + console.printEmptyLine(); + switch (chosenActivity) { + case DISPLAY_BOARD -> printBoardView(); + case DISPLAY_PLAYER_RESOURCES -> printResourcesOfCurrentPlayer(); + case DISPLAY_BANK_RESOURCES -> printResourcesOfBank(); + case DISPLAY_STRUCTURE_COSTS -> printStructureCosts(); + case DISPLAY_SCORES -> printScores(); + case TRADE -> trade(); + case BUILD_ROAD -> buildRoad(); + case BUILD_SETTLEMENT -> isGameFinished = buildSettlement(); + case BUILD_CITY -> isGameFinished = buildCity(); + case NEXT_TURN -> { + game.switchToNextPlayer(); + printBoardView(); + processDiceRoll(dice.roll()); + } + case QUIT -> { + console.printLine("Thanks for playing, see you next game!"); + sleep(3000); + console.close(); + isGameFinished = true; + } + } + } + } + + /** + * Processes a dice roll and updates the game state accordingly. + * + * @param diceRoll the result of the dice roll + */ + private void processDiceRoll(final int diceRoll) { + console.printLine(game.getCurrentPlayerFaction() + " rolled " + diceRoll); + final DiceResult diceResult = game.processDiceRoll(diceRoll); + if (diceResult.isThiefExecuted()) { + console.printEmptyLine(); + console.printLine("The " + THIEF_NAME + " was rolled. Resources of any player who has more than " + + MAX_CARDS_IN_HAND_NO_DROP + " were cut in half."); + placeThiefAndStealCard(); + } + printDiceResultResources(diceResult); + } + + /** + * Lets the current {@link Player} place the thief on the board and steal a {@link Resource} card from another {@link Player}. + */ + private void placeThiefAndStealCard() { + boolean isPlaced = false; + while (!isPlaced) { + final Point point = readCoordinates("for the new location of the " + THIEF_NAME); + if (game.placeThiefAndStealCard(point)) { + isPlaced = true; + printValidPlacement(THIEF_NAME); + } else { + printInvalidPlacement(THIEF_NAME); + } + } + } + + /** + * Prints the {@link Resource}s received or lost by each {@link Player} as a result of the given dice roll. + * + * @param diceResult the result of the dice roll + */ + private void printDiceResultResources(final DiceResult diceResult) { + boolean firstLine = true; + + for (Map.Entry> resourcesPerFaction : diceResult.getAffectedResources().entrySet()) { + if (resourcesPerFaction.getValue() != null && !resourcesPerFaction.getValue().isEmpty()) { + if (firstLine) { + console.printEmptyLine(); + firstLine = false; + } + + console.print(resourcesPerFaction.getKey() + " has " + (diceResult.isThiefExecuted() ? "lost" : "received") + " the following resources: "); + printAllResources(resourcesPerFaction.getValue()); + console.printEmptyLine(); + } + } + } + + /** + * Prints the amount of each {@link Resource} in the given {@link Map}. + * + * @param amountPerResources a {@link Map} of {@link Resource}s and their corresponding amount + */ + private void printAllResources(final Map amountPerResources) { + boolean firstLine = true; + + for (Map.Entry resource : amountPerResources.entrySet()) { + console.print((firstLine ? "" : ", ") + resource.getValue() + " " + resource.getKey().name()); + if (firstLine) { + firstLine = false; + } + } + } + + /** + * Prints the {@link Resource}s of the current {@link Player}. + */ + private void printResourcesOfCurrentPlayer() { + console.print(game.getCurrentPlayer().getFaction() + " possesses: "); + final Map inventory = game.getCurrentPlayer().getInventory(); + printAllResources(inventory); + console.printEmptyLine(); + } + + /** + * Prints the {@link Resource}s of the {@link Bank}. + */ + private void printResourcesOfBank() { + console.print("Bank possesses: "); + printAllResources(game.getBank().getInventory()); + console.printEmptyLine(); + } + + /** + * Prints the costs of all {@link Structure}s. + */ + private void printStructureCosts() { + console.printLine("Structure costs:"); + for (Structure structure : allOf(Structure.class)) { + console.print(structure.name() + " requires: "); + printAllResources(structure.getCostsAsIntegerMap()); + console.printEmptyLine(); + } + } + + /** + * Prints the scores of all {@link Player}s. + */ + private void printScores() { + final List> sortedFactionScores = new ArrayList<>(); + + for (Player player : game.getCurrentPlayers()) { + sortedFactionScores.add(new AbstractMap.SimpleEntry<>(player.getFaction(), player.getScore())); + } + + sortedFactionScores.sort(Map.Entry.comparingByValue(Comparator.reverseOrder())); + + console.printLine("Scores:"); + for (Map.Entry factionScore : sortedFactionScores) { + console.printLine(factionScore.getKey() + " with " + factionScore.getValue() + " points"); + } + } + + /** + * Lets the current {@link Player} trade with the {@link Bank}. + */ + private void trade() { + final Resource offer = console.readEnum(game.getCurrentPlayerFaction() + + " please choose any of the following resources for a " + FOUR_TO_ONE_TRADE_OFFER + ":" + FOUR_TO_ONE_TRADE_WANT + + " trade with the bank", Resource.class); + console.printEmptyLine(); + final Resource requisition = console.readEnum("Which resource would you like from the bank?", Resource.class); + console.printEmptyLine(); + if (game.tradeWithBankFourToOne(offer, requisition)) { + console.printLine("Trade was successful. " + FOUR_TO_ONE_TRADE_WANT + " " + requisition.name() + " has been added to your inventory."); + } else { + console.printLine("Trade failed. Either you or the bank have insufficient resources."); + } + } + + /** + * Lets the current {@link Player} build a {@link Settlement}. + * + * @return boolean to indicate if the current {@link Player} has won the game + * @throws InterruptedException if thread is interrupted while sleeping + */ + private boolean buildSettlement() throws InterruptedException { + if (game.buildSettlement(readCoordinates("for the " + SETTLEMENT_NAME))) { + printBoardView(); + printValidPlacement(SETTLEMENT_NAME); + return hasPlayerWon(); + } else { + printInvalidPlacement(SETTLEMENT_NAME); + return false; + } + } + + /** + * Lets the current {@link Player} build a {@link Road}. + */ + private void buildRoad() { + if (game.buildRoad(readCoordinates("for start point of " + ROAD_NAME), + readCoordinates("for end point of " + ROAD_NAME))) { + printBoardView(); + printValidPlacement(ROAD_NAME); + } else { + printInvalidPlacement(ROAD_NAME); + } + } + + /** + * Lets the current {@link Player} build a {@link City}. + * + * @return boolean to indicate if the current {@link Player} has won the game + * @throws InterruptedException if thread is interrupted while sleeping + */ + private boolean buildCity() throws InterruptedException { + if (game.buildCity(readCoordinates("for the " + CITY_NAME))) { + printBoardView(); + printValidPlacement(CITY_NAME); + return hasPlayerWon(); + } else { + printInvalidPlacement(CITY_NAME); + return false; + } + } + + /** + * Checks if the current {@link Player} has won the game. If the {@link Player} has won, a message is printed. + * + * @return true if the current {@link Player} has won the game, false otherwise + */ + private boolean hasPlayerWon() throws InterruptedException { + final Faction winner = game.getWinner(); + if (winner != null) { + console.printLine(winner + " has won the game. Congratulations!"); + console.print("You can exit the game by pressing the key " + QUIT_SHORTCUT + "."); + console.celebrate(); + return true; + } else { + return false; + } + } + + /** + * Prints a message indicating that the given type of {@link Structure} has been placed. + * + * @param type the type of {@link Structure} that has been placed + */ + private void printValidPlacement(final String type) { + console.printLine(String.format("%s has been placed.", type)); + } + + /** + * Prints a message indicating that the given type of object could not be placed. + * + * @param type the type of object that could not be placed + */ + private void printInvalidPlacement(final String type) { + console.printLine(format("Not able to place %s.", type)); + console.printEmptyLine(); + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/Bank.java b/src/main/java/ch/zhaw/catan/game/Bank.java new file mode 100644 index 0000000..9e44a1d --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/Bank.java @@ -0,0 +1,41 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Resource; +import ch.zhaw.catan.structure.Structure; + +import java.util.EnumMap; +import java.util.List; + +import static ch.zhaw.catan.game.Config.INITIAL_RESOURCE_CARDS_BANK; + +/** + * Represents a bank that provides resources to the {@link Player}s. It also tracks its own inventory. + */ +public class Bank extends InventoryOwner { + + /** + * Constructs a new bank with the given initial {@link Resource} cards. + */ + Bank() { + super(new EnumMap<>(INITIAL_RESOURCE_CARDS_BANK)); + } + + /** + * Determines whether the bank has sufficient inventory for the payout of a given {@link Resource} and {@link List} of {@link Structure}s. + * + * @param resource the {@link Resource} to check for + * @param structures the {@link List} of {@link Structure}s to consider + * @return true if the bank has sufficient inventory, false otherwise + */ + public boolean isInventorySufficientForPayoutOfResource(final Resource resource, final List structures) { + int availableBankResourceCards = getAmountOfResource(resource); + + for (Structure structure : structures) { + availableBankResourceCards -= structure.getAmountPerResource(); + } + + return availableBankResourceCards >= 0; + } + +} + diff --git a/src/main/java/ch/zhaw/catan/game/Config.java b/src/main/java/ch/zhaw/catan/game/Config.java new file mode 100644 index 0000000..30d19f1 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/Config.java @@ -0,0 +1,271 @@ +package ch.zhaw.catan.game; + +import java.awt.Point; +import java.util.Collections; +import java.util.EnumMap; +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 final 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 final 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 final 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 final List costs; + private final 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 + * @deprecated replaced by {@link #getCostsAsIntegerMap()} + */ + @Deprecated + public Map getCostsAsMap() { + return costs.stream() + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + } + + /** + * Returns the Structure costs as a map where the key is the {@link Resource} and the value is the amount + * @return Map containing the amounts per {@link Resource} + */ + public Map getCostsAsIntegerMap() { + Map resourceIntegerMap = new EnumMap<>(Resource.class); + for (Resource resource : getCosts()) { + resourceIntegerMap.merge(resource, 1, Integer::sum); + } + return resourceIntegerMap; + } + + /** + * 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() { + + return Map.ofEntries( + Map.entry(new Point(4, 8), 2), + Map.entry(new Point(7, 5), 3), + Map.entry(new Point(8, 14), 3), + Map.entry(new Point(6, 8), 4), + Map.entry(new Point(7, 17), 4), + Map.entry(new Point(3, 11), 5), + Map.entry(new Point(8, 8), 5), + Map.entry(new Point(5, 5), 6), + Map.entry(new Point(9, 11), 6), + Map.entry(new Point(7, 11), 7), + Map.entry(new Point(9, 5), 8), + Map.entry(new Point(5, 17), 8), + Map.entry(new Point(5, 11), 9), + Map.entry(new Point(11, 11), 9), + Map.entry(new Point(4, 14), 10), + Map.entry(new Point(10, 8), 10), + Map.entry(new Point(6, 14), 11), + Map.entry(new Point(9, 17), 11), + Map.entry(new Point(10, 14), 12)); + } + + // 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/main/java/ch/zhaw/catan/game/Console.java b/src/main/java/ch/zhaw/catan/game/Console.java new file mode 100644 index 0000000..a23f221 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/Console.java @@ -0,0 +1,158 @@ +package ch.zhaw.catan.game; + +import org.beryx.textio.TerminalProperties; +import org.beryx.textio.TextIO; +import org.beryx.textio.swing.SwingTextTerminal; + +import java.awt.Color; + +import static java.awt.Color.BLACK; +import static java.awt.Color.BLUE; +import static java.awt.Color.CYAN; +import static java.awt.Color.GREEN; +import static java.awt.Color.ORANGE; +import static java.awt.Color.PINK; +import static java.awt.Color.RED; +import static java.awt.Color.WHITE; +import static java.awt.Color.YELLOW; +import static java.lang.Thread.sleep; + +/** + * Represents a console that provides input and output capabilities for the game. + */ +public class Console { + + static final String QUIT_SHORTCUT = "Q"; + + private final TextIO textIO; + private final SwingTextTerminal textTerminal; + + /** + * Constructs a new Console with the given pane title. + * + * @param paneTitle the title of the terminal pane + */ + public Console(final String paneTitle) { + textIO = new TextIO(new SwingTextTerminal()); + textTerminal = (SwingTextTerminal) textIO.getTextTerminal(); + initProperties(paneTitle); + } + + /** + * Initializes the properties of the {@link org.beryx.textio.TextTerminal}. + * + * @param paneTitle the title of the terminal pane + */ + private void initProperties(final String paneTitle) { + textTerminal.setUserInterruptKey(QUIT_SHORTCUT); + textTerminal.setPaneTitle(paneTitle); + final TerminalProperties properties = getProperties(); + properties.setPromptColor(WHITE); + properties.setInputBold(true); + properties.setInputColor(ORANGE); + properties.setPaneDimension(1300, 1000); + } + + private TerminalProperties getProperties() { + return textTerminal.getProperties(); + } + + /** + * Prints the given message to the console without any line break. + * + * @param message the message to print + */ + public void print(final String message) { + textTerminal.print(message); + } + + /** + * Prints the given message to the console and inserts a line break afterwards. + * + * @param message the message to print + */ + public void printLine(final String message) { + textTerminal.println(message); + } + + /** + * Prints an empty line to the console. + */ + public void printEmptyLine() { + printLine(""); + } + + /** + * Reads an {@link Integer} from the console. + * + * @param prompt the prompt to display + * @param minValue the minimum value that is required + * @param maxValue the maximum value that is allowed + * @return the {@link Integer} that was read from the console + */ + public Integer readInteger(final String prompt, final int minValue, final int maxValue) { + return textIO.newIntInputReader() + .withMinVal(minValue) + .withMaxVal(maxValue) + .read(prompt); + } + + /** + * Reads an EnumClass from the console. + * + * @param prompt the prompt to display + * @param the {@link Enum} class + * @param enumClass the {@link Enum} class to read + * @return the {@link Enum} that was read from the console + */ + public > E readEnum(final String prompt, final Class enumClass) { + return textIO.newEnumInputReader(enumClass) + .read(prompt); + } + + /** + * Disposes the {@link TextIO} and {@link org.beryx.textio.TextTerminal} instances. + */ + public void close() { + textIO.dispose(); + textTerminal.dispose(); + } + + /** + * Changes the background color of the console to indicate that a {@link Player} has won. Celebration time! + * + * @throws InterruptedException if thread is interrupted while sleeping + */ + public void celebrate() throws InterruptedException { + for (int i = 0; i < 10; i++) { + colorChange(); + } + getProperties().setPaneBackgroundColor(BLACK); + } + + /** + * Changes the color of the pane in a sequence of colors. + * + * @throws InterruptedException if the thread is interrupted while sleeping + */ + private void colorChange() throws InterruptedException { + changePaneColor(GREEN); + changePaneColor(RED); + changePaneColor(BLUE); + changePaneColor(YELLOW); + changePaneColor(CYAN); + changePaneColor(PINK); + } + + /** + * Changes the background color of the pane to the specified color. + * + * @param color the color to set the pane's background color to + * @throws InterruptedException if the current thread is interrupted while sleeping + */ + private void changePaneColor(final Color color) throws InterruptedException { + getProperties().setPaneBackgroundColor(color); + sleep(75); + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/Dice.java b/src/main/java/ch/zhaw/catan/game/Dice.java new file mode 100644 index 0000000..f4eed63 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/Dice.java @@ -0,0 +1,31 @@ +package ch.zhaw.catan.game; + +import static ch.zhaw.catan.game.App.RANDOM; + +/** + * A class for representing a dice in the game of Catan. + */ +public class Dice { + + static final int LOWER_DICE_LIMIT = 1; + static final int UPPER_DICE_LIMIT = 6; + + /** + * Rolls the dice and returns the result. + * + * @return The result of the dice roll. + */ + public int roll() { + return getSingleDiceRoll() + getSingleDiceRoll(); + } + + /** + * Gets the result of rolling a single dice. + * + * @return The result of rolling a single dice. + */ + private int getSingleDiceRoll() { + return RANDOM.nextInt(UPPER_DICE_LIMIT) + LOWER_DICE_LIMIT; + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/DiceResult.java b/src/main/java/ch/zhaw/catan/game/DiceResult.java new file mode 100644 index 0000000..25166c8 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/DiceResult.java @@ -0,0 +1,49 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Faction; +import ch.zhaw.catan.game.Config.Resource; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * Represents the result of a dice roll. It indicates the affected {@link Resource}s per {@link Faction} by the dice roll and whether + * the thief was executed. + */ +public class DiceResult { + + private final Map> affectedResources; + private final boolean thiefExecuted; + + /** + * Creates a new `DiceResult` with the given affected {@link Resource}s and thief execution status. + * + * @param affectedResources the {@link Resource}s that were affected by this dice roll + * @param thiefExecuted whether the thief was executed as a result of this dice roll + */ + public DiceResult(final Map> affectedResources, + final boolean thiefExecuted) { + this.affectedResources = requireNonNull(affectedResources, "affectedResources must not be null"); + this.thiefExecuted = thiefExecuted; + } + + /** + * Returns the {@link Resource}s that were affected by this dice roll. + * + * @return the affected {@link Resource}s + */ + public Map> getAffectedResources() { + return affectedResources; + } + + /** + * Returns whether the thief was executed as a result of this dice roll. + * + * @return whether the thief was executed + */ + public boolean isThiefExecuted() { + return thiefExecuted; + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/InventoryOwner.java b/src/main/java/ch/zhaw/catan/game/InventoryOwner.java new file mode 100644 index 0000000..50b6909 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/InventoryOwner.java @@ -0,0 +1,159 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Resource; + +import java.util.EnumMap; +import java.util.Map; + +import static java.util.Map.entry; +import static java.util.Objects.requireNonNull; + +/** + * InventoryOwner super Class to hold Inventory of {@link Resource}s in an {@link EnumMap} + * The Inventory either can be initiated empty or with a given amount of starting resources. + * The class also provides functionality to increase or decrease the Inventory. + */ +public abstract class InventoryOwner { + + private static final Integer NO_RESOURCES = 0; + private final Map inventory; + + /** + * Default constructor which fills all {@link Resource}s using the default amount {@link #NO_RESOURCES} + */ + protected InventoryOwner() { + inventory = new EnumMap<>(Resource.class); + + for (Resource resource : Resource.values()) { + inventory.put(resource, NO_RESOURCES); + } + } + + /** + * Constructor of Inventory with a given Inventory List prepared. + * + * @param inventory {@link EnumMap} of {@link Resource} and {@link Integer} values + */ + protected InventoryOwner(final Map inventory) { + this.inventory = requireNonNull(inventory, "inventory must not be null"); + } + + /** + * Getter for the current Inventory + * + * @return inventory of Owner as {@link EnumMap} + */ + public Map getInventory() { + return inventory; + } + + /** + * Gives the amount for a specific {@link Resource} which is being held. + * + * @param resource the {@link Resource} to get the amount from + * @return the amount of said {@link Resource} + */ + public int getAmountOfResource(final Resource resource) { + return inventory.get(resource); + } + + /** + * Gives the amount of all {@link Resource}s which are being held. + * + * @return the amount of all {@link Resource}s + */ + public int getTotalAmountOfResources() { + int sum = 0; + for (Integer amountPerResource : getInventory().values()) { + sum += amountPerResource; + } + return sum; + } + + /** + * Function to increase an Inventory. + * Receives a {@link Map} of {@link Resource} with their respective amount to increment + * + * @param resourcesToAdd {@link Map} of {@link Resource} Objects and {@link Integer} + */ + public void increaseInventory(final Map resourcesToAdd) { + for (Map.Entry resourceToAdd : resourcesToAdd.entrySet()) { + increaseInventoryItem(resourceToAdd.getKey(), resourceToAdd.getValue()); + } + } + + /** + * Function to Increase the inventory by a single {@link Resource} only. + * Receives the {@link Resource} and {@link Integer} with a positive amount to increment. + * + * @param resource {@link Resource} + * @param amount Amount of the {@link Resource} that should be added. + */ + public void increaseInventoryItem(final Resource resource, final Integer amount) { + if (isValidIncrease(amount)) { + inventory.put(resource, inventory.get(resource) + amount); + } + } + + /** + * Function to decrease an Inventory. + * Receives a {@link Map} of {@link Resource} with their respective amount to decrement + * Checks first if the decrease is valid based on the current inventory. + * + * @param resourcesToRemove {@link Map} of {@link Resource} Objects and {@link Integer} + * @return returns {@code true} if the change was valid and could be made. + */ + public boolean decreaseInventoryIfApplicable(final Map resourcesToRemove) { + for (Map.Entry resourceToRemove : resourcesToRemove.entrySet()) { + if (!isValidDecrease(resourceToRemove.getKey(), resourceToRemove.getValue())) { + return false; + } + } + + for (Map.Entry resourceToRemove : resourcesToRemove.entrySet()) { + decreaseInventoryItemIfApplicable(resourceToRemove.getKey(), resourceToRemove.getValue()); + } + + return true; + } + + /** + * Function to decrease the inventory by a single {@link Resource} only. + * Receives the {@link Resource} and {@link Integer} with a positive amount to decrement. + * + * @param resource {@link Resource} + * @param amount Amount of the {@link Resource} that should be removed. + * @return Map.Entry with {@link Resource} and {@link Integer} or null if nothing could be decreased. + */ + public Map.Entry decreaseInventoryItemIfApplicable(final Resource resource, + final Integer amount) { + if (isValidDecrease(resource, amount)) { + inventory.put(resource, inventory.get(resource) - amount); + return entry(resource, amount); + } + + return null; + } + + /** + * Checks whether an amount is a valid increase. + * + * @param amount the amount to check + * @return true if the amount is greater than zero, false otherwise + */ + private boolean isValidIncrease(final Integer amount) { + return amount > 0; + } + + /** + * Checks whether a decrease in a given {@link Resource} is valid. + * + * @param resource the {@link Resource} to check + * @param amount the amount of the {@link Resource} to decrease + * @return true if the decrease is valid, false otherwise + */ + private boolean isValidDecrease(final Resource resource, final Integer amount) { + return amount > 0 && getAmountOfResource(resource) >= amount; + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/Player.java b/src/main/java/ch/zhaw/catan/game/Player.java new file mode 100644 index 0000000..4b04844 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/Player.java @@ -0,0 +1,188 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Faction; +import ch.zhaw.catan.game.Config.Resource; +import ch.zhaw.catan.structure.Structure; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static ch.zhaw.catan.game.App.RANDOM; +import static ch.zhaw.catan.game.Config.MAX_CARDS_IN_HAND_NO_DROP; +import static java.util.Objects.requireNonNull; + +/** + * Class for the players playing the game. + * The class holds information about score + * and gets the {@link Resource} inventory functionality from the {@link InventoryOwner} Class + */ +public class Player extends InventoryOwner { + + private final Faction faction; + private final List structures; + + /** + * Constructor creates new Player Object with the default amount of {@link Resource}s. + * As well as the given {@link Faction} + * + * @param faction {@link Faction} that the player is assigned for the game. + */ + public Player(final Faction faction) { + super(); + this.faction = requireNonNull(faction, "faction must not be null"); + structures = new ArrayList<>(); + } + + /** + * Getter for the {@link Faction} of the player + * + * @return the players {@link Faction} + */ + public Faction getFaction() { + return faction; + } + + /** + * Returns a list of all {@link Structure}s in this player's possession. + * + * @return a {@link List} of all {@link Structure}s in this player's possession + */ + public List getStructures() { + return structures; + } + + /** + * Adds the given {@link Structure} to this player's possession. + * + * @param structure the {@link Structure} to add + */ + public void addStructure(final Structure structure) { + structures.add(structure); + } + + /** + * Removes the given {@link Structure} from this player's possession. + * + * @param structure the {@link Structure} to remove + */ + public void removeStructure(final Structure structure) { + structures.remove(structure); + } + + /** + * Returns true if this player has not reached the maximum stock of the given {@link Structure} type. + * + * @param structureType the type of {@link Structure} to check + * @return true if this player has not reached the maximum stock of the given {@link Structure} type, false otherwise + */ + public boolean hasNotReachedMaxStockOf(final Config.Structure structureType) { + return getCurrentAmountOfStructureType(structureType) < structureType.getStockPerPlayer(); + } + + /** + * Processes a thief's action, which involves stealing half of the total number of {@link Resource}s + * from the player's inventory (rounded down). + * + * @return a {@link Map} of {@link Resource}s and their corresponding quantities that were stolen + */ + public Map processThief() { + final int totalResources = getTotalAmountOfResources(); + final Map stolenResources = new EnumMap<>(Resource.class); + + if (totalResources > MAX_CARDS_IN_HAND_NO_DROP) { + final List nonZeroResources = getNonZeroResources(); + int i = 0; + while (i < totalResources / 2) { + final int resourceIndex = RANDOM.nextInt(nonZeroResources.size()); + final Entry stolenResource = decreaseInventoryItemIfApplicable(nonZeroResources.get(resourceIndex), 1); + if (stolenResource != null) { + stolenResources.merge(stolenResource.getKey(), 1, Integer::sum); + i++; + } else { + nonZeroResources.remove(resourceIndex); + } + } + } + return stolenResources; + } + + /** + * Returns a list of non-zero {@link Resource}s from the player's inventory. + * + * @return a list of non-zero {@link Resource}s from the player's inventory + */ + private List getNonZeroResources() { + final List resources = new ArrayList<>(); + for (Resource resource : getInventory().keySet()) { + if (getAmountOfResource(resource) > 0) { + resources.add(resource); + } + } + return resources; + } + + /** + * Steals a random {@link Resource} from one of the provided nearby players. + * + * @param nearbyPlayersToStealFrom a {@link List} of nearby players to steal from + */ + public void stealRandomResourceFrom(final List nearbyPlayersToStealFrom) { + if (!nearbyPlayersToStealFrom.isEmpty()) { + final Player playerToStealFrom = nearbyPlayersToStealFrom.get(RANDOM.nextInt(nearbyPlayersToStealFrom.size())); + final List resourcesToStealFrom = playerToStealFrom.getNonZeroResources(); + final Resource resourceToSteal = resourcesToStealFrom.get(RANDOM.nextInt(resourcesToStealFrom.size())); + playerToStealFrom.decreaseInventoryItemIfApplicable(resourceToSteal, 1); + this.increaseInventoryItem(resourceToSteal, 1); + } + } + + /** + * Gives the amount of all {@link Resource}s which are being held. + * + * @return the amount of all {@link Resource}s + */ + public int getTotalAmountOfResources() { + int sum = 0; + for (Integer amountPerResource : getInventory().values()) { + sum += amountPerResource; + } + return sum; + } + + /** + * Returns the current amount of a specific {@link Structure} type owned by the player. + * + * @param structureType the type of {@link Structure} to check + * @return the current amount of the specified {@link Structure} type + */ + private int getCurrentAmountOfStructureType(final Config.Structure structureType) { + int amountOfStructure = 0; + + for (Structure existingStructure : getStructures()) { + if (existingStructure.getStructureType().equals(structureType)) { + amountOfStructure++; + } + } + + return amountOfStructure; + } + + /** + * Returns the current score for this player. + * + * @return the current score + */ + public int getScore() { + int score = 0; + + for (final Structure structure : getStructures()) { + score += structure.getScore(); + } + + return score; + } + +} diff --git a/src/main/java/ch/zhaw/catan/game/SiedlerGame.java b/src/main/java/ch/zhaw/catan/game/SiedlerGame.java new file mode 100644 index 0000000..4f0c87f --- /dev/null +++ b/src/main/java/ch/zhaw/catan/game/SiedlerGame.java @@ -0,0 +1,518 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.board.Field; +import ch.zhaw.catan.board.SiedlerBoard; +import ch.zhaw.catan.game.Config.Faction; +import ch.zhaw.catan.game.Config.Land; +import ch.zhaw.catan.game.Config.Resource; +import ch.zhaw.catan.structure.City; +import ch.zhaw.catan.structure.Road; +import ch.zhaw.catan.structure.Settlement; +import ch.zhaw.catan.structure.Structure; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static ch.zhaw.catan.game.Config.Structure.CITY; +import static ch.zhaw.catan.game.Config.Structure.ROAD; +import static ch.zhaw.catan.game.Config.Structure.SETTLEMENT; +import static java.util.EnumSet.allOf; + +/** + * This class performs all actions related to modifying the game state of the Settler of Catan board game. + *

+ * The SiedlerGame class is responsible for maintaining the state of the game, including the {@link Player}s and their inventories, + * the {@link SiedlerBoard}, and the bank. It provides methods for performing actions such as rolling the dice, building {@link Structure}s, + * and trading {@link Resource}s. + */ +public class SiedlerGame { + + static final int FOUR_TO_ONE_TRADE_OFFER = 4; + static final int FOUR_TO_ONE_TRADE_WANT = 1; + static final int THIEF_DICE_NUMBER = 7; + + private static final List FACTION_ASSIGNMENTS = new ArrayList<>(); + + static { + FACTION_ASSIGNMENTS.addAll(allOf(Faction.class)); + } + + private final List currentPlayers; + private final SiedlerBoard board; + private final Bank bank; + private final int winPoints; + + private int currentPlayerIndex; + + /** + * Constructs a SiedlerGame game state object. + * + * @param winPoints the number of points required to win the game + * @param numberOfPlayers the number of {@link Player}s + */ + public SiedlerGame(final int winPoints, final int numberOfPlayers) { + this.winPoints = winPoints; + bank = new Bank(); + + currentPlayerIndex = 0; + currentPlayers = new ArrayList<>(); + for (int i = 0; i < numberOfPlayers; i++) { + addPlayer(new Player(FACTION_ASSIGNMENTS.get(i))); + } + + board = new SiedlerBoard(); + } + + /** + * Switches to the next {@link Player} in the defined sequence of {@link Player}s. + */ + public void switchToNextPlayer() { + currentPlayerIndex = (currentPlayerIndex + 1) % currentPlayers.size(); + } + + /** + * Switches to the previous {@link Player} in the defined sequence of {@link Player}s. + */ + public void switchToPreviousPlayer() { + currentPlayerIndex = currentPlayerIndex == 0 + ? currentPlayers.size() - 1 + : currentPlayerIndex - 1; + } + + /** + * Returns the {@link Faction}s of the active {@link Player}s. + *

+ * The order of the {@link Player}'s {@link Faction}s in the {@link List} must correspond to the order in which they play. Hence, the + * {@link Player} that sets the first {@link Settlement} must be at position 0 in the list etc.

+ *

Important note: The {@link List} must contain the {@link Faction}s of active {@link Player}s only.

+ * + * @return the {@link List} with {@link Player}'s {@link Faction}s + */ + public List getPlayerFactions() { + final List list = new ArrayList<>(); + + for (Player currentPlayer : currentPlayers) { + Faction faction = currentPlayer.getFaction(); + list.add(faction); + } + + return list; + } + + /** + * Returns the game {@link SiedlerBoard}. + * + * @return the game board + */ + public SiedlerBoard getBoard() { + return this.board; + } + + /** + * Returns the {@link Faction} of the current {@link Player}. + * + * @return the {@link Faction} of the current {@link Player} + */ + public Faction getCurrentPlayerFaction() { + return getCurrentPlayer().getFaction(); + } + + /** + * Returns how many {@link Resource}e cards of the specified type the current {@link Player} owns. + * + * @param resource the {@link Resource} type + * @return the number of {@link Resource} cards of this type + */ + public int getCurrentPlayerResourceStock(final Resource resource) { + return getCurrentPlayer().getAmountOfResource(resource); + } + + /** + * Places a {@link Settlement} in the founder's phase (phase II) of the game. + * + *

The placement does not cost any {@link Resource} cards. If payout is set to true, for each adjacent resource-producing + * {@link Field}, a {@link Resource} card of the type of the {@link Resource} produced by the {@link Field} is taken from the {@link Bank} (if available) + * and added to the {@link Player}s' stock of {@link Resource} cards.

+ * + * @param position the position of the {@link Settlement} + * @param payout if true, the {@link Player} gets one {@link Resource} card per adjacent resource-producing {@link Field} + * @return true, if the placement was successful, false otherwise + */ + public boolean placeInitialSettlement(final Point position, final boolean payout) { + if (board.canPlaceSettlementOnPosition(position, true, getCurrentPlayerFaction())) { + final Structure settlement = new Settlement(getCurrentPlayerFaction()); + getCurrentPlayer().addStructure(settlement); + board.setCorner(position, settlement); + + if (payout) { + for (Land land : board.getLandsForCorner(position)) { + bank.decreaseInventoryItemIfApplicable(land.getResource(), 1); + getCurrentPlayer().increaseInventoryItem(land.getResource(), 1); + } + } + + return true; + } + + return false; + } + + /** + * Places a {@link Road} in the founder's phase (phase II) of the game. The placement does not cost any {@link Resource} cards. + * + * @param roadStart position of the start of the {@link Road} + * @param roadEnd position of the end of the {@link Road} + * @return true, if the placement was successful, false otherwise + */ + public boolean placeInitialRoad(final Point roadStart, final Point roadEnd) { + if (board.canPlaceRoadOnPosition(roadStart, roadEnd, getCurrentPlayerFaction())) { + final Road road = new Road(getCurrentPlayerFaction()); + getCurrentPlayer().addStructure(road); + board.setEdge(roadStart, roadEnd, road); + + return true; + } + + return false; + } + + /** + * This method takes care of actions depending on the dice throw result. + *

+ * A key action is the payout of the {@link Resource} cards to the {@link Player}s 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} {@link Resource} cards. + *

+ * If a {@link Player} does not get {@link Resource} cards, the list for this {@link Player}s' {@link Faction} is an empty {@link List} (not + * {@code null})!. + *

+ * The payout rules of the game take into account factors such as, the number of {@link Resource} cards currently available + * in the {@link Bank}, {@link Settlement} types({@link Settlement} or {@link City}), and the number of {@link Player}s that should get {@link Resource} cards of a + * certain type (relevant if there are not enough left in the {@link Bank}). + *

+ * + * @param dicethrow the {@link Resource} cards that have been distributed to the {@link Player}s + * @return the {@link Resource} cards added to the stock of the different {@link Player}s + * @deprecated replaced by {@link #processDiceRoll(int)} + */ + @Deprecated(since = "1.0", forRemoval = true) + Map> throwDice(int dicethrow) { + throw new UnsupportedOperationException("Method has been replaced by processDiceRoll(int). Please use that method instead."); + } + + /** + * Delegates the dice roll to either have the thief executed {@link #executeThief()} or to pay out {@link Resource}s + * {@link #payoutResources(int)} to the {@link Player}s having a {@link Settlement} or {@link City} nearby. + * + * @param diceRoll the latest dice roll by the current {@link Player} + * @return It returns a {@link DiceResult} object containing the affected {@link Faction}'s {@link Resource}s and an indication if + * the thief has been executed or not. + */ + public DiceResult processDiceRoll(final int diceRoll) { + if (isThiefRolled(diceRoll)) { + return new DiceResult(executeThief(), true); + } else { + return new DiceResult(payoutResources(diceRoll), false); + } + } + + /** + * Builds a {@link Settlement} at the specified position on the board. + * + *

The {@link Settlement} can be built if: + *

    + *
  • the {@link Player} possesses the required {@link Resource} cards
  • + *
  • a {@link Settlement} to place on the board
  • + *
  • the specified position meets the build rules for {@link Settlement}s
  • + *
+ * + * @param position the position of the {@link Settlement} + * @return true, if the placement was successful, false otherwise + */ + public boolean buildSettlement(final Point position) { + if (board.canPlaceSettlementOnPosition(position, false, getCurrentPlayerFaction()) + && getCurrentPlayer().hasNotReachedMaxStockOf(SETTLEMENT) + && getCurrentPlayer().decreaseInventoryIfApplicable(SETTLEMENT.getCostsAsIntegerMap())) { + bank.increaseInventory(SETTLEMENT.getCostsAsIntegerMap()); + final Structure settlement = new Settlement(getCurrentPlayerFaction()); + getCurrentPlayer().addStructure(settlement); + board.setCorner(position, settlement); + + return true; + } + + return false; + } + + /** + * Builds a {@link City} at the specified position on the board. + * + *

The {@link City} can be built if: + *

    + *
  • the {@link Player} possesses the required {@link Resource} cards
  • + *
  • a {@link City} to place on the board
  • + *
  • the specified position meets the build rules for cities
  • + *
+ * + * @param position the position of the {@link City} + * @return true, if the placement was successful, false otherwise + */ + public boolean buildCity(final Point position) { + if (board.canPlaceCityOnPosition(position, getCurrentPlayerFaction()) + && getCurrentPlayer().hasNotReachedMaxStockOf(CITY) + && getCurrentPlayer().decreaseInventoryIfApplicable(CITY.getCostsAsIntegerMap())) { + bank.increaseInventory(CITY.getCostsAsIntegerMap()); + final Structure city = new City(getCurrentPlayerFaction()); + getCurrentPlayer().removeStructure(board.getCorner(position)); + getCurrentPlayer().addStructure(city); + board.setCorner(position, city); + + return true; + } + + return false; + } + + /** + * Builds a {@link Road} at the specified position on the board. + * + *

The {@link Road} can be built if: + *

    + *
  • the {@link Player} possesses the required {@link Resource} cards
  • + *
  • a {@link Road} to place on the board
  • + *
  • the specified position meets the build rules for {@link Road}s
  • + *
+ * + * @param roadStart the position of the start of the {@link Road} + * @param roadEnd the position of the end of the {@link Road} + * @return true, if the placement was successful, false otherwise + */ + public boolean buildRoad(final Point roadStart, final Point roadEnd) { + if (board.canPlaceRoadOnPosition(roadStart, roadEnd, getCurrentPlayerFaction()) + && getCurrentPlayer().hasNotReachedMaxStockOf(ROAD) + && getCurrentPlayer().decreaseInventoryIfApplicable(ROAD.getCostsAsIntegerMap())) { + bank.increaseInventory(ROAD.getCostsAsIntegerMap()); + final Road road = new Road(getCurrentPlayerFaction()); + getCurrentPlayer().addStructure(road); + board.setEdge(roadStart, roadEnd, road); + + return true; + } + + return false; + } + + /** + *

Trades in {@link #FOUR_TO_ONE_TRADE_OFFER} {@link Resource} cards of the + * offered type for {@link #FOUR_TO_ONE_TRADE_WANT} {@link Resource} cards of the wanted type. + *

+ * The trade only works when {@link Bank} and {@link Player} possess the {@link 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, false otherwise + */ + public boolean tradeWithBankFourToOne(final Resource offer, final Resource want) { + if (FOUR_TO_ONE_TRADE_OFFER <= getCurrentPlayerResourceStock(offer) && + FOUR_TO_ONE_TRADE_WANT <= bank.getAmountOfResource(want)) { + getCurrentPlayer().decreaseInventoryItemIfApplicable(offer, FOUR_TO_ONE_TRADE_OFFER); + getCurrentPlayer().increaseInventoryItem(want, FOUR_TO_ONE_TRADE_WANT); + bank.decreaseInventoryItemIfApplicable(want, FOUR_TO_ONE_TRADE_WANT); + bank.increaseInventoryItem(offer, FOUR_TO_ONE_TRADE_OFFER); + + return true; + } + + 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() { + return getCurrentPlayer().getScore() >= winPoints + ? getCurrentPlayer().getFaction() + : null; + } + + /** + * Places the thief on the specified {@link Field} and steals a random {@link Resource} card (if the {@link Player} has such cards) + * from a random {@link Player} with a {@link Settlement} at that fieldPosition (if there is a {@link Settlement}) and adds it to the + * {@link Resource} cards of the current {@link Player}. + * + * @param fieldPosition the fieldPosition on which to place the thief + * @return false, if the specified fieldPosition is not a fieldPosition or the thief cannot be placed there (e.g., + * on water) + */ + public boolean placeThiefAndStealCard(final Point fieldPosition) { + if (!board.canPlaceThiefOnPosition(fieldPosition)) { + return false; + } + + board.switchThiefPosition(fieldPosition); + + final Set otherCornerFactions = board.getOtherFieldCornerFactions(getCurrentPlayerFaction()); + getCurrentPlayer().stealRandomResourceFrom(getNearbyPlayersToStealFrom(otherCornerFactions)); + return true; + } + + /** + * Returns a {@link List} of {@link Player}s that are nearby and have {@link Resource}s to steal. + * + * @param otherCornerFactions the {@link Faction}s of the {@link Player}s in the nearby corners + * @return a {@link List} of {@link Player}s that can be stolen from + */ + private List getNearbyPlayersToStealFrom(final Set otherCornerFactions) { + final List nearbyPlayers = new ArrayList<>(); + for (Faction faction : otherCornerFactions) { + final Player player = getPlayerFromFaction(faction); + if (player != null && player.getTotalAmountOfResources() > 0) { + nearbyPlayers.add(player); + } + } + return nearbyPlayers; + } + + /** + * Returns the {@link List} of {@link Player}s currently playing the game. + * + * @return the {@link List} of {@link Player}s + */ + public List getCurrentPlayers() { + return currentPlayers; + } + + /** + * Returns the {@link Player} who is currently playing the game. + * + * @return the current {@link Player} + */ + public Player getCurrentPlayer() { + return currentPlayers.get(currentPlayerIndex); + } + + /** + * Returns whether the thief has been rolled on the dice. + * + * @param diceRoll the number rolled on the dice + * @return true if the thief was rolled, false otherwise + */ + private boolean isThiefRolled(final int diceRoll) { + return diceRoll == THIEF_DICE_NUMBER; + } + + /** + * Takes half of the {@link Resource}s in the {@link Player} inventory is bigger than the required amount. + * The {@link Resource} {@link Map} is given to the {@link Bank}. + * + * @return returns {@link Map} with {@link Faction}s and {@link Resource} and {@link Integer} taken. + */ + public Map> executeThief() { + final Map> stolenResourcesPerFaction = new EnumMap<>(Faction.class); + for (Player player : getCurrentPlayers()) { + Map resourcesStolen = player.processThief(); + stolenResourcesPerFaction.put(player.getFaction(), resourcesStolen); + bank.increaseInventory(resourcesStolen); + } + + return stolenResourcesPerFaction; + } + + /** + * Returns the {@link Bank}. + * + * @return the {@link Bank} + */ + public Bank getBank() { + return bank; + } + + /** + * Return the {@link Player} object for the specified {@link Faction}. + * + * @param playerFaction the {@link Faction} for which the {@link Player} object is requested + * @return the {@link Player} object for the specified {@link Faction} + */ + private Player getPlayerFromFaction(final Faction playerFaction) { + for (Player player : currentPlayers) { + if (player.getFaction() == playerFaction) { + return player; + } + } + + return null; + } + + /** + * Adds a {@link Player} to the game. + * + * @param player the {@link Player} to add + */ + private void addPlayer(final Player player) { + currentPlayers.add(player); + } + + /** + * Calculates the {@link Resource}s that each {@link Player} should receive based on the given dice roll. + * + * @param diceRoll the dice roll to use for the calculation + * @return a {@link Map} of {@link Faction}s to maps of {@link Resource}s to amounts, indicating the {@link Resource}s each {@link Player} should receive + */ + private Map> payoutResources(final int diceRoll) { + final Map> paidOutResources = new EnumMap<>(Faction.class); + + for (Field field : board.getFieldsForDiceValue(diceRoll)) { + if (!field.isOccupiedByThief()) { + final Resource fieldResource = field.getLand().getResource(); + final List adjacentStructures = board.getCornersOfField(field.getPosition()); + + if (bank.isInventorySufficientForPayoutOfResource(fieldResource, adjacentStructures)) { + for (Structure structure : adjacentStructures) { + payoutResourcesBasedOnStructureType(structure, fieldResource, paidOutResources); + } + } + } + } + + return paidOutResources; + } + + /** + * Pay out {@link Resource}s to the {@link Player} who owns the given {@link Structure} based on the type of {@link Structure} and the specified {@link Resource}. + * + * @param structure the {@link Structure} to pay out {@link Resource}s for + * @param resource the {@link Resource} to pay out + * @param paidOutResources a {@link Map} of {@link Resource}s paid out to each {@link Player} + */ + private void payoutResourcesBasedOnStructureType(final Structure structure, final Resource resource, + final Map> paidOutResources) { + final Faction faction = structure.getFaction(); + final Player playerFromFaction = getPlayerFromFaction(faction); + final int amountPerResource = structure.getAmountPerResource(); + + if (playerFromFaction != null) { + playerFromFaction.increaseInventoryItem(resource, amountPerResource); + bank.decreaseInventoryItemIfApplicable(resource, amountPerResource); + paidOutResources.computeIfAbsent(faction, SiedlerGame::initializeResourceMap) + .merge(resource, amountPerResource, Integer::sum); + } + } + + /** + * Initializes a {@link Map} of {@link Resource}s for a {@link Player}'s {@link Faction}. + * + * @param f the {@link Player}'s {@link Faction} + * @return a Map of {@link Resource}s for the given {@link Faction} + */ + private static Map initializeResourceMap(final Faction f) { + return new EnumMap<>(Resource.class); + } + +} diff --git a/src/main/java/ch/zhaw/catan/structure/City.java b/src/main/java/ch/zhaw/catan/structure/City.java new file mode 100644 index 0000000..36c77a2 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/structure/City.java @@ -0,0 +1,77 @@ +package ch.zhaw.catan.structure; + +import ch.zhaw.catan.game.Config; +import ch.zhaw.catan.game.Config.Faction; + +import static ch.zhaw.catan.game.Config.Structure.CITY; + +/** + * A class representing a city structure in the siedler game. + */ +public class City extends Structure { + + /** + * The number of points that this city provides for its {@link Faction}. + */ + static final int SCORE = 2; + + /** + * The amount of resources that get paid out for this city. + */ + static final int AMOUNT_PER_RESOURCE = 2; + + /** + * A unique String identifier for this city. + */ + static final String IDENTIFIER = "C"; + + /** + * Creates a new city belonging to the specified {@link Faction}. + * + * @param faction the {@link Faction} that this city belongs to. + */ + public City(final Faction faction) { + super(faction); + } + + /** + * Returns the type of this {@link Structure}, which is always `CITY`. + * + * @return the type of this {@link Structure} + */ + @Override + public Config.Structure getStructureType() { + return CITY; + } + + /** + * Returns the number of points that this city provides for its {@link Faction}. + * + * @return the number of points provided by this city + */ + @Override + public int getScore() { + return SCORE; + } + + /** + * Returns the unique identifier for this city. + * + * @return the unique identifier for this city + */ + @Override + public String getIdentifier() { + return IDENTIFIER; + } + + /** + * Returns the number of resources that get paid out for this city. + * + * @return the number of resources that get paid out for this city. + */ + @Override + public int getAmountPerResource() { + return AMOUNT_PER_RESOURCE; + } + +} diff --git a/src/main/java/ch/zhaw/catan/structure/Road.java b/src/main/java/ch/zhaw/catan/structure/Road.java new file mode 100644 index 0000000..91c9533 --- /dev/null +++ b/src/main/java/ch/zhaw/catan/structure/Road.java @@ -0,0 +1,77 @@ +package ch.zhaw.catan.structure; + +import ch.zhaw.catan.game.Config; +import ch.zhaw.catan.game.Config.Faction; + +import static ch.zhaw.catan.game.Config.Structure.ROAD; + +/** + * A class representing a road structure in the siedler game. + */ +public class Road extends Structure { + + /** + * The number of points that this {@link Road} provides for its {@link Faction}. + */ + static final int SCORE = 0; + + /** + * The amount of resources that get paid out for this road. + */ + static final int AMOUNT_PER_RESOURCE = 0; + + /** + * A unique String identifier for this road. + */ + static final String IDENTIFIER = "R"; + + /** + * Creates a new road belonging to the specified {@link Faction}. + * + * @param faction the {@link Faction} that this road belongs to. + */ + public Road(final Faction faction) { + super(faction); + } + + /** + * Returns the type of this {@link Structure}, which is always `ROAD`. + * + * @return the type of this {@link Structure} + */ + @Override + public Config.Structure getStructureType() { + return ROAD; + } + + /** + * Returns the number of points that this road provides for its {@link Faction}. + * + * @return the number of points provided by this road + */ + @Override + public int getScore() { + return SCORE; + } + + /** + * Returns the unique identifier for this road. + * + * @return the unique identifier for this road + */ + @Override + public String getIdentifier() { + return IDENTIFIER; + } + + /** + * Returns the number of resources that get paid out for this road. + * + * @return the number of resources that get paid out for this road. + */ + @Override + public int getAmountPerResource() { + return AMOUNT_PER_RESOURCE; + } + +} diff --git a/src/main/java/ch/zhaw/catan/structure/Settlement.java b/src/main/java/ch/zhaw/catan/structure/Settlement.java new file mode 100644 index 0000000..207957c --- /dev/null +++ b/src/main/java/ch/zhaw/catan/structure/Settlement.java @@ -0,0 +1,77 @@ +package ch.zhaw.catan.structure; + +import ch.zhaw.catan.game.Config; +import ch.zhaw.catan.game.Config.Faction; + +import static ch.zhaw.catan.game.Config.Structure.SETTLEMENT; + +/** + * A class representing a settlement structure in the siedler game. + */ +public class Settlement extends Structure { + + /** + * The number of points that this settlement provides for its {@link Faction}. + */ + static final int SCORE = 1; + + /** + * The amount of resources that get paid out for this settlement. + */ + static final int AMOUNT_PER_RESOURCE = 1; + + /** + * A unique {@link String} identifier for this settlement. + */ + static final String IDENTIFIER = "S"; + + /** + * Creates a new settlement belonging to the specified {@link Faction}. + * + * @param faction the {@link Faction} that this settlement belongs to. + */ + public Settlement(final Faction faction) { + super(faction); + } + + /** + * Returns the type of this {@link Structure}, which is always `SETTLEMENT`. + * + * @return the type of this {@link Structure} + */ + @Override + public Config.Structure getStructureType() { + return SETTLEMENT; + } + + /** + * Returns the number of points that this settlement provides for its {@link Faction}. + * + * @return the number of points provided by this settlement + */ + @Override + public int getScore() { + return SCORE; + } + + /** + * Returns the unique identifier for this settlement. + * + * @return the unique identifier for this settlement + */ + @Override + public String getIdentifier() { + return IDENTIFIER; + } + + /** + * Returns the number of resources that get paid out for this settlement. + * + * @return the number of resources that get paid out for this settlement + */ + @Override + public int getAmountPerResource() { + return AMOUNT_PER_RESOURCE; + } + +} diff --git a/src/main/java/ch/zhaw/catan/structure/Structure.java b/src/main/java/ch/zhaw/catan/structure/Structure.java new file mode 100644 index 0000000..6641cdf --- /dev/null +++ b/src/main/java/ch/zhaw/catan/structure/Structure.java @@ -0,0 +1,75 @@ +package ch.zhaw.catan.structure; + +import ch.zhaw.catan.game.Config; +import ch.zhaw.catan.game.Config.Faction; + +import static java.util.Objects.requireNonNull; + +/** + * A class representing a structure in the siedler game. + */ +public abstract class Structure { + + /** + * The faction that this structure belongs to. + */ + private final Faction faction; + + /** + * Creates a new structure belonging to the specified {@link Faction}. + * + * @param faction the {@link Faction} that this structure belongs to + */ + protected Structure(final Faction faction) { + this.faction = requireNonNull(faction, "faction must not be null"); + } + + /** + * Returns the type of this structure. + * + * @return the type of this structure + */ + public abstract Config.Structure getStructureType(); + + /** + * Returns the score that this structure provides for its {@link Faction}. + * + * @return the score provided by this structure + */ + public abstract int getScore(); + + /** + * Returns a unique identifier for this structure. + * + * @return a unique identifier for this structure + */ + public abstract String getIdentifier(); + + /** + * Returns the number of resources that are required to produce this + * structure. + * + * @return the number of resources required to produce this structure + */ + public abstract int getAmountPerResource(); + + /** + * Returns the {@link Faction} that this structure belongs to. + * + * @return the {@link Faction} that this structure belongs to + */ + public Faction getFaction() { + return faction; + } + + /** + * Returns a string representation of this structure. + * + * @return a string representation of this structure + */ + @Override + public String toString() { + return getFaction().toString().charAt(0) + getIdentifier(); + } + +} diff --git a/src/main/java/ch/zhaw/hexboard/Edge.java b/src/main/java/ch/zhaw/hexboard/Edge.java new file mode 100644 index 0000000..f6b5c1d --- /dev/null +++ b/src/main/java/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 final Point start; + private final 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/main/java/ch/zhaw/hexboard/FieldAnnotationPosition.java b/src/main/java/ch/zhaw/hexboard/FieldAnnotationPosition.java new file mode 100644 index 0000000..7bf5971 --- /dev/null +++ b/src/main/java/ch/zhaw/hexboard/FieldAnnotationPosition.java @@ -0,0 +1,113 @@ +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 final Point field; + private final 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/main/java/ch/zhaw/hexboard/HexBoard.java b/src/main/java/ch/zhaw/hexboard/HexBoard.java new file mode 100644 index 0000000..b712d51 --- /dev/null +++ b/src/main/java/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 exist 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); + return edge.getOrDefault(e, 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/main/java/ch/zhaw/hexboard/HexBoardTextView.java b/src/main/java/ch/zhaw/hexboard/HexBoardTextView.java new file mode 100644 index 0000000..7f54188 --- /dev/null +++ b/src/main/java/ch/zhaw/hexboard/HexBoardTextView.java @@ -0,0 +1,364 @@ +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 final 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/main/java/ch/zhaw/hexboard/Label.java b/src/main/java/ch/zhaw/hexboard/Label.java new file mode 100644 index 0000000..bd61c6c --- /dev/null +++ b/src/main/java/ch/zhaw/hexboard/Label.java @@ -0,0 +1,49 @@ +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/src/test/java/ch/zhaw/catan/ThreePlayerStandard.java b/src/test/java/ch/zhaw/catan/ThreePlayerStandard.java new file mode 100644 index 0000000..706b5d3 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/ThreePlayerStandard.java @@ -0,0 +1,461 @@ +package ch.zhaw.catan; + +import ch.zhaw.catan.game.Config; +import ch.zhaw.catan.game.Config.Resource; +import ch.zhaw.catan.game.SiedlerGame; +import org.junit.jupiter.api.Assertions; + +import java.awt.Point; +import java.util.List; +import java.util.Map; + + +/** + * 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], Map.of(Resource.GRAIN, 1)), + 3, Map.of( + Config.Faction.values()[1], Map.of(Resource.WOOL, 1)), + 4, Map.of( + Config.Faction.values()[0], Map.of(Resource.ORE, 1), + Config.Faction.values()[2], Map.of(Resource.BRICK, 1)), + 5, Map.of( + Config.Faction.values()[2], Map.of(Resource.LUMBER, 1)), + 6, Map.of( + Config.Faction.values()[0], Map.of(Resource.LUMBER, 1)), + 8, Map.of( + Config.Faction.values()[1], Map.of(Resource.WOOL, 1)), + 9, Map.of( + Config.Faction.values()[1], Map.of(Resource.GRAIN, 1)), + 10, Map.of(), + 11, Map.of( + Config.Faction.values()[0], Map.of(Resource.BRICK, 1)), + 12, Map.of( + Config.Faction.values()[0], Map.of(Resource.WOOL, 1), + Config.Faction.values()[1], Map.of(Resource.WOOL, 1)) + ); + + 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(); + Assertions.assertTrue(model.placeInitialSettlement(INITIAL_SETTLEMENT_POSITIONS.get(f).first, false)); + Assertions.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(); + Assertions.assertTrue(model.placeInitialSettlement(INITIAL_SETTLEMENT_POSITIONS.get(f).second, true)); + Assertions.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 < numberOfTimes; i++) { + model.processDiceRoll(diceValue); + } + return model; + } + + /** + *

Returns a siedler game after building four additional roads and two + * settlements after the setup phase with the resource cards and roads + * for player one ready to build a fifth settlement at {@link #PLAYER_ONE_READY_TO_BUILD_FIFTH_SETTLEMENT_FIFTH_SETTLEMENT_POSITION} + *

+ *

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); + Assertions.assertTrue(model.buildSettlement(position)); + } + + private static void buildRoad(SiedlerGame model, List roads) { + for (int i = 0; i < roads.size() - 1; i++) { + Assertions.assertTrue(model.buildRoad(roads.get(i), roads.get(i + 1))); + } + } +} diff --git a/src/test/java/ch/zhaw/catan/Tuple.java b/src/test/java/ch/zhaw/catan/Tuple.java new file mode 100644 index 0000000..6d6d911 --- /dev/null +++ b/src/test/java/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/src/test/java/ch/zhaw/catan/board/FieldTest.java b/src/test/java/ch/zhaw/catan/board/FieldTest.java new file mode 100644 index 0000000..842d8b6 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/board/FieldTest.java @@ -0,0 +1,34 @@ +package ch.zhaw.catan.board; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.awt.Point; + +import static ch.zhaw.catan.board.Field.THIEF_IDENTIFIER; +import static ch.zhaw.catan.game.Config.Land.HILLS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class FieldTest { + + private Field field; + + @BeforeEach + void setup() { + field = new Field(HILLS, new Point(1, 2)); + } + + @Test + void testUnoccupiedByThief() { + assertFalse(field.isOccupiedByThief()); + assertEquals(HILLS.getResource().toString(), field.toString()); + } + + @Test + void testOccupiedByThief() { + field.setOccupiedByThief(true); + assertEquals(THIEF_IDENTIFIER, field.toString()); + } + +} diff --git a/src/test/java/ch/zhaw/catan/board/SiedlerBoardTest.java b/src/test/java/ch/zhaw/catan/board/SiedlerBoardTest.java new file mode 100644 index 0000000..7c87c03 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/board/SiedlerBoardTest.java @@ -0,0 +1,41 @@ +package ch.zhaw.catan.board; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SiedlerBoardTest { + SiedlerBoard siedlerBoard = new SiedlerBoard(); + + @BeforeEach + void SiedlerBoard() { + siedlerBoard = new SiedlerBoard(); + } + + /** + * This test first creates a string representation of the game board and splits it into individual lines, + * then creates the expected first line of the board view and checks that it matches the actual first line. + * It also checks that the last line of the board view has the correct number of characters. + */ + @Test + void getBoardView() { + String[] boardView = siedlerBoard.getView().toString().split(System.lineSeparator()); + String whiteSpace = " "; + + StringBuilder firstLine = new StringBuilder(whiteSpace.repeat(5)); + for (int i = 0; i <= 14; i++) { + if (i < 9) { + firstLine.append(i).append(whiteSpace.repeat(7)); + } else if (i < 14) { + firstLine.append(i).append(whiteSpace.repeat(6)); + } else { + firstLine.append(i); + } + + } + + assertEquals(firstLine.toString(), boardView[1]); + assertEquals(whiteSpace.repeat(131).length(), boardView[42].length()); + } +} \ No newline at end of file diff --git a/src/test/java/ch/zhaw/catan/game/BankTest.java b/src/test/java/ch/zhaw/catan/game/BankTest.java new file mode 100644 index 0000000..b4311c7 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/game/BankTest.java @@ -0,0 +1,96 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Resource; +import ch.zhaw.catan.structure.City; +import ch.zhaw.catan.structure.Settlement; +import ch.zhaw.catan.structure.Structure; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static ch.zhaw.catan.game.Config.Faction.GREEN; +import static ch.zhaw.catan.game.Config.Faction.RED; +import static ch.zhaw.catan.game.Config.Resource.LUMBER; +import static ch.zhaw.catan.game.Config.Resource.ORE; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This class performs tests for the class {@link Bank}. + */ +class BankTest { + + private Bank bank; + private List structures; + + + /** + * Creates a player object before each Test. + */ + @BeforeEach + void init() { + bank = new Bank(); + structures = new ArrayList<>(); + structures.add(new City(GREEN)); + structures.add(new Settlement(RED)); + } + + /** + * Tests if isInventorySufficientForPayoutOfResource returns true if the bank has all resources left. + */ + @Test + void isInventorySufficientForPayoutOfResourceTestWithAllBankResourcesLeft() { + assertTrue(bank.isInventorySufficientForPayoutOfResource(ORE, structures)); + } + + /** + * Tests if isInventorySufficientForPayoutOfResource returns true if the bank has more than enough resources left. + */ + @Test + void isInventorySufficientForPayoutOfResourceTestWithMoreThanEnoughResourcesLeft() { + for (Resource resource : Resource.values()) { + bank.decreaseInventoryItemIfApplicable(resource, 10); + } + + assertTrue(bank.isInventorySufficientForPayoutOfResource(LUMBER, structures)); + } + + /** + * Tests if isInventorySufficientForPayoutOfResource returns true if the bank has just enough resources left. + */ + @Test + void isInventorySufficientForPayoutOfResourceTestWithJustEnoughResourcesLeft() { + for (Resource resource : Resource.values()) { + bank.decreaseInventoryItemIfApplicable(resource, 16); + } + + assertTrue(bank.isInventorySufficientForPayoutOfResource(LUMBER, structures)); + } + + /** + * Tests if isInventorySufficientForPayoutOfResource returns false if the bank has one resource less left. + */ + @Test + void isInventorySufficientForPayoutOfResourceTestWithOneResourceLessLeft() { + for (Resource resource : Resource.values()) { + bank.decreaseInventoryItemIfApplicable(resource, 17); + } + + assertFalse(bank.isInventorySufficientForPayoutOfResource(LUMBER, structures)); + } + + /** + * Tests if isInventorySufficientForPayoutOfResource returns false if the bank has no resource left. + */ + @Test + void isInventorySufficientForPayoutOfResourceTestWithNoResourceLessLeft() { + for (Resource resource : Resource.values()) { + bank.decreaseInventoryItemIfApplicable(resource, 19); + } + + assertFalse(bank.isInventorySufficientForPayoutOfResource(LUMBER, structures)); + } + +} diff --git a/src/test/java/ch/zhaw/catan/game/DiceTest.java b/src/test/java/ch/zhaw/catan/game/DiceTest.java new file mode 100644 index 0000000..233cbed --- /dev/null +++ b/src/test/java/ch/zhaw/catan/game/DiceTest.java @@ -0,0 +1,18 @@ +package ch.zhaw.catan.game; + +import org.junit.jupiter.api.RepeatedTest; + +import static ch.zhaw.catan.game.Dice.UPPER_DICE_LIMIT; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DiceTest { + + private final Dice dice = new Dice(); + + @RepeatedTest(1000) + void testRoll() { + int nextRoll = dice.roll(); + assertTrue(nextRoll >= 1 && nextRoll <= (UPPER_DICE_LIMIT * 2)); + } + +} diff --git a/src/test/java/ch/zhaw/catan/game/PlayerTest.java b/src/test/java/ch/zhaw/catan/game/PlayerTest.java new file mode 100644 index 0000000..f44d497 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/game/PlayerTest.java @@ -0,0 +1,292 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.game.Config.Resource; +import ch.zhaw.catan.structure.City; +import ch.zhaw.catan.structure.Road; +import ch.zhaw.catan.structure.Settlement; +import ch.zhaw.catan.structure.Structure; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static ch.zhaw.catan.game.Config.Faction.BLUE; +import static ch.zhaw.catan.game.Config.Faction.RED; +import static ch.zhaw.catan.game.Config.Faction.YELLOW; +import static ch.zhaw.catan.game.Config.Resource.BRICK; +import static ch.zhaw.catan.game.Config.Resource.LUMBER; +import static ch.zhaw.catan.game.Config.Resource.ORE; +import static ch.zhaw.catan.game.Config.Resource.WOOL; +import static ch.zhaw.catan.game.Config.Structure.CITY; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This class performs tests for the class {@link Player} + */ +class PlayerTest { + + private Player player1; + private Player player2; + + /** + * Creates a {@link Player} object before each Test. + */ + @BeforeEach + public void init() { + player1 = new Player(RED); + player2 = new Player(YELLOW); + } + + + /** + * Tests if the inventory is empty after initialising a player. + */ + @Test + void getInventoryTest() { + final Map inventory = player1.getInventory(); + + for (Resource resource : Resource.values()) { + assertEquals(0, inventory.get(resource)); + } + } + + /** + * Tests if the inventory is empty after initialising a new player. And if the method + * getAmountOfResource works properly. + */ + @Test + void getAmountOfResourceTest() { + assertEquals(0, player1.getAmountOfResource(Resource.WOOL)); + } + + /** + * Tests if the inventory is empty after initialising a new player. And if the method + * getTotalAmountOfResources works properly. + */ + @Test + void getTotalAmountOfResourcesTest() { + assertEquals(0, player1.getTotalAmountOfResources()); + } + + /** + * Tests if the inventory can be increased by a map of resources. + */ + @Test + void increaseInventoryTest() { + final Map toAdd = new HashMap<>(); + + for (Resource resource : Resource.values()) { + toAdd.put(resource, 2); + } + + player1.increaseInventory(toAdd); + assertEquals(10, player1.getTotalAmountOfResources()); + } + + /** + * 1. Tests if a single {@link Resource} can be added to a players inventory. + * 2. Tests if the method doesn't put a negative amount of resource into inventory. + */ + @Test + void increaseInventoryItemTest() { + player1.increaseInventoryItem(Resource.WOOL, 2); + assertEquals(2, player1.getAmountOfResource(Resource.WOOL)); + + player1.increaseInventoryItem(LUMBER, -5); + assertEquals(0, player1.getAmountOfResource(LUMBER)); + } + + /** + * 1. Tests if the inventory can be decreased by a map of resources. + * 2. Tests if the inventory can't be decreased if there aren't enough resources. + */ + @Test + void decreaseInventoryIfApplicableTest() { + final Map toAdd = new HashMap<>(); + + for (Resource resource : Resource.values()) { + toAdd.put(resource, 10); + } + + player1.increaseInventory(toAdd); + + final Map toTake1 = new HashMap<>(); + + for (Resource resource : Resource.values()) { + toTake1.put(resource, 6); + } + + player1.decreaseInventoryIfApplicable(toTake1); + assertEquals(4, player1.getAmountOfResource(WOOL)); + assertEquals(20, player1.getTotalAmountOfResources()); + + final Map toTake2 = new HashMap<>(); + + for (Resource resource : Resource.values()) { + toTake2.put(resource, 6); + } + + player1.decreaseInventoryIfApplicable(toTake2); + assertEquals(4, player1.getAmountOfResource(WOOL)); + assertEquals(20, player1.getTotalAmountOfResources()); + } + + /** + * 1. Tests if the inventory can be decreased by a single item at the time. + * 2. Tests if the inventory isn't decreased if the number of items to be decreased is higher + * than the amount of {@link Resource} in the inventory. + */ + @Test + void decreaseInventoryItemIfApplicableTest() { + player1.increaseInventoryItem(WOOL, 5); + player1.decreaseInventoryItemIfApplicable(WOOL, 3); + assertEquals(2, player1.getAmountOfResource(WOOL)); + + player1.decreaseInventoryItemIfApplicable(WOOL, 5); + assertEquals(2, player1.getAmountOfResource(WOOL)); + } + + /** + * Tests if the method getFaction returns the correct faction. + */ + @Test + void getFactionTest() { + assertEquals(RED, player1.getFaction()); + assertNotEquals(BLUE, player1.getFaction()); + } + + /** + * Tests if the Hashset structures of the player is empty. + */ + @Test + void getStructuresTest() { + assertTrue(player1.getStructures().isEmpty()); + } + + /** + * 1. Tests if structures can be added to a {@link Player}. + * 2. Tests if addStructure ads the correct structures to a {@link Player}. + */ + @Test + void addStructureTest() { + Settlement settlement1 = new Settlement(RED); + Settlement settlement2 = new Settlement(RED); + City city = new City(RED); + Road road = new Road(RED); + player1.addStructure(settlement1); + player1.addStructure(settlement2); + player1.addStructure(city); + player1.addStructure(road); + assertEquals(4, player1.getStructures().size()); + + List addedStructures = new ArrayList<>(); + addedStructures.add(settlement1); + addedStructures.add(settlement2); + addedStructures.add(city); + addedStructures.add(road); + assertEquals(player1.getStructures(), addedStructures); + } + + /** + * Tests if a Structure can be removed from a player. + */ + @Test + void removeStructureTest() { + final Settlement settlement = new Settlement(RED); + + player1.addStructure(new Settlement(RED)); + player1.addStructure(settlement); + assertEquals(2, player1.getStructures().size()); + + player1.removeStructure(settlement); + assertEquals(1, player1.getStructures().size()); + } + + /** + * Tests if the method hasNotReachedMaxedStock returns the correct boolean. + * 1. Tests if the initial return value is false + * 2. Tests that after adding structures the value is still false. + * 3. Tests that after meeting the required MaxStock the function returns true. + */ + @Test + void hasNotReachedMaxedStock() { + assertTrue(player1.hasNotReachedMaxStockOf(CITY)); + + player1.addStructure(new City(RED)); + player1.addStructure(new City(RED)); + player1.addStructure(new City(RED)); + assertTrue(player1.hasNotReachedMaxStockOf(CITY)); + + player1.addStructure(new City(RED)); + assertFalse(player1.hasNotReachedMaxStockOf(CITY)); + } + + /** + * Tests if the method processThief takes away half of the resources. But only when the player + * has more than 7 resources in total. + * 1. Tests if processThief doesn't take away resources if the player has less than 7 resources. + * 2. Tests if processThief does take away half the resources of the player. + * 3. Tests if processThief does take away one less than half the resources of the player, + * if the amount of resources was odd. + * 4. Tests if processThief does take away half the resources of the player if the player has more than 7 resources. + */ + @Test + void processThiefTest() { + player1.increaseInventoryItem(WOOL, 3); + player1.processThief(); + assertEquals(3, player1.getTotalAmountOfResources()); + + player1.increaseInventoryItem(LUMBER, 8); + player1.increaseInventoryItem(ORE, 11); + player1.processThief(); + assertEquals(11, player1.getTotalAmountOfResources()); + + player1.processThief(); + assertEquals(6, player1.getTotalAmountOfResources()); + + player1.increaseInventoryItem(BRICK, 2); + player1.processThief(); + assertEquals(4, player1.getTotalAmountOfResources()); + } + + /** + * Tests if the stealRandomResource steals a random resource. + */ + @Test + void stealRandomResourceTest() { + final Map toAdd = new HashMap<>(); + + for (Resource resource : Resource.values()) { + toAdd.put(resource, 10); + } + + player1.increaseInventory(toAdd); + player2.increaseInventory(toAdd); + player1.stealRandomResourceFrom(singletonList(player2)); + assertEquals(51, player1.getTotalAmountOfResources()); + assertEquals(49, player2.getTotalAmountOfResources()); + } + + /** + * Tests if the getScore method works properly. + * 1. Tests if the score is 0 if the player has no structures. + * 2. Tests if the score is correct if the player has a certain amount of structures. + */ + @Test + void getScoreTest() { + assertEquals(0, player1.getScore()); + + player1.addStructure(new Settlement(RED)); + player1.addStructure(new Settlement(RED)); + player1.addStructure(new City(RED)); + player1.addStructure(new Road(RED)); + assertEquals(4, player1.getScore()); + } +} diff --git a/src/test/java/ch/zhaw/catan/game/SiedlerGameTest.java b/src/test/java/ch/zhaw/catan/game/SiedlerGameTest.java new file mode 100644 index 0000000..04b4cf3 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/game/SiedlerGameTest.java @@ -0,0 +1,568 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.ThreePlayerStandard; +import ch.zhaw.catan.board.SiedlerBoard; +import ch.zhaw.catan.structure.City; +import ch.zhaw.catan.structure.Road; +import ch.zhaw.catan.structure.Settlement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.awt.Point; +import java.util.Collections; +import java.util.List; + +import static ch.zhaw.catan.game.App.REQUIRED_WINNING_SCORE; +import static ch.zhaw.catan.game.Config.INITIAL_RESOURCE_CARDS_BANK; +import static ch.zhaw.catan.game.SiedlerGame.THIEF_DICE_NUMBER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/*** + * The SiedlerGameTest class is a JUnit test class for the {@link SiedlerGame} class. + * It contains several test methods to test various aspects of the SiedlerGame class. + */ +class SiedlerGameTest { + private final static List validSettlementPoints = List.of(new Point(5, 9), new Point(6, 12), new Point(6, 6), new Point(4, 12)); + private final static List invalidSettlementPoints = List.of(new Point(0, 0), new Point(3, 3), new Point(14, 12), new Point(5, 5)); + private final static Point firstField = new Point(6, 8); + private final static Point emptyField = new Point(11, 11); + private final static Point waterField = new Point(4, 2); + private final static Point validRoadStartPoint = new Point(6, 10); + private final static List validRoadEndPoints = List.of(new Point(5, 9), new Point(6, 12), new Point(7, 9)); + private final static List invalidRoadEndPoints = List.of(new Point(9, 15), new Point(9, 9), new Point(0, 0)); + + private final static List waterEdge = List.of(new Point(3, 1), new Point(3, 3)); + + private final static int DEFAULT_NUMBER_OF_PLAYERS = 2; + private SiedlerGame siedlerGame; + + private static int getInitialBankInventory() { + int initialBankInventory = 0; + for (Config.Resource resource : Config.Resource.values()) { + initialBankInventory += INITIAL_RESOURCE_CARDS_BANK.get(resource); + } + return initialBankInventory; + } + + @BeforeEach + void init() { + siedlerGame = new SiedlerGame(REQUIRED_WINNING_SCORE, DEFAULT_NUMBER_OF_PLAYERS); + } + + /** + * This is a test method for the switchToNextPlayer() and switchToPreviousPlayer() methods of the SiedlerGame class. + * It checks if the methods correctly switch between the current Players of the game. + * It also checks that calling the switchToNextPlayer() and switchToPreviousPlayer() method repeatedly + * eventually returns to the first player in the list. + *

+ * Additionally, the test creates a SiedlerGame instance with four players + * and tests that the methods correctly switch between all four players. + */ + @Test + void switchMultiplePlayer() { + assertEquals(siedlerGame.getPlayerFactions().get(0), siedlerGame.getCurrentPlayerFaction()); + siedlerGame.switchToNextPlayer(); + assertEquals(siedlerGame.getPlayerFactions().get(1), siedlerGame.getCurrentPlayerFaction()); + siedlerGame.switchToNextPlayer(); + assertEquals(siedlerGame.getPlayerFactions().get(0), siedlerGame.getCurrentPlayerFaction()); + siedlerGame.switchToPreviousPlayer(); + siedlerGame.switchToPreviousPlayer(); + assertEquals(siedlerGame.getPlayerFactions().get(0), siedlerGame.getCurrentPlayerFaction()); + + SiedlerGame siedlerGameFourPlayers = new SiedlerGame(REQUIRED_WINNING_SCORE, 4); + + assertEquals(siedlerGameFourPlayers.getPlayerFactions().get(0), siedlerGameFourPlayers.getCurrentPlayerFaction()); + siedlerGameFourPlayers.switchToNextPlayer(); + siedlerGameFourPlayers.switchToNextPlayer(); + siedlerGameFourPlayers.switchToNextPlayer(); + siedlerGameFourPlayers.switchToNextPlayer(); + assertEquals(siedlerGameFourPlayers.getPlayerFactions().get(0), siedlerGameFourPlayers.getCurrentPlayerFaction()); + siedlerGameFourPlayers.switchToPreviousPlayer(); + assertEquals(siedlerGameFourPlayers.getPlayerFactions().get(3), siedlerGameFourPlayers.getCurrentPlayerFaction()); + } + + /** + * This is a test method for the getPlayerFactions() method of the SiedlerGame class. + * It first calls the getPlayerFactions() method to retrieve the factions of the players in the game, + * then checks that the returned list is in the correct order and contains the expected factions. + * It also checks that the function returns an empty list when there are no players in the game. + */ + @Test + void getPlayerFactions() { + List playerFactions = siedlerGame.getPlayerFactions(); + + Player player1 = siedlerGame.getCurrentPlayer(); + siedlerGame.switchToNextPlayer(); + Player player2 = siedlerGame.getCurrentPlayer(); + + assertEquals(2, playerFactions.size()); + assertEquals(player1.getFaction(), playerFactions.get(0)); + assertEquals(player2.getFaction(), playerFactions.get(1)); + } + + /** + * This method tests that the getPlayerFactions() method of the SiedlerGame class + * throws an {@link IndexOutOfBoundsException}, when an attempt is made to retrieve a non-existent Player in the list + * returned by getPlayerFactions(). + */ + @Test + void getPlayerFactionsOutOfBounds() { + List playerFactions = siedlerGame.getPlayerFactions(); + + assertThrows(IndexOutOfBoundsException.class, () -> playerFactions.get(2)); + } + + /** + * This method tests that the getPlayerFactions() method of the SiedlerGame class + * returns an empty list when the SiedlerGame object is constructed with zero players. + */ + @Test + void getPlayerFactionsEmptyList() { + assertEquals(Collections.EMPTY_LIST, new SiedlerGame(REQUIRED_WINNING_SCORE, 0).getPlayerFactions()); + } + + /** + * This test first creates a new game and compares the fields and corners of the game boards, + * then checks that the edges of the game boards are not equal. + * It also checks that the getEdge function correctly retrieves the edge between two points on the game board. + */ + @Test + void getBoard() { + SiedlerGame game = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + + assertEquals(game.getBoard().getFields(), siedlerGame.getBoard().getFields()); + assertNotEquals(game.getBoard().getCorners(), siedlerGame.getBoard().getCorners()); + assertNotEquals(game.getBoard().getEdge(new Point(5, 7), new Point(6, 6)), siedlerGame.getBoard().getEdge(new Point(5, 7), new Point(6, 6))); + } + + /** + * This method tests that the getEdge() method of the Board class throws an {@link IllegalArgumentException} + * when it is called with the same coordinate twice or otherwise invalid edge coordinates. + */ + @Test + void getBoardThrows() { + SiedlerBoard board = siedlerGame.getBoard(); + Point firstValidSettlementPoint = validSettlementPoints.get(0); + Point firstInvalidRoadEndPoints = invalidRoadEndPoints.get(0); + + + assertThrows(IllegalArgumentException.class, () -> board.getEdge(firstValidSettlementPoint, firstValidSettlementPoint), "Coordinates " + validSettlementPoints.get(0) + " and " + validSettlementPoints.get(0) + " are not coordinates of an edge."); + assertThrows(IllegalArgumentException.class, () -> board.getEdge(validRoadStartPoint, firstInvalidRoadEndPoints), "Coordinates " + validRoadStartPoint + " and " + invalidRoadEndPoints.get(0) + " are not coordinates of an edge."); + } + + /** + * This test first checks that the function returns 0 when the current player has no resources of the specified type, + * then adds resources to the player's inventory and checks that the function returns the correct number of resources. + */ + @Test + void getCurrentPlayerResourceStock() { + assertEquals(0, siedlerGame.getCurrentPlayerResourceStock(Config.Resource.WOOL)); + + siedlerGame.getCurrentPlayer().increaseInventoryItem(Config.Resource.WOOL, 5); + assertEquals(5, siedlerGame.getCurrentPlayerResourceStock(Config.Resource.WOOL)); + } + + /** + * Testing for placement of initial settlements and payout. + *

+ * 1. Place invalid settlements + * 2. Place settlements on valid positions + * 3. Try to place settlements again + * 4. Check player Inventory after no payout + * 5. Compare player inventory with Player that got a payout + * 6. Check inventory of Player that got payout + */ + @Test + void placeInitialSettlement() { + // Trying to place settlements on invalid positions + for (Point invalidSettlementPoint : invalidSettlementPoints) { + assertFalse(siedlerGame.placeInitialSettlement(invalidSettlementPoint, false)); + } + + // place settlements on valid positions + for (Point validSettlementPoint : validSettlementPoints) { + assertTrue(siedlerGame.placeInitialSettlement(validSettlementPoint, false)); + } + + // Trying to place settlements on same positions again. + for (Point validSettlementPoint : validSettlementPoints) { + assertFalse(siedlerGame.placeInitialSettlement(validSettlementPoint, false)); + } + + // Trying to place settlements next to previously placed settlement + assertFalse(siedlerGame.placeInitialSettlement(validRoadEndPoints.get(0), false)); + assertFalse(siedlerGame.placeInitialSettlement(validRoadEndPoints.get(1), false)); + + assertFalse(siedlerGame.placeInitialSettlement(validRoadStartPoint, false)); + + Player playerBeforePlacement = siedlerGame.getCurrentPlayer(); + + assertEquals(0, playerBeforePlacement.getTotalAmountOfResources()); + + SiedlerGame siedlerGame2 = new SiedlerGame(REQUIRED_WINNING_SCORE, 2); + + // place settlements on valid positions and payout resources + for (Point validSettlementPoint : validSettlementPoints) { + assertTrue(siedlerGame2.placeInitialSettlement(validSettlementPoint, true)); + } + + assertNotEquals(playerBeforePlacement.getTotalAmountOfResources(), siedlerGame2.getCurrentPlayer().getTotalAmountOfResources()); + assertEquals(11, siedlerGame2.getCurrentPlayer().getTotalAmountOfResources()); + } + + /** + * This test first attempts to place a road without any settlements, + * then creates settlements and attempts to place roads at valid locations. + * It also attempts to place roads at invalid locations and from a different player's perspective. + */ + @Test + void placeInitialRoad() { + // No settlements exist + assertFalse(siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(0))); + assertFalse(siedlerGame.placeInitialRoad(waterEdge.get(0), waterEdge.get(1))); + + // Create Settlements + for (Point settlementPoint : validSettlementPoints) { + siedlerGame.placeInitialSettlement(settlementPoint, false); + } + + // Place roads at valid Points + assertTrue(siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(0))); + assertTrue(siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(1))); + + //Place invalid roads + assertFalse(siedlerGame.placeInitialRoad(validRoadStartPoint, invalidRoadEndPoints.get(0))); + assertFalse(siedlerGame.placeInitialRoad(invalidSettlementPoints.get(0), validRoadEndPoints.get(0))); + + //Place road of different Player + siedlerGame.switchToNextPlayer(); + assertFalse(siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(2))); + } + + /** + * This method tests the buildSettlement() method of the SiedlerGame class. The test first adds some resources to + * the current player's inventory, and then attempts to build a settlement at an invalid location. + * It then places an initial settlement and connects it to two roads, and then attempts to build another settlement + * at an invalid location and a valid location. It uses the assertFalse() and assertTrue() methods to verify that + * the buildSettlement() method returns false when it is called with an invalid location,and true when it is called + * with a valid location. + */ + @Test + void buildSettlement() { + // Adding non-critical Resources + siedlerGame.getCurrentPlayer().increaseInventory(Config.Structure.SETTLEMENT.getCostsAsIntegerMap()); + siedlerGame.getCurrentPlayer().increaseInventory(Config.Structure.SETTLEMENT.getCostsAsIntegerMap()); + + // No Settlement / Roads exist yet to build a settlement + assertFalse(siedlerGame.buildSettlement(invalidSettlementPoints.get(0))); + assertFalse(siedlerGame.buildSettlement(validSettlementPoints.get(0))); + + // Place Settlement with connecting Roads to new settlement position + siedlerGame.placeInitialSettlement(validSettlementPoints.get(0), false); + siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(0)); + siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(1)); + + // No resources are available to build Settlement + assertFalse(siedlerGame.buildSettlement(invalidSettlementPoints.get(0))); + assertTrue(siedlerGame.buildSettlement(validSettlementPoints.get(1))); + } + + /** + * This method tests the buildSettlement() method of the SiedlerGame class in the case where the player does not + * have enough resources to build a settlement. The test first adds some resources to the current player's inventory, + * but not enough to build a settlement. It then attempts to build a settlement at an invalid location and a valid + * location, and uses the assertFalse() method to verify that the buildSettlement() method returns false in both + * cases. This test ensures that the buildSettlement() method correctly checks the player's inventory to make sure + * that they have enough resources to build a settlement, and returns the correct value if they do not. + */ + @Test + void buildSettlementWithInsufficientResources() { + // Adding non-critical Resources + siedlerGame.getCurrentPlayer().increaseInventory(Config.Structure.CITY.getCostsAsIntegerMap()); + siedlerGame.getCurrentPlayer().increaseInventory(Config.Structure.CITY.getCostsAsIntegerMap()); + + // No Settlement / Roads exist yet to build a settlement + assertFalse(siedlerGame.buildSettlement(invalidSettlementPoints.get(0))); + assertFalse(siedlerGame.buildSettlement(validSettlementPoints.get(0))); + + // Place Settlement with connecting Roads to new settlement position + siedlerGame.placeInitialSettlement(validSettlementPoints.get(0), false); + siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(0)); + siedlerGame.placeInitialRoad(validRoadStartPoint, validRoadEndPoints.get(1)); + + // No resources are available to build Settlement + assertFalse(siedlerGame.buildSettlement(invalidSettlementPoints.get(0))); + assertFalse(siedlerGame.buildSettlement(validSettlementPoints.get(1))); + } + + /** + * This test first attempts to build a city without sufficient resources or at an invalid location, + * then adds resources to the player's inventory and attempts to build a city again at both valid and invalid locations. + * Finally, it builds a city at a valid location and checks that the function returns true to indicate success. + */ + @Test + void buildCity() { + siedlerGame.placeInitialSettlement(validSettlementPoints.get(0), false); + + // Try to build City without resources + assertFalse(siedlerGame.buildCity(validSettlementPoints.get(0))); + + // Try to build City without resources at wrong position + assertFalse(siedlerGame.buildCity(validSettlementPoints.get(1))); + assertFalse(siedlerGame.buildCity(invalidSettlementPoints.get(0))); + + siedlerGame.getCurrentPlayer().increaseInventory(Config.Structure.CITY.getCostsAsIntegerMap()); + + // Try to build City with resources at wrong position + assertFalse(siedlerGame.buildCity(validSettlementPoints.get(1))); + assertFalse(siedlerGame.buildCity(invalidSettlementPoints.get(0))); + // Build city at correct position + assertTrue(siedlerGame.buildCity(validSettlementPoints.get(0))); + } + + /** + * This test first checks that a road cannot be built without sufficient resources or settlements, + * then adds resources to the player's inventory + * and attempts to build roads from a valid starting point to both valid and invalid endpoints. + */ + @Test + void buildRoad() { + Player player = siedlerGame.getCurrentPlayer(); + // Player doesn't have enough resources && no settlements exist + assertFalse(siedlerGame.buildRoad(validRoadStartPoint, validRoadEndPoints.get(0))); + + buildInitialSettlements(); + + // Player doesn't have enough resources but settlements exist + assertFalse(siedlerGame.buildRoad(validRoadStartPoint, validRoadEndPoints.get(0))); + player.increaseInventoryItem(Config.Resource.LUMBER, 2); + player.increaseInventoryItem(Config.Resource.BRICK, 2); + // Player has enough Resources for Road placement + assertFalse(siedlerGame.buildRoad(validRoadStartPoint, invalidRoadEndPoints.get(0))); + assertTrue(siedlerGame.buildRoad(validRoadStartPoint, validRoadEndPoints.get(0))); + assertFalse(siedlerGame.buildRoad(validRoadStartPoint, invalidRoadEndPoints.get(1))); + assertTrue(siedlerGame.buildRoad(validRoadStartPoint, validRoadEndPoints.get(1))); + } + + /** + * This test first asserts that trading with no resources will fail, + * then adds resources to the player's inventory and trades them for other resources from the bank + * until the bank runs out of the desired resource. + * Finally, it checks that the bank's and player's inventories have been updated as expected. + */ + @Test + void tradeWithBankFourToOne() { + // Try trading with no resources + assertFalse(siedlerGame.tradeWithBankFourToOne(Config.Resource.WOOL, Config.Resource.LUMBER)); + + siedlerGame.getCurrentPlayer().increaseInventoryItem(Config.Resource.WOOL, INITIAL_RESOURCE_CARDS_BANK.get(Config.Resource.WOOL) * 4); + + // Trade for all Bank resources + while (siedlerGame.getBank().getAmountOfResource(Config.Resource.LUMBER) > 0) { + assertTrue(siedlerGame.tradeWithBankFourToOne(Config.Resource.WOOL, Config.Resource.LUMBER)); + } + + // Bank has no Lumber but all Wool traded. And player has all Lumber but no Wool. + assertFalse(siedlerGame.tradeWithBankFourToOne(Config.Resource.WOOL, Config.Resource.LUMBER)); + assertEquals(0, siedlerGame.getBank().getAmountOfResource(Config.Resource.LUMBER)); + assertEquals(INITIAL_RESOURCE_CARDS_BANK.get(Config.Resource.WOOL) * 5, siedlerGame.getBank().getAmountOfResource(Config.Resource.WOOL)); + assertEquals(0, siedlerGame.getCurrentPlayer().getAmountOfResource(Config.Resource.WOOL)); + assertEquals(INITIAL_RESOURCE_CARDS_BANK.get(Config.Resource.LUMBER), siedlerGame.getCurrentPlayer().getAmountOfResource(Config.Resource.LUMBER)); + } + + /** + * This is a JUnit test for the getWinner method. + * It first asserts that getWinner returns null before any structures have been built, + * indicating that no winner has been determined yet. + *

+ * Next, it builds three cities and three roads for the current player, which should not be enough to win the game. + * It then asserts that getWinner still returns null. + *

+ * Finally, it builds one more settlement for the current player, which should trigger the player to win the game. + * It then asserts that getWinner returns the Faction of the current player, indicating that they have won the game. + */ + @Test + void getWinner() { + assertNull(siedlerGame.getWinner()); + + for (int i = 0; i < 3; i++) { + siedlerGame.getCurrentPlayer().addStructure(new City(siedlerGame.getCurrentPlayerFaction())); + siedlerGame.getCurrentPlayer().addStructure(new Road(siedlerGame.getCurrentPlayerFaction())); + } + + assertNull(siedlerGame.getWinner()); + + siedlerGame.getCurrentPlayer().addStructure(new Settlement(siedlerGame.getCurrentPlayerFaction())); + + assertEquals(siedlerGame.getCurrentPlayerFaction(), siedlerGame.getWinner()); + + + } + + /** + * This test places the initial settlements for both players on the map. + * It then calculates the initial inventory of the bank by iterating over the resources and summing the initial resource cards in the bank. + * It then asserts that the sum of the resources in the bank and the resources of the two players is equal to the initial bank inventory. + *

+ * Next, it rolls the dice four times, which should distribute resources to the players based on the numbers rolled and the placement of their settlements on the map. + * It then checks that the players received the expected number of resources. + *

+ * Finally, it asserts that the sum of the resources in the bank and the resources of the two players is still equal to the initial bank inventory. + */ + @Test + void processPayoutResource() { + buildInitialSettlements(); + + int initialBankInventory = getInitialBankInventory(); + + assertEquals(initialBankInventory, siedlerGame.getBank().getTotalAmountOfResources() + + siedlerGame.getCurrentPlayers().get(0).getTotalAmountOfResources() + + siedlerGame.getCurrentPlayers().get(1).getTotalAmountOfResources()); + + // Check starting values for Settlement placement + // Player 1 = 3 Resources + // Player 2 = 3 Resources + + siedlerGame.processDiceRoll(4); + siedlerGame.processDiceRoll(9); + siedlerGame.processDiceRoll(11); + siedlerGame.processDiceRoll(11); + + // Check if resources were paid out according to map Placement + assertEquals(8, siedlerGame.getCurrentPlayers().get(0).getTotalAmountOfResources()); + assertEquals(5, siedlerGame.getCurrentPlayers().get(1).getTotalAmountOfResources()); + + assertEquals(initialBankInventory, siedlerGame.getBank().getTotalAmountOfResources() + + siedlerGame.getCurrentPlayers().get(0).getTotalAmountOfResources() + + siedlerGame.getCurrentPlayers().get(1).getTotalAmountOfResources()); + } + + /** + * This method tests that the processPayoutResources() method of the SiedlerGame class correctly handles the case + * where there are not enough resources in the bank to pay out the resources to the players. + * The test first creates a SiedlerGame object with three players, and then rolls the dice until the bank has only one wool resource left. + * It then rolls the dice again, which should trigger a payout of wool resources to the players. + * Finally, it uses the assertEquals() method to verify that the total number of resources in the bank and the players remains the same as it was before the last dice roll. + */ + @Test + void processPayoutResourcesNotEnoughResources() { + SiedlerGame threePlayerGame = ThreePlayerStandard.getAfterSetupPhaseSomeRoads(REQUIRED_WINNING_SCORE); + int initialBankInventory = getInitialBankInventory(); + + // Reduce Bank Resource Wool until only one is left. + for (int i = 0; i < 7; i++) { + threePlayerGame.processDiceRoll(12); + } + + Integer bankInventory = threePlayerGame.getBank().getTotalAmountOfResources(); + + // Roll Wool one more time + threePlayerGame.processDiceRoll(12); + + assertEquals(bankInventory, threePlayerGame.getBank().getTotalAmountOfResources()); + assertEquals(initialBankInventory, threePlayerGame.getBank().getTotalAmountOfResources() + + threePlayerGame.getCurrentPlayers().get(0).getTotalAmountOfResources() + + threePlayerGame.getCurrentPlayers().get(1).getTotalAmountOfResources() + + threePlayerGame.getCurrentPlayers().get(2).getTotalAmountOfResources()); + } + + /** + * This is a test method for the processDiceRoll() method when the thief is rolled of the SiedlerGame class. + * It first creates two players and builds their initial settlements using the buildInitialSettlements() method. + * Then it rolls the dice several times to simulate the game being played and resources being paid out, + * and finally it rolls the number for the executeThief() method + * which checks that the thief is executed and that the players' inventories are correctly modified as a result. + * It also checks that the total amount of resources in the game remains the same after the thief is executed. + */ + @Test + void processThiefRolled() { + buildInitialSettlements(); + + siedlerGame.processDiceRoll(4); + siedlerGame.processDiceRoll(9); + siedlerGame.processDiceRoll(11); + siedlerGame.processDiceRoll(11); + + int initialBankInventory = getInitialBankInventory(); + + // Check if thief is executed and cuts players inventory with more than 7 + // Resources in half but doesn't touch the other players resource + assertTrue(siedlerGame.processDiceRoll(THIEF_DICE_NUMBER).isThiefExecuted()); + assertEquals(4, siedlerGame.getCurrentPlayers().get(0).getTotalAmountOfResources()); + assertEquals(5, siedlerGame.getCurrentPlayers().get(1).getTotalAmountOfResources()); + + assertEquals(initialBankInventory, siedlerGame.getBank().getTotalAmountOfResources() + + siedlerGame.getCurrentPlayers().get(0).getTotalAmountOfResources() + + siedlerGame.getCurrentPlayers().get(1).getTotalAmountOfResources()); + } + + /** + * Testing the placeThiefAndStealCard Method + *

+ * The test creates the initial settlements on the board and checks whether the first field is occupied by a thief. + * It then attempts to place the thief on the water field and on the empty field, + * and checks whether these attempts were successful. + * Finally, the test places the thief on the first field, checks whether it is occupied by the thief, + * and checks the total amount of resources for the current player and the next player. + */ + @Test + void placeThiefAndStealCard() { + buildInitialSettlements(); + + assertFalse(siedlerGame.getBoard().getField(firstField).isOccupiedByThief()); + + assertFalse(siedlerGame.placeThiefAndStealCard(waterField)); + assertTrue(siedlerGame.placeThiefAndStealCard(emptyField)); + siedlerGame.placeThiefAndStealCard(firstField); + + assertTrue(siedlerGame.getBoard().getField(firstField).isOccupiedByThief()); + assertEquals(4, siedlerGame.getCurrentPlayer().getTotalAmountOfResources()); + assertEquals(2, siedlerGame.getCurrentPlayers().get(1).getTotalAmountOfResources()); + } + + /** + * This test first checks that the number of players returned by the function matches the number of players in the game, + * then checks that the factions of the players in the game and the manually generated list of players match. + */ + @Test + void getCurrentPlayers() { + assertEquals(siedlerGame.getPlayerFactions().size(), siedlerGame.getCurrentPlayers().size()); + assertEquals(siedlerGame.getPlayerFactions().get(0), siedlerGame.getCurrentPlayers().get(0).getFaction()); + } + + /** + * This test first checks that the current player's faction matches the faction of the first player in the list of players in the game, + * then checks that the player's faction matches the returned value of the getCurrentPlayerFaction function. + */ + @Test + void getCurrentPlayer() { + assertEquals(siedlerGame.getPlayerFactions().get(0), siedlerGame.getCurrentPlayer().getFaction()); + assertEquals(siedlerGame.getCurrentPlayer().getFaction(), siedlerGame.getCurrentPlayerFaction()); + } + + /** + * This test checks if the inventory of the bank returned by the getBank() method + * matches the expected INITIAL_RESOURCE_CARDS_BANK value. + */ + @Test + void getBank() { + assertEquals(INITIAL_RESOURCE_CARDS_BANK, siedlerGame.getBank().getInventory()); + } + + private void buildInitialSettlements() { + siedlerGame.placeInitialSettlement(validSettlementPoints.get(0), true); + siedlerGame.placeInitialSettlement(validSettlementPoints.get(1), false); + siedlerGame.switchToNextPlayer(); + + siedlerGame.placeInitialSettlement(validSettlementPoints.get(2), true); + siedlerGame.placeInitialSettlement(validSettlementPoints.get(3), false); + siedlerGame.switchToNextPlayer(); + } + +} diff --git a/src/test/java/ch/zhaw/catan/game/SiedlerGameTestBasic.java b/src/test/java/ch/zhaw/catan/game/SiedlerGameTestBasic.java new file mode 100644 index 0000000..9797380 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/game/SiedlerGameTestBasic.java @@ -0,0 +1,304 @@ +package ch.zhaw.catan.game; + +import ch.zhaw.catan.ThreePlayerStandard; +import ch.zhaw.catan.board.SiedlerBoardTextView; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.awt.Point; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static ch.zhaw.catan.game.App.REQUIRED_WINNING_SCORE; + +/** + * This class contains some basic tests for the {@link SiedlerGame} class + *

+ *

+ *

+ * DO NOT MODIFY THIS CLASS + *

+ * + * @author tebe + */ +class SiedlerGameTestBasic { + 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}) + void requirementPlayerSwitching(int numberOfPlayers) { + SiedlerGame model = new SiedlerGame(REQUIRED_WINNING_SCORE, numberOfPlayers); + Assertions.assertEquals(numberOfPlayers, model.getPlayerFactions().size(), + "Wrong number of players returned by getPlayers()"); + // Switching forward + for (int i = 0; i < numberOfPlayers; i++) { + Assertions.assertEquals(Config.Faction.values()[i], model.getCurrentPlayerFaction(), + "Player order does not match order of Faction.values()"); + model.switchToNextPlayer(); + } + Assertions.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(); + Assertions.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 + void requirementLandPlacementTest() { + SiedlerGame model = new SiedlerGame(REQUIRED_WINNING_SCORE, DEFAULT_NUMBER_OF_PLAYERS); + Assertions.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()) { + Assertions.assertEquals(e.getValue(), model.getBoard().getField(e.getKey()).getLand(), + "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 + void requirementSettlementAndRoadPositionsOccupiedThreePlayerStandard() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + Assertions.assertEquals(DEFAULT_NUMBER_OF_PLAYERS, model.getPlayerFactions().size()); + for (Config.Faction f : model.getPlayerFactions()) { + Assertions.assertTrue( + model.getBoard().getCorner(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).first) != null); + Assertions.assertTrue( + model.getBoard().getCorner(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).second) != null); + Assertions + .assertTrue(model.getBoard().getEdge(ThreePlayerStandard.INITIAL_SETTLEMENT_POSITIONS.get(f).first, + ThreePlayerStandard.INITIAL_ROAD_ENDPOINTS.get(f).first) != null); + Assertions + .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#processDiceRoll(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}) + void requirementDiceThrowResourcePayoutThreePlayerStandardTest(int diceValue) { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + Map> expected = ThreePlayerStandard.INITIAL_DICE_THROW_PAYOUT.get(diceValue); + Map> actual = model.processDiceRoll(diceValue).getAffectedResources(); + Assertions.assertEquals(expected, actual); + } + + /** + * Tests whether the resource card stock of the players matches the expected + * stock + * for the game state {@link ThreePlayerStandard#getAfterSetupPhase(int)}}. + */ + @Test + void requirementPlayerResourceCardStockAfterSetupPhase() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + 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 + void requirementPlayerResourceCardStockAfterSetupPhaseAlmostEmptyBank() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(REQUIRED_WINNING_SCORE); + 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 + void requirementPlayerResourceCardStockPlayerOneReadyToBuildFifthSettlement() { + SiedlerGame model = ThreePlayerStandard.getPlayerOneReadyToBuildFifthSettlement(REQUIRED_WINNING_SCORE); + 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 + void requirementDiceThrowPlayerResourceCardStockUpdateTest() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + for (int i : List.of(2, 3, 4, 5, 6, 8, 9, 10, 11, 12)) { + model.processDiceRoll(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()) { + Assertions.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 + void requirementBuildRoad() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(REQUIRED_WINNING_SCORE); + Assertions.assertTrue(model.buildRoad(new Point(6, 6), new Point(6, 4))); + Assertions.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 + void requirementBuildSettlement() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(REQUIRED_WINNING_SCORE); + Assertions.assertTrue(model.buildRoad(new Point(9, 15), new Point(9, 13))); + Assertions.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 + void requirementTwoSettlementsSamePlayerSameFieldResourceCardPayout() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + for (int diceValue : List.of(2, 6, 6, 11)) { + model.processDiceRoll(diceValue); + } + Assertions.assertTrue(model.buildRoad(new Point(6, 6), new Point(7, 7))); + Assertions.assertTrue(model.buildSettlement(new Point(7, 7))); + Assertions.assertEquals(2, model.processDiceRoll(4).getAffectedResources().get(model.getCurrentPlayerFaction()) + .get(Config.Resource.ORE)); + } + + /** + * Tests whether player one can build a city starting in game state + * {@link ThreePlayerStandard#getAfterSetupPhaseAlmostEmptyBank(int)}. + */ + @Test + void requirementBuildCity() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(REQUIRED_WINNING_SCORE); + Assertions.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 + void requirementCanTradeFourToOneWithBank() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhaseAlmostEmptyBank(REQUIRED_WINNING_SCORE); + model.switchToNextPlayer(); + + Map expectedResourceCards = ThreePlayerStandard.BANK_ALMOST_EMPTY_RESOURCE_CARD_STOCK + .get(model.getCurrentPlayerFaction()); + Assertions.assertEquals(expectedResourceCards.get(Config.Resource.WOOL), + model.getCurrentPlayerResourceStock(Config.Resource.WOOL)); + Assertions.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; + Assertions.assertEquals(expectedResourceCards.get(Config.Resource.WOOL) - cardsOffered, + model.getCurrentPlayerResourceStock(Config.Resource.WOOL)); + Assertions.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. + */ + @Test + void print() { + SiedlerGame model = ThreePlayerStandard.getAfterSetupPhase(REQUIRED_WINNING_SCORE); + 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())); + } + + private Map> convertToMap( + Map> factionListMap) { + final Map> resultMap = new HashMap<>(); + for (Map.Entry> factionListEntry : factionListMap.entrySet()) { + resultMap.put(factionListEntry.getKey(), getAmountsPerResource(factionListEntry.getValue())); + } + return resultMap; + } + + private Map getAmountsPerResource(final List resources) { + final Map amountsPerResource = new HashMap<>(); + for (Config.Resource resource : resources) { + amountsPerResource.merge(resource, 1, Integer::sum); + } + return amountsPerResource; + } + +} diff --git a/src/test/java/ch/zhaw/catan/structure/CityTest.java b/src/test/java/ch/zhaw/catan/structure/CityTest.java new file mode 100644 index 0000000..fcfbe81 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/structure/CityTest.java @@ -0,0 +1,69 @@ +package ch.zhaw.catan.structure; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static ch.zhaw.catan.game.Config.Faction.YELLOW; +import static ch.zhaw.catan.game.Config.Structure.CITY; +import static ch.zhaw.catan.structure.City.AMOUNT_PER_RESOURCE; +import static ch.zhaw.catan.structure.City.IDENTIFIER; +import static ch.zhaw.catan.structure.City.SCORE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for the {@link City} class. + * + */ +class CityTest { + + private City city; + + /** + * Sets up the test fixture by creating a new City object. + */ + @BeforeEach + void setup() { + city = new City(YELLOW); + } + + /** + * Tests the {@link City#getStructureType()} method. + */ + @Test + void testGetStructureType() { + assertEquals(CITY, city.getStructureType()); + } + + /** + * Tests the {@link City#getScore()} method. + */ + @Test + void testGetScore() { + assertEquals(SCORE, city.getScore()); + } + + /** + * Tests the {@link City#getIdentifier()} method. + */ + @Test + void testGetIdentifier() { + assertEquals(IDENTIFIER, city.getIdentifier()); + } + + /** + * Tests the {@link City#getAmountPerResource()} method. + */ + @Test + void testGetAmountPerResource() { + assertEquals(AMOUNT_PER_RESOURCE, city.getAmountPerResource()); + } + + /** + * Tests the {@link City#toString()} method. + */ + @Test + void testToString() { + assertEquals("y" + IDENTIFIER, city.toString()); + } + +} diff --git a/src/test/java/ch/zhaw/catan/structure/RoadTest.java b/src/test/java/ch/zhaw/catan/structure/RoadTest.java new file mode 100644 index 0000000..ac80e33 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/structure/RoadTest.java @@ -0,0 +1,69 @@ +package ch.zhaw.catan.structure; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static ch.zhaw.catan.game.Config.Faction.YELLOW; +import static ch.zhaw.catan.game.Config.Structure.ROAD; +import static ch.zhaw.catan.structure.Road.AMOUNT_PER_RESOURCE; +import static ch.zhaw.catan.structure.Road.IDENTIFIER; +import static ch.zhaw.catan.structure.Road.SCORE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for the {@link Road} class. + * + */ +class RoadTest { + + private Road road; + + /** + * Sets up the test fixture by creating a new Road object. + */ + @BeforeEach + void setup() { + road = new Road(YELLOW); + } + + /** + * Tests the {@link Road#getStructureType()} method. + */ + @Test + void testGetStructureType() { + assertEquals(ROAD, road.getStructureType()); + } + + /** + * Tests the {@link Road#getScore()} method. + */ + @Test + void testGetScore() { + assertEquals(SCORE, road.getScore()); + } + + /** + * Tests the {@link Road#getIdentifier()} method. + */ + @Test + void testGetIdentifier() { + assertEquals(IDENTIFIER, road.getIdentifier()); + } + + /** + * Tests the {@link Road#getAmountPerResource()} method. + */ + @Test + void testGetAmountPerResource() { + assertEquals(AMOUNT_PER_RESOURCE, road.getAmountPerResource()); + } + + /** + * Tests the {@link Road#toString()} method. + */ + @Test + void testToString() { + assertEquals("y" + IDENTIFIER, road.toString()); + } + +} \ No newline at end of file diff --git a/src/test/java/ch/zhaw/catan/structure/SettlementTest.java b/src/test/java/ch/zhaw/catan/structure/SettlementTest.java new file mode 100644 index 0000000..49a7f08 --- /dev/null +++ b/src/test/java/ch/zhaw/catan/structure/SettlementTest.java @@ -0,0 +1,69 @@ +package ch.zhaw.catan.structure; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static ch.zhaw.catan.game.Config.Faction.YELLOW; +import static ch.zhaw.catan.game.Config.Structure.SETTLEMENT; +import static ch.zhaw.catan.structure.Settlement.AMOUNT_PER_RESOURCE; +import static ch.zhaw.catan.structure.Settlement.IDENTIFIER; +import static ch.zhaw.catan.structure.Settlement.SCORE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for the {@link Settlement} class. + * + */ +class SettlementTest { + + private Settlement settlement; + + /** + * Sets up the test fixture by creating a new Settlement object. + */ + @BeforeEach + void setup() { + settlement = new Settlement(YELLOW); + } + + /** + * Tests the {@link Settlement#getStructureType()} method. + */ + @Test + void testGetStructureType() { + assertEquals(SETTLEMENT, settlement.getStructureType()); + } + + /** + * Tests the {@link Settlement#getScore()} method. + */ + @Test + void testGetScore() { + assertEquals(SCORE, settlement.getScore()); + } + + /** + * Tests the {@link Settlement#getIdentifier()} method. + */ + @Test + void testGetIdentifier() { + assertEquals(IDENTIFIER, settlement.getIdentifier()); + } + + /** + * Tests the {@link Settlement#getAmountPerResource()} method. + */ + @Test + void testGetAmountPerResource() { + assertEquals(AMOUNT_PER_RESOURCE, settlement.getAmountPerResource()); + } + + /** + * Tests the {@link Settlement#toString()} method. + */ + @Test + void testToString() { + assertEquals("y" + IDENTIFIER, settlement.toString()); + } + +} \ No newline at end of file diff --git a/src/test/java/ch/zhaw/hexboard/EdgeTest.java b/src/test/java/ch/zhaw/hexboard/EdgeTest.java new file mode 100644 index 0000000..7c89195 --- /dev/null +++ b/src/test/java/ch/zhaw/hexboard/EdgeTest.java @@ -0,0 +1,106 @@ +package ch.zhaw.hexboard; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.awt.Point; + +/*** + *

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

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

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

+ * @author tebe + */ +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 + void edgeTest() { + for (int i = 0; i < corner.length - 1; i++) { + Assertions.assertNull(board.getEdge(corner[i], corner[i + 1])); + board.setEdge(corner[i], corner[i + 1], Integer.toString(i)); + Assertions.assertEquals(board.getEdge(corner[i], corner[i + 1]), Integer.toString(i)); + } + } + + @Test + void noEdgeCoordinatesTest() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> board.getEdge(new Point(2, 2), new Point(0, 2))); + } + + @Test + void edgeDoesNotExistTest() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> board.getEdge(new Point(0, 2), new Point(3, 1))); + } + + // Corner retrieval + @Test + void cornerTest() { + for (Point p : corner) { + Assertions.assertNull(board.getCorner(p)); + board.setCorner(p, p.toString()); + Assertions.assertEquals(board.getCorner(p), p.toString()); + } + } + + @Test + void noCornerCoordinateTest() { + Assertions.assertThrows(IllegalArgumentException.class, () -> board.getCorner(new Point(2, 2))); + } + + @Test + void cornerDoesNotExistTest() { + Assertions.assertThrows(IllegalArgumentException.class, () -> board.getCorner(new Point(2, 2))); + } + + // Field addition/retrieval + @Test + void fieldAreadyExistsErrorTest() { + board.addField(new Point(2, 2), "22"); + Assertions.assertThrows(IllegalArgumentException.class, () -> board.addField(new Point(2, 2), "22")); + } + + @Test + void fieldRetrievalTest() { + Point field = new Point(2, 2); + board.addField(field, "22"); + Assertions.assertTrue(board.hasField(field)); + Assertions.assertEquals("22", board.getField(field)); + } + + @Test + void fieldRetrievalWrongCoordinatesOutsideTest() { + Assertions.assertThrows(IllegalArgumentException.class, () -> board.getField(new Point(10, 10))); + } + + @Test + void fieldRetrievalWrongCoordinatesInsideTest() { + Assertions.assertThrows(IllegalArgumentException.class, () -> board.getField(new Point(2, 2))); + } +}