package comp1110.ass2.stringcomparator; import java.util.*; import java.util.stream.Collectors; /** * Comparison class that lets you check certain parts of the String * Main method is compare, which will take two strings: A expected and B actual. * You can pick which parts of the Strings you want to make sure match. The order of Statements is not * important in each String * You can choose which checks you want to make using check() checkAll() and checkIgnore(). These take a * resultType enum value that points to the type of statement you want to check. You can also choose some * options within each resultType (e.g. you may want to just check the NUMBER of resource coordinates in the * Resource Statements match, rather than the actual coordinate values). *

* These are the options you have for each resultType * General: * "numStatements": do the number of statements in each String match? * "unrecognisedStatements": are there any statements that begin with an unrecognised Statement ID? * GameArrangement: * "boardHeight": does the boardHeight value match? * "numPlayers": does the numPlayers value match? * CurrentState: * "currentPlayer": does the currentPlayer value match? * "phase": does the phase value match? * Island: * No options - just check if the Strings match * Stones: * No options - just check if there is exactly 1 Stones Statement, and it matches * Resources: * "checkNumCoords": does the number of coordinates match? * "checkMatching": do the actual coordinate values match? * Player: * "score": does the score value match? * "resources": do the resource counts match? * "pieces": do the piece coordinates match? *

* An example of checking all General, only the phase of the CurrentState, and all but the resources of Player would be * StringComparator sc = new StringComparator(); * sc.checkAll(ResultType.General); * sc.check(ResultType.CurrentState, List.of(new String[]{"phase"})); * sc.checkIgnore(ResultType.Player, List.of(new String[]{"resources"})); * List errors = sc.compare(stateStrA, stateStrB); *

* errors will be a list of Strings that point out issues found in B, or differences between A and B. It is assumed * that A is well-formed. *

* A helper method, compareFormatted, will return a formatted String given both A and B and the errors between them. */ public class StringComparator { Map> checks = new HashMap<>(); public void checkIgnore(ResultType resultType, List fields) { checkAll(resultType); checks.get(resultType).removeAll(fields); } public void checkAll(ResultType resultType) { switch (resultType) { case General -> checks.put(ResultType.General, new ArrayList<>(List.of(new String[]{"numStatements", "unrecognisedStatements"})) ); case GameArrangement -> checks.put(ResultType.GameArrangement, new ArrayList<>(List.of(new String[]{"boardHeight", "numPlayers"})) ); case CurrentState -> checks.put(ResultType.CurrentState, new ArrayList<>(List.of(new String[]{"currentPlayer", "phase"})) ); case Island -> checks.put(ResultType.Island, new ArrayList<>()); case Stones -> checks.put(ResultType.Stones, new ArrayList<>()); case Resources -> checks.put(ResultType.Resources, new ArrayList<>(List.of(new String[]{"checkNumCoords", "checkMatching"})) ); case Player -> checks.put(ResultType.Player, new ArrayList<>(List.of(new String[]{"score", "resources", "pieces"})) ); } } public List compare(String stateStringA, String stateStringB) { // Result List errors = new LinkedList<>(); // Gather, trim, filter statements List statementsA = List.of(stateStringA.split(";")); statementsA = statementsA.stream().map(String::trim) .filter(statement -> statement.length() > 0).collect(Collectors.toList()); List statementsB = List.of(stateStringB.split(";")); statementsB = statementsB.stream().map(String::trim) .filter(statement -> statement.length() > 0).collect(Collectors.toList()); // Check the general string formation if (checks.containsKey(ResultType.General)) { checkGeneralString(statementsA, statementsB, errors); } // Check the Game Arrangement Statements // “a {boardHeight} {numPlayers}” if (checks.containsKey(ResultType.GameArrangement)) { checkArrangementStatements(statementsA, statementsB, errors); } // Check the Current State Statement if (checks.containsKey(ResultType.CurrentState)) { checkCurrentStateStatements(statementsA, statementsB, errors); } // Check the Island Statements if (checks.containsKey(ResultType.Island)) { checkIslandStatements(statementsA, statementsB, errors); } // Check the Stones Statement if (checks.containsKey(ResultType.Stones)) { checkStonesStatements(statementsA, statementsB, errors); } // Check the Unclaimed Resources Statement if (checks.containsKey(ResultType.Resources)) { checkResourcesStatements(statementsA, statementsB, errors); } // Check the Player Statements if (checks.containsKey(ResultType.Player)) { checkPlayerStatements(statementsA, statementsB, errors); } // Return Result return errors; } private void checkGeneralString(List statementsA, List statementsB, List errors) { // Check for the number of statements if (checks.get(ResultType.General).contains("num_statements") && statementsA.size() != statementsB.size()) { errors.add("Strings contain different numbers of statements"); } // Check if there are any unrecognised statement IDs if (checks.get(ResultType.General).contains("unrecognised_statements")) { List unrecognised = statementsB .stream() .filter(statement -> !statement.matches("[acisrp].*")) .toList(); if (unrecognised.size() != 0) { errors.add("Unrecognised Statements: "+unrecognised); } } } private void checkPlayerStatements(List statementsA, List statementsB, List errors) { //“p {playerId} {score} {coconut} {bamboo} {water} {preciousStone} {statuette} S ({row,col})* V ({row,col})*” List toCheck = checks.get(ResultType.Player); List playerStatementsA = getStatement(statementsA, 'p'); List playerStatementsB = getStatement(statementsB, 'p'); if (playerStatementsA.size() != playerStatementsB.size()) { errors.add("Different number of Player Statements. Expecting: "+playerStatementsA.size()+ " Found: "+playerStatementsB.size()); } // Break into checkable pieces List> playerAParts = playerStatementsA .stream() .map(this::getPlayerParts) .toList(); // Note the IDs expected boolean unexpectedID = false; List expectedIDs = new ArrayList<>(); for (List parts : playerAParts) { expectedIDs.add(parts.get(1)); } // Check through all submitted Player Statements outer: for (String playerBStr : playerStatementsB) { List playerBParts = getPlayerParts(playerBStr); if (playerBParts.size() != 10) { errors.add("Player Statement not well formed "+playerBStr); continue; } String playerBID = playerBParts.get(1); // Note if the ID was not expected if (expectedIDs.contains(playerBID)) { expectedIDs.remove(playerBID); } else { unexpectedID = true; continue; } // Find the matching Player Statement in A for (List playerA : playerAParts) { if (playerBID.equals(playerA.get(1))) { // We've found the matching playerID // Check score if (toCheck.contains("score") && !playerBParts.get(2).equals(playerA.get(2))) errors.add("Player Statement with ID: "+playerBID+" has incorrect {score} information"); // Check resource numbers if (toCheck.contains("resources")) { for (int i = 3; i <= 7; i++) { if (!playerBParts.get(i).equals(playerA.get(i))) { errors.add("Player Statement with ID: "+playerBID+" has incorrect {resource} information"); break; } } } // Check piece coordinates if (toCheck.contains("pieces") && (!playerBParts.get(8).equals(playerA.get(8)) || !playerBParts.get(9).equals(playerA.get(9)))) errors.add("Player Statement with ID: "+playerBID+" has incorrect {piece} information"); continue outer; } } errors.add("No matching Player ID for Player: "+playerBStr); } // Check that no unexpected IDs were found if (unexpectedID) { errors.add("An unexpected Player ID was found"); } // Check that all expectedIDs were found if (expectedIDs.size() != 0) { errors.add("Missing Player Statements. Should have IDs: "+expectedIDs); } } List getPlayerParts(String playerStatement) { String[] splitStatement = playerStatement.split("[SV]"); List parts = new ArrayList<>(Arrays.stream(splitStatement[0].split(" ")).toList()); parts.add(splitStatement[1].trim()); if (splitStatement.length == 3) { parts.add(splitStatement[2].trim()); } else { parts.add(""); } return parts; } private void checkResourcesStatements(List statementsA, List statementsB, List errors) { List toCheck = checks.get(ResultType.Resources); List resourcesStatementsA = getStatement(statementsA, 'r'); List resourcesStatementsB = getStatement(statementsB, 'r'); if (resourcesStatementsB.size() != 1) { errors.add("String should have 1 Current State Statement. There are: "+resourcesStatementsB.size()); return; } String resourceA = resourcesStatementsA.get(0); String resourceB = resourcesStatementsB.get(0); String[] resourceAParts = resourceA.split("[CBWPS]"); resourceAParts = Arrays.copyOfRange(resourceAParts, 1, resourceAParts.length); String[] resourceBParts = resourceB.split("[CBWPS]"); resourceBParts = Arrays.copyOfRange(resourceBParts, 1, resourceBParts.length); // Check the coords in B are well-formed boolean wellFormed = resourceB.charAt(0) == 'r' && Arrays.stream(resourceBParts) .allMatch(StringComparator::coordsStrWellFormed); if (!wellFormed) { errors.add("Resource Statement coordinates are not well formed"); return; } // Check that there is a matching NUMBER of resource coordinates if (toCheck.contains("checkNumCoords")) { int numACoords = Arrays.stream(resourceAParts) .map(coordsStr -> coordsStr.split(" ").length) .reduce(0, Integer::sum); int numBCoords = Arrays.stream(resourceBParts) .map(coordsStr -> coordsStr.split(" ").length) .reduce(0, Integer::sum); if (numACoords != numBCoords) { errors.add("Number of placed resources does not match"); } } // Check that the resource coordinates ACTUALLY match if (toCheck.contains("checkMatching")) { if (!resourceA.equals(resourceB)) { errors.add("Resource Statements do not match"); } } } static boolean coordsStrWellFormed(String coordsStr) { if(coordsStr.trim().length() == 0){ return true; } return Arrays.stream(coordsStr.trim().split(" ")) .allMatch(coordinate -> coordinate.matches("\\d+,\\d+")); } private void checkStonesStatements(List statementsA, List statementsB, List errors) { List stonesStatementsA = getStatement(statementsA, 's'); List stonesStatementsB = getStatement(statementsB, 's'); if (stonesStatementsB.size() != 1) { errors.add("String should have 1 Stones Statement. Found "+stonesStatementsB.size()); return; } if (!stonesStatementsB.equals(stonesStatementsA)) { errors.add("Stones Statements do not match"); } } private void checkIslandStatements(List statementsA, List statementsB, List errors) { //“i {bonus} ({row,col})*” List islandStatementsA = new ArrayList<>(getStatement(statementsA, 'i')); List islandStatementsB = new ArrayList<>(getStatement(statementsB, 'i')); if (islandStatementsA.size() != islandStatementsB.size()) { errors.add("Different number of Island Statements. Expecting: "+islandStatementsA.size()+ " Found: "+islandStatementsB.size()); } outer: for (String bStatement : islandStatementsB) { for (String aStatement : islandStatementsA) { if (aStatement.equals(bStatement)) { islandStatementsA.remove(aStatement); continue outer; } } errors.add("Island Statement matches none in expected: "+bStatement); } if (islandStatementsA.size() != 0) { errors.add("Island Statements expected but not found: "+islandStatementsA); } } private void checkCurrentStateStatements(List statementsA, List statementsB, List errors) { //"c {currentPlayer} {phase}" List toCheck = checks.get(ResultType.CurrentState); List currentStateStatementsA = getStatement(statementsA, 'c'); List currentStateStatementsB = getStatement(statementsB, 'c'); if (currentStateStatementsB.size() != 1) { errors.add("String should have 1 Current State Statement. There are: " + currentStateStatementsB.size()); return; } String[] AParts = currentStateStatementsA.get(0).split(" "); String[] BParts = currentStateStatementsB.get(0).split(" "); if (AParts.length != BParts.length) { errors.add("Current State Statement not well formed"); return; } if (toCheck.contains("currentPlayer") && !AParts[1].equals(BParts[1])) { errors.add("Current State {currentPlayer} does not match"); } if (toCheck.contains("phase") && !AParts[2].equals(BParts[2])) { errors.add("Current State {phase} does not match"); } } private void checkArrangementStatements(List statementsA, List statementsB, List errors) { List toCheck = checks.get(ResultType.GameArrangement); List arrangementStatementsA = getStatement(statementsA, 'a'); List arrangementStatementsB = getStatement(statementsB, 'a'); if (arrangementStatementsB.size() != 1) { errors.add("String should have 1 Arrangement Statement. There are: " + arrangementStatementsB.size()); return; } String[] AParts = arrangementStatementsA.get(0).split(" "); String[] BParts = arrangementStatementsB.get(0).split(" "); if (AParts.length != BParts.length) { errors.add("Arrangement Statement not well formed"); return; } if (toCheck.contains("boardHeight") && !AParts[1].equals(BParts[1])) { errors.add("Arrangement Statement {boardHeight} does not match"); } if (toCheck.contains("numPlayers") && !AParts[2].equals(BParts[2])) { errors.add("Arrangement Statement {numPlayers} does not match"); } } // Return the statements that match the identifier static List getStatement(List statements, char statementIdentifier) { return statements .stream() .filter(statement -> statement.length() > 0 && statement.startsWith(String.valueOf(statementIdentifier))) .toList(); } public enum ResultType { General, GameArrangement, CurrentState, Island, Stones, Resources, Player } }