diff --git a/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java b/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java index 54e33756a..fac5ff1d9 100644 --- a/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java +++ b/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java @@ -23,7 +23,6 @@ public class AutoCaseRuleCommand extends PuzzleCommand { private List caseTrans; - private static final int MAX_CASES = 10; /** * AutoCaseRuleCommand Constructor creates a command for validating a case rule @@ -118,9 +117,13 @@ public String getErrorString() { return "The selection must produce at least one case"; } - if (caseRule.getCases(caseBoard.getBaseBoard(), elementView.getPuzzleElement()).size() - > MAX_CASES) { - return "The selection can produce a max of " + MAX_CASES + " cases"; + int numberOfCaseRules = caseRule.getCases(caseBoard.getBaseBoard(), elementView.getPuzzleElement()).size(); + System.out.println("Number of cases:" + numberOfCaseRules); + if (numberOfCaseRules > caseRule.MAX_CASES) { + return "The selection can produce a max of " + caseRule.MAX_CASES + " cases"; + } + if (numberOfCaseRules < caseRule.MIN_CASES) { + return "The selection must produce a minimum of " + caseRule.MIN_CASES + " cases"; } return null; diff --git a/src/main/java/edu/rpi/legup/model/rules/CaseRule.java b/src/main/java/edu/rpi/legup/model/rules/CaseRule.java index e87896fcc..ab2cc8f0d 100644 --- a/src/main/java/edu/rpi/legup/model/rules/CaseRule.java +++ b/src/main/java/edu/rpi/legup/model/rules/CaseRule.java @@ -14,6 +14,8 @@ public abstract class CaseRule extends Rule { private final String INVALID_USE_MESSAGE; + public int MAX_CASES; + public int MIN_CASES; /** * CaseRule Constructor creates a new case rule. @@ -27,6 +29,8 @@ public CaseRule(String ruleID, String ruleName, String description, String image super(ruleID, ruleName, description, imageName); this.ruleType = CASE; this.INVALID_USE_MESSAGE = "Invalid use of the case rule " + this.ruleName; + this.MAX_CASES = 10; + this.MIN_CASES = 1; // By default, this will not actually have any effect } /** @@ -68,7 +72,6 @@ public String checkRule(TreeTransition transition) { return "All children nodes must be justified with the same case rule."; } } - String check = checkRuleRaw(transition); // Mark transition and new data as valid or not diff --git a/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/BlackOrWhiteCaseRule.java b/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/BlackOrWhiteCaseRule.java index fe1ffcf91..ac0ab6df6 100644 --- a/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/BlackOrWhiteCaseRule.java +++ b/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/BlackOrWhiteCaseRule.java @@ -19,6 +19,7 @@ public BlackOrWhiteCaseRule() { "Black or White", "Each blank cell is either black or white.", "edu/rpi/legup/images/nurikabe/cases/BlackOrWhite.png"); + this.MAX_CASES = 2; } /** diff --git a/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/FinishRoomCaseRule.java b/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/FinishRoomCaseRule.java new file mode 100644 index 000000000..47ad7af37 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/FinishRoomCaseRule.java @@ -0,0 +1,182 @@ +package edu.rpi.legup.puzzle.nurikabe.rules; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.CaseBoard; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.CaseRule; +import edu.rpi.legup.model.tree.TreeTransition; +import edu.rpi.legup.puzzle.nurikabe.NurikabeBoard; +import edu.rpi.legup.puzzle.nurikabe.NurikabeCell; +import edu.rpi.legup.puzzle.nurikabe.NurikabeType; +import edu.rpi.legup.puzzle.nurikabe.NurikabeUtilities; +import edu.rpi.legup.utility.DisjointSets; + +import java.awt.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FinishRoomCaseRule extends CaseRule { + public FinishRoomCaseRule() { + super("NURI-CASE-0002", + "Finish Room", + "Room can be finished in up to five ways", + "edu/rpi/legup/images/nurikabe/cases/FinishRoom.png"); + this.MAX_CASES = 5; + this.MIN_CASES = 2; + } + + /** + * Checks whether the {@link TreeTransition} logically follows from the parent node using this rule. This method is + * the one that should overridden in child classes. + * + * @param transition transition to check + * @return null if the child node logically follow from the parent node, otherwise error message + */ + @Override + public String checkRuleRaw(TreeTransition transition) { + NurikabeBoard destBoardState = (NurikabeBoard) transition.getBoard(); + List childTransitions = transition.getParents().get(0).getChildren(); + if (childTransitions.size() > 5) { + return super.getInvalidUseOfRuleMessage() + ": This case rule must have 5 or less children."; + } + if(childTransitions.size() < 2) { + return super.getInvalidUseOfRuleMessage() + ": This case rule must have 2 or more children."; + } + Set locations = new HashSet<>(); + for (TreeTransition t1 : childTransitions) { + locations.add(((NurikabeCell) t1.getBoard().getModifiedData().iterator().next()).getLocation()); //loop see if matches + if (t1.getBoard().getModifiedData().size() != 1) { + return super.getInvalidUseOfRuleMessage() + ": This case rule must have 1 modified cell for each case."; + } + for (Point loc : locations) { + for (Point loc2 : locations) { + if (!(loc.equals(loc2)) && (loc.x == loc2.x) && (loc.y == loc2.y)) { + return super.getInvalidUseOfRuleMessage() + ": This case rule must alter a different cell for each case."; + } + } + } + } + + return null; + } + + @Override + public CaseBoard getCaseBoard(Board board) { + NurikabeBoard nurikabeBoard = (NurikabeBoard) board.copy(); + CaseBoard caseBoard = new CaseBoard(nurikabeBoard, this); + DisjointSets regions = NurikabeUtilities.getNurikabeRegions(nurikabeBoard); + nurikabeBoard.setModifiable(false); + + + for (PuzzleElement element : nurikabeBoard.getPuzzleElements()) { //loops all puzzle elements + if (((NurikabeCell) element).getType() == NurikabeType.NUMBER) { //if the tile is a white number block + Set disRow = regions.getSet(((NurikabeCell) element)); //store the row of the white region + boolean only = true; //placeholder boolean of if the element being tested is the only number block in the room or not + for(NurikabeCell d : disRow) { //loops through tiles in the room + if((d.getType() == NurikabeType.NUMBER) && !(d.getData().equals(((NurikabeCell) element).getData()))) { //if found another number tile and it's data is different than the element we're working with + only = false; //set only to false + } + } + if (disRow.size() + 1 == ((NurikabeCell) element).getData() && only) { //if size of region is 1 less than the number block and the number block is only number block in the region + caseBoard.addPickableElement(element); //add that room as a pickable element + } + } + } + return caseBoard; + } + + /** + * Gets the possible cases at a specific location based on this case rule + * + * @param board the current board state + * @param puzzleElement equivalent puzzleElement + * @return a list of elements the specified could be + */ + @Override + public ArrayList getCases(Board board, PuzzleElement puzzleElement) {//throws IllegalStateException { + ArrayList cases = new ArrayList<>(); //makes array list of cases + NurikabeBoard nuriBoard = (NurikabeBoard) board.copy(); //nurikabe board to edit + NurikabeCell numbaCell = nuriBoard.getCell(((NurikabeCell) puzzleElement).getLocation().x, + ((NurikabeCell) puzzleElement).getLocation().y); //number cell whose room we want to fill + int filledRoomSize = numbaCell.getData(); //size of room we want afterward + Set locations = new HashSet<>(); //locations where white space is added to finish room + Point left = new Point(-1,0); + Point right = new Point(1, 0); + Point bot = new Point(0, -1); + Point top = new Point(0, 1); + Set directions = new HashSet<>(); + directions.add(left); + directions.add(right); + directions.add(top); + directions.add(bot); + Set checkedPoints = new HashSet<>(); //add all into checked points and continue at start of loop if inside + DisjointSets regions = NurikabeUtilities.getNurikabeRegions(nuriBoard); //gathers regions + Set disRow = regions.getSet(numbaCell); //set of white spaces + for (NurikabeCell d : disRow) { //loops through white spaces + if(cases.size() >= 6) { //no need to check this many cases + //throw new IllegalStateException("Too many cases"); + continue; //crash/runtime protection + } + for(Point direction : directions) { + if(cases.size() >= 6) { //no need to check this many cases + //throw new IllegalStateException("Too many cases"); + continue; //crash/runtime protection + } + if(!((nuriBoard.getWidth() > (d.getLocation().x + direction.x) && (nuriBoard.getHeight() > d.getLocation().y + direction.y) && + (d.getLocation().x + direction.x >= 0) && (d.getLocation().y + direction.y >= 0)))) { + continue; //if next location check would be outside of grid then continue + } + NurikabeCell curr = nuriBoard.getCell(d.getLocation().x + direction.x, d.getLocation().y + direction.y); + if(checkedPoints.contains(curr.getLocation())) { + continue; //if we already checked whether or not making this tile white would complete the room then continue + } + checkedPoints.add(curr.getLocation()); //adds location to checkedPoints so we don't check it again and accidentally add + if (curr.getType() == NurikabeType.UNKNOWN) { //found adjacent space to region that is currently unknown + curr.setData(NurikabeType.WHITE.toValue()); //changes adjacent cell color to white + nuriBoard.addModifiedData(curr); // adds modified before check + regions = NurikabeUtilities.getNurikabeRegions(nuriBoard); //update regions + Set disCreatedRow = regions.getSet(curr); //gets set of created row with new white cell added + if (disCreatedRow.size() == filledRoomSize) { //If adding white fills the room to exact size of number block and doesn't connect with another room + Point here = curr.getLocation(); //gets current location of new white tile that fills room + boolean alreadyIn = false; //sets whether or not the tile has already been added to false + for (Point p : locations) { //loops through locations of previously added tiles + if (p == here) { //if point is already in + alreadyIn = true; //change already in to true + break; + } + } + if (!alreadyIn) { //if point wasn't already in + Board casey = nuriBoard.copy(); //copy the current board with white tile changed + PuzzleElement datacasey = curr; //gets changed white tile as a puzzle element + datacasey.setData(NurikabeType.WHITE.toValue()); //ensure set to white, probably redundant + casey.addModifiedData(datacasey); //ensure confirmed white change + regions = NurikabeUtilities.getNurikabeRegions(nuriBoard); //update regions + cases.add(casey); //add this case to list of cases + locations.add(here); //add location of new white tile to list of locations so that we don't accidentally add it again later + } + } + curr.setData(NurikabeType.UNKNOWN.toValue()); //set cell type back to unknown + nuriBoard.addModifiedData(curr); // confirms change back to unknown + regions = NurikabeUtilities.getNurikabeRegions(nuriBoard); //updates regions + } + } + } + return cases; + } + + /** + * Checks whether the child node logically follows from the parent node + * at the specific puzzleElement index using this rule + * + * @param transition transition to check + * @param puzzleElement equivalent puzzleElement + * @return null if the child node logically follow from the parent node at the specified puzzleElement, + * otherwise error message + */ + @Override + public String checkRuleRawAt(TreeTransition transition, PuzzleElement puzzleElement) { + return null; + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/nurikabe_reference_sheet.txt b/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/nurikabe_reference_sheet.txt index 6a6e6bc1f..596de7819 100644 --- a/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/nurikabe_reference_sheet.txt +++ b/src/main/java/edu/rpi/legup/puzzle/nurikabe/rules/nurikabe_reference_sheet.txt @@ -16,4 +16,5 @@ NURI-CONT-0005 : NoNumberContradictionRule NURI-CONT-0006 : TooFewSpacesContradictionRule NURI-CONT-0007 : TooManySpacesContradictionRule -NURI-CASE-0001 : BlackOrWhiteCaseRule \ No newline at end of file +NURI-CASE-0001 : BlackOrWhiteCaseRule +NURI-CASE-0002 : FinishRoomCaseRule \ No newline at end of file diff --git a/src/main/resources/edu/rpi/legup/images/nurikabe/cases/FinishRoom.png b/src/main/resources/edu/rpi/legup/images/nurikabe/cases/FinishRoom.png new file mode 100644 index 000000000..207d4c820 Binary files /dev/null and b/src/main/resources/edu/rpi/legup/images/nurikabe/cases/FinishRoom.png differ diff --git a/src/test/java/puzzles/nurikabe/rules/FinishRoomCaseRuleTest.java b/src/test/java/puzzles/nurikabe/rules/FinishRoomCaseRuleTest.java new file mode 100644 index 000000000..ec9b1cff9 --- /dev/null +++ b/src/test/java/puzzles/nurikabe/rules/FinishRoomCaseRuleTest.java @@ -0,0 +1,114 @@ +package puzzles.nurikabe.rules; + +import edu.rpi.legup.history.AutoCaseRuleCommand; +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.puzzle.nurikabe.*; +import edu.rpi.legup.puzzle.nurikabe.rules.FinishRoomCaseRule; +import edu.rpi.legup.utility.DisjointSets; +import legup.MockGameBoardFacade; +import legup.TestUtilities; +import edu.rpi.legup.model.tree.TreeNode; +import edu.rpi.legup.model.tree.TreeTransition; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import edu.rpi.legup.save.InvalidFileFormatException; + +import java.util.ArrayList; + +import java.awt.*; +import java.util.Set; + +public class FinishRoomCaseRuleTest { + + private static final FinishRoomCaseRule RULE = new FinishRoomCaseRule(); + private static Nurikabe nurikabe; + + @BeforeClass + public static void setUp() { + MockGameBoardFacade.getInstance(); + nurikabe = new Nurikabe(); + } + + /** + * Tests the Finish Room case rule by ensuring that it results in 5 or less children, that contain a modified cell that is white + */ + @Test + public void FinishRoomCaseRule_FinishRoomCaseRuleBaseTest() throws InvalidFileFormatException { + TestUtilities.importTestBoard("puzzles/nurikabe/rules/FinishRoomCaseRule/FinishRoomCaseRuleBase", nurikabe); + TreeNode rootNode = nurikabe.getTree().getRootNode(); + TreeTransition transition = rootNode.getChildren().get(0); + transition.setRule(RULE); + + NurikabeBoard board = (NurikabeBoard) transition.getBoard(); + NurikabeCell cell = board.getCell(6,4); + ArrayList cases = RULE.getCases(board,cell); + Assert.assertEquals(2,cases.size()); + + NurikabeBoard caseBoard = (NurikabeBoard) cases.get(0); + NurikabeBoard caseBoard2 = (NurikabeBoard) cases.get(1); + + NurikabeType board1Type = caseBoard.getCell(5,5).getType(); + NurikabeType board2Type = caseBoard2.getCell(6,6).getType(); + + Assert.assertTrue((board1Type.equals(NurikabeType.WHITE) && board2Type.equals(NurikabeType.WHITE))); + + Assert.assertEquals(caseBoard.getHeight(),caseBoard2.getHeight(), board.getHeight()); + Assert.assertEquals(caseBoard.getWidth(),caseBoard2.getWidth(), board.getWidth()); + + DisjointSets regions = NurikabeUtilities.getNurikabeRegions(caseBoard); //gathers regions + Set disRow = regions.getSet(caseBoard.getCell(5, 5)); + Assert.assertEquals(disRow.size(), 3); + + DisjointSets regions2 = NurikabeUtilities.getNurikabeRegions(caseBoard2); //gathers regions + Set disRow2 = regions2.getSet(caseBoard2.getCell(6, 6)); + Assert.assertEquals(disRow2.size(), 3); + + for(int i=0; i cases2 = RULE.getCases(board,cell2); + + Assert.assertEquals(6,cases2.size()); //correctly stops generating possible cases after + // more than 5 (the max) is found. Would have generated 8 cases +// FinishRoomCaseRule finny = new FinishRoomCaseRule(); +// finny.checkRuleRaw(); + //"Invalid use of the case rule FinishRoom: This case rule must have 5 or less children." + + //getErrorString in auto case rule + //should display "The selection can produce a max of 5 cases." + //AutoCaseRuleCommand autoCaseRuleCommand = new AutoCaseRuleCommand(elementView, selection, caseBoard.getCaseRule(), caseBoard, e); + +// NurikabeBoard caseyBoard = (NurikabeBoard) cases2.get(0); +// NurikabeBoard caseyBoard2 = (NurikabeBoard) cases2.get(1); +// NurikabeBoard caseyBoard3 = (NurikabeBoard) cases2.get(2); +// NurikabeBoard caseyBoard4 = (NurikabeBoard) cases2.get(3); +// NurikabeBoard caseyBoard5 = (NurikabeBoard) cases2.get(4); +// NurikabeBoard caseyBoard6 = (NurikabeBoard) cases2.get(5); +// NurikabeBoard caseyBoard7 = (NurikabeBoard) cases2.get(6); +// NurikabeBoard caseyBoard8 = (NurikabeBoard) cases2.get(7); +// +// NurikabeType boardy1Type = caseyBoard.getCell(5,5).getType(); +// NurikabeType boardy2Type = caseyBoard2.getCell(6,6).getType(); +// NurikabeType boardy3Type = caseyBoard.getCell(5,5).getType(); +// NurikabeType boardy4Type = caseyBoard2.getCell(6,6).getType(); +// NurikabeType boardy5Type = caseyBoard.getCell(5,5).getType(); +// NurikabeType boardy6Type = caseyBoard2.getCell(6,6).getType(); +// NurikabeType boardy7Type = caseyBoard.getCell(5,5).getType(); +// NurikabeType boardy8Type = caseyBoard2.getCell(6,6).getType(); + + + } +} diff --git a/src/test/resources/puzzles/nurikabe/rules/FinishRoomCaseRule/FinishRoomCaseRuleBase b/src/test/resources/puzzles/nurikabe/rules/FinishRoomCaseRule/FinishRoomCaseRuleBase new file mode 100644 index 000000000..c40870b96 --- /dev/null +++ b/src/test/resources/puzzles/nurikabe/rules/FinishRoomCaseRule/FinishRoomCaseRuleBase @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file