diff --git a/puzzles files/kakurasu/test b/puzzles files/kakurasu/test new file mode 100644 index 000000000..d3718ce1b --- /dev/null +++ b/puzzles files/kakurasu/test @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/edu/rpi/legup/history/History.java b/src/main/java/edu/rpi/legup/history/History.java index b244e8f88..3d49424fa 100644 --- a/src/main/java/edu/rpi/legup/history/History.java +++ b/src/main/java/edu/rpi/legup/history/History.java @@ -11,6 +11,7 @@ * It maintains a list of commands and a current index to track the position in the history stack. */ public class History { + // This object does not refer to edu.rpi.legup.utility's class Logger, but rather apache's interface Logger private static final Logger LOGGER = LogManager.getLogger(History.class.getName()); private final Object lock = new Object(); diff --git a/src/main/java/edu/rpi/legup/model/tree/Tree.java b/src/main/java/edu/rpi/legup/model/tree/Tree.java index 3e68015a1..a4aa3a822 100644 --- a/src/main/java/edu/rpi/legup/model/tree/Tree.java +++ b/src/main/java/edu/rpi/legup/model/tree/Tree.java @@ -97,9 +97,13 @@ public TreeElement addTreeElement(TreeTransition transition, TreeNode treeNode) * @param element the tree element to remove */ public void removeTreeElement(TreeElement element) { + // Currently this function does not delete children elements that extend from this node or transition. + // The children are indirectly removed from view (TreeView?) as they no longer have a connection to the base node. + // TODO: Recursively remove all children elements of this TreeElement if (element.getType() == TreeElementType.NODE) { TreeNode node = (TreeNode) element; + // Removes this node from its parent transition node.getParent().removeChild(node); node.getParent().setChildNode(null); } else { @@ -109,6 +113,7 @@ public void removeTreeElement(TreeElement element) { TreeController treeController = new TreeController(); TreeView treeView = new TreeView(treeController); treeView.removeTreeTransition(transition); + // Ensures that the other transition are still correct when this transition gets removed (redundant?) transition.getParents().get(0).getChildren().forEach(TreeTransition::reverify); } } diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/ClueCommand.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/ClueCommand.java new file mode 100644 index 000000000..6beebc0ef --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/ClueCommand.java @@ -0,0 +1,185 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.history.CommandError; +import edu.rpi.legup.history.PuzzleCommand; +import edu.rpi.legup.model.Puzzle; +import edu.rpi.legup.model.tree.*; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeElementView; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeView; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeViewSelection; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static edu.rpi.legup.app.GameBoardFacade.getInstance; + +public class ClueCommand extends PuzzleCommand { + private TreeViewSelection selection; + private KakurasuClueView clueView; + private Map addTran; + private List> emptyCells; + + public ClueCommand(TreeViewSelection selection, KakurasuClueView clueView) { + this.selection = selection; + this.clueView = clueView; + this.addTran = new HashMap<>(); + this.emptyCells = new ArrayList<>(); + } + + /** Executes a command */ + @Override + public void executeCommand() { + Puzzle puzzle = getInstance().getPuzzleModule(); + Tree tree = puzzle.getTree(); + TreeView treeView = getInstance().getLegupUI().getTreePanel().getTreeView(); + + final TreeViewSelection newSelection = new TreeViewSelection(); + for (int i = 0; i < selection.getSelectedViews().size(); i++) { + TreeElementView selectedView = selection.getSelectedViews().get(i); + TreeElement treeElement = selectedView.getTreeElement(); + + final TreeTransition finalTran; + KakurasuBoard board = (KakurasuBoard) treeElement.getBoard(); + List tempList = emptyCells.get(i); + if (treeElement.getType() == TreeElementType.NODE) { + TreeNode treeNode = (TreeNode) treeElement; + + TreeTransition transition = addTran.get(treeNode); + if (transition == null) { + transition = tree.addNewTransition(treeNode); + addTran.put(treeNode, transition); + } else { + treeNode.addChild(transition); + } + + finalTran = transition; + puzzle.notifyTreeListeners(listener -> listener.onTreeElementAdded(finalTran)); + + newSelection.addToSelection(treeView.getElementView(finalTran)); + board = (KakurasuBoard) finalTran.getBoard(); + } else { + finalTran = (TreeTransition) treeElement; + newSelection.addToSelection(treeView.getElementView(treeElement)); + } + + for (KakurasuCell cell : tempList) { + cell = (KakurasuCell) board.getPuzzleElement(cell); + cell.setData(KakurasuType.EMPTY); + board.addModifiedData(cell); + finalTran.propagateChange(cell); + + final KakurasuCell finalCell = cell; + puzzle.notifyBoardListeners(listener -> listener.onBoardDataChanged(finalCell)); + } + if (i == 0) { + puzzle.notifyBoardListeners(listener -> listener.onTreeElementChanged(finalTran)); + } + } + puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(newSelection)); + } + + /** + * Gets the reason why the command cannot be executed + * + * @return if command cannot be executed, returns reason for why the command cannot be executed, + * otherwise null if command can be executed + */ + @Override + public String getErrorString() { + if (selection.getSelectedViews().isEmpty()) { + return CommandError.NO_SELECTED_VIEWS.toString(); + } + + emptyCells.clear(); + for (TreeElementView view : selection.getSelectedViews()) { + TreeElement treeElement = view.getTreeElement(); + KakurasuBoard board = (KakurasuBoard) treeElement.getBoard(); + if (treeElement.getType() == TreeElementType.NODE) { + TreeNode node = (TreeNode) treeElement; + if (!node.getChildren().isEmpty()) { + return CommandError.UNMODIFIABLE_BOARD.toString(); + } + } else { + if (!board.isModifiable()) { + return CommandError.UNMODIFIABLE_BOARD.toString(); + } + } + + List tempList = new ArrayList<>(); + KakurasuClue clue = clueView.getPuzzleElement(); + if (clue.getType() == KakurasuType.CLUE_NORTH + || clue.getType() == KakurasuType.CLUE_SOUTH) { + int col = + clue.getType() == KakurasuType.CLUE_NORTH + ? clue.getClueIndex() + : clue.getClueIndex() - 1; + for (int i = 0; i < board.getWidth(); i++) { + KakurasuCell cell = board.getCell(col, i); + if (cell.getType() == KakurasuType.UNKNOWN && cell.isModifiable()) { + tempList.add(cell); + } + } + } else { + int row = + clue.getType() == KakurasuType.CLUE_WEST + ? clue.getClueIndex() + : clue.getClueIndex() - 1; + for (int i = 0; i < board.getWidth(); i++) { + KakurasuCell cell = board.getCell(i, row); + if (cell.getType() == KakurasuType.UNKNOWN && cell.isModifiable()) { + tempList.add(cell); + } + } + } + if (tempList.isEmpty()) { + return "There are no modifiable unknown cells in every selected tree element."; + } + emptyCells.add(tempList); + } + return null; + } + + /** Undoes a command */ + @Override + public void undoCommand() { + Puzzle puzzle = getInstance().getPuzzleModule(); + Tree tree = puzzle.getTree(); + + for (int i = 0; i < selection.getSelectedViews().size(); i++) { + TreeElementView selectedView = selection.getSelectedViews().get(i); + TreeElement treeElement = selectedView.getTreeElement(); + + final TreeTransition finalTran; + KakurasuBoard board = (KakurasuBoard) treeElement.getBoard(); + List tempList = emptyCells.get(i); + if (treeElement.getType() == TreeElementType.NODE) { + TreeNode treeNode = (TreeNode) treeElement; + + finalTran = treeNode.getChildren().get(0); + tree.removeTreeElement(finalTran); + puzzle.notifyTreeListeners(listener -> listener.onTreeElementRemoved(finalTran)); + + board = (KakurasuBoard) finalTran.getBoard(); + } else { + finalTran = (TreeTransition) treeElement; + } + + for (KakurasuCell cell : tempList) { + cell = (KakurasuCell) board.getPuzzleElement(cell); + cell.setData(KakurasuType.UNKNOWN); + board.removeModifiedData(cell); + + final KakurasuCell finalCell = cell; + puzzle.notifyBoardListeners(listener -> listener.onBoardDataChanged(finalCell)); + } + + if (i == 0) { + puzzle.notifyBoardListeners(listener -> listener.onTreeElementChanged(finalTran)); + } + } + final TreeViewSelection newSelection = selection; + puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(newSelection)); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/Kakurasu.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/Kakurasu.java new file mode 100644 index 000000000..82b3763f0 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/Kakurasu.java @@ -0,0 +1,86 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.Puzzle; +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.ContradictionRule; + +import java.util.List; + +public class Kakurasu extends Puzzle { + + public Kakurasu() { + super(); + + this.name = "Kakurasu"; + + this.importer = new KakurasuImporter(this); + this.exporter = new KakurasuExporter(this); + + this.factory = new KakurasuCellFactory(); + } + + /** Initializes the game board. Called by the invoker of the class */ + @Override + public void initializeView() { + KakurasuBoard board = (KakurasuBoard) currentBoard; + boardView = new KakurasuView((KakurasuBoard) currentBoard); + boardView.setBoard(board); + } + + /** + * Generates a random edu.rpi.legup.puzzle based on the difficulty + * + * @param difficulty level of difficulty (1-10) + * @return board of the random edu.rpi.legup.puzzle + */ + @Override + public Board generatePuzzle(int difficulty) { + return null; + } + + @Override + /** + * Determines if the given dimensions are valid for Kakurasu + * + * @param rows the number of rows + * @param columns the number of columns + * @return true if the given dimensions are valid for Tree Tent, false otherwise + */ + public boolean isValidDimensions(int rows, int columns) { + // This is a placeholder, this method needs to be implemented + return rows > 0 && columns > 0; + } + + /** + * Determines if the current board is a valid state + * + * @param board board to check for validity + * @return true if board is valid, false otherwise + */ + @Override + public boolean isBoardComplete(Board board) { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board; + + for (ContradictionRule rule : contradictionRules) { + if (rule.checkContradiction(kakurasuBoard) == null) { + return false; + } + } + for (PuzzleElement data : kakurasuBoard.getPuzzleElements()) { + KakurasuCell cell = (KakurasuCell) data; + if (cell.getType() == KakurasuType.UNKNOWN) { + return false; + } + } + return true; + } + + /** + * Callback for when the board puzzleElement changes + * + * @param board the board that has changed + */ + @Override + public void onBoardChange(Board board) {} +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuBoard.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuBoard.java new file mode 100644 index 000000000..36ee8ebfa --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuBoard.java @@ -0,0 +1,189 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.GridBoard; +import edu.rpi.legup.model.gameboard.PuzzleElement; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class KakurasuBoard extends GridBoard { + + private ArrayList rowClues; + private ArrayList colClues; + + public KakurasuBoard(int width, int height) { + super(width, height); + + this.rowClues = new ArrayList<>(); + this.colClues = new ArrayList<>(); + + for (int i = 0; i < height; i++) { + rowClues.add(null); + } + for (int i = 0; i < width; i++) { + colClues.add(null); + } + } + + public KakurasuBoard(int size) { + this(size, size); + } + + public ArrayList getRowClues() { + return rowClues; + } + + public ArrayList getColClues() { + return colClues; + } + + @Override + public KakurasuCell getCell(int x, int y) { + return (KakurasuCell) super.getCell(x, y); + } + + @Override + public PuzzleElement getPuzzleElement(PuzzleElement element) { + return switch (element.getIndex()) { + case -2, -1 -> element; + default -> super.getPuzzleElement(element); + }; + } + + @Override + public void setPuzzleElement(int index, PuzzleElement puzzleElement) { + if (index < puzzleElements.size()) { + puzzleElements.set(index, puzzleElement); + } + } + + @Override + public void notifyChange(PuzzleElement puzzleElement) { + int index = puzzleElement.getIndex(); + if (index < puzzleElements.size()) { + puzzleElements.set(index, puzzleElement); + } + } + + public KakurasuClue getClue(int x, int y) { + if (x == getWidth() && 0 <= y && y < getHeight()) { + return rowClues.get(y); + } else if (y == getHeight() && 0 <= x && x < getWidth()) { + return colClues.get(x); + } + return null; + } + + /** + * Get a list of all orthogonally adjacent cells. + * + * @param cell The cell to get adjacent cells from. + * @param type The cell types to get. + * @return List of adjacent cells in the form { up, right, down, left }. If an adjacent cell is + * null, it will not be added to the list. + */ + public List getAdjacent(KakurasuCell cell, KakurasuType type) { + List adj = new ArrayList<>(); + Point loc = cell.getLocation(); + for (int i = -2; i < 2; i++) { + KakurasuCell adjCell = getCell(loc.x + (i % 2), loc.y + ((i + 1) % 2)); + if (adjCell != null && adjCell.getType() == type) { + adj.add(adjCell); + } + } + return adj; + } + + /** + * Gets all cells of a specified type that are diagonals of a specified cell + * + * @param cell the base cell + * @param type the type to look for + * @return a list of TreeTentCells that are diagonals of the given KakurasuCell and are of the + * given KakurasuType + */ + public List getDiagonals(KakurasuCell cell, KakurasuType type) { + List dia = new ArrayList<>(); + Point loc = cell.getLocation(); + KakurasuCell upRight = getCell(loc.x + 1, loc.y - 1); + KakurasuCell downRight = getCell(loc.x + 1, loc.y + 1); + KakurasuCell downLeft = getCell(loc.x - 1, loc.y + 1); + KakurasuCell upLeft = getCell(loc.x - 1, loc.y - 1); + if (upRight != null && upRight.getType() == type) { + dia.add(upRight); + } + if (downLeft != null && downLeft.getType() == type) { + dia.add(downLeft); + } + if (downRight != null && downRight.getType() == type) { + dia.add(downRight); + } + if (upLeft != null && upLeft.getType() == type) { + dia.add(upLeft); + } + return dia; + } + + /** + * Creates and returns a list of TreeTentCells that match the given KakurasuType + * + * @param index the row or column number + * @param type type of Kakurasu element + * @param isRow boolean value based on whether a row of column is being checked + * @return List of TreeTentCells that match the given KakurasuType + */ + public List getRowCol(int index, KakurasuType type, boolean isRow) { + List list = new ArrayList<>(); + if (isRow) { + for (int i = 0; i < dimension.width; i++) { + KakurasuCell cell = getCell(i, index); + if (cell.getType() == type) { + list.add(cell); + } + } + } else { + for (int i = 0; i < dimension.height; i++) { + KakurasuCell cell = getCell(index, i); + if (cell.getType() == type) { + list.add(cell); + } + } + } + return list; + } + + /** + * Determines if this board contains the equivalent puzzle elements as the one specified + * + * @param board board to check equivalence + * @return true if the boards are equivalent, false otherwise + */ + @Override + public boolean equalsBoard(Board board) { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board; + return super.equalsBoard(kakurasuBoard); + } + + /** + * Performs a deep copy of the KakurasuBoard + * + * @return a KakurasuBoard object that is a deep copy of the current KakurasuBoard + */ + @Override + public KakurasuBoard copy() { + KakurasuBoard copy = new KakurasuBoard(dimension.width, dimension.height); + for (int x = 0; x < this.dimension.width; x++) { + for (int y = 0; y < this.dimension.height; y++) { + copy.setCell(x, y, getCell(x, y).copy()); + } + } + for (PuzzleElement e : modifiedData) { + copy.getPuzzleElement(e).setModifiable(false); + } + copy.rowClues = rowClues; + copy.colClues = colClues; + return copy; + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuCell.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuCell.java new file mode 100644 index 000000000..4aefb5b1f --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuCell.java @@ -0,0 +1,53 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.elements.Element; +import edu.rpi.legup.model.gameboard.GridCell; + +import java.awt.*; +import java.awt.event.MouseEvent; + +public class KakurasuCell extends GridCell { + + public KakurasuCell(KakurasuType value, Point location) { + super(value, location); + } + + public KakurasuType getType() { + return data; + } + + public int getValue() { + return switch (data) { + case FILLED -> 1; + case EMPTY -> 2; + default -> 0; + }; + } + + @Override + public void setType(Element e, MouseEvent m) { + switch (e.getElementName()) { + case "Unknown Tile": + this.data = KakurasuType.UNKNOWN; + break; + case "Filled Tile": + this.data = KakurasuType.FILLED; + break; + case "Empty Tile": + this.data = KakurasuType.EMPTY; + break; + default: + System.out.println("KakurasuCell.setType: Unknown element"); + break; + } + } + + @Override + public KakurasuCell copy() { + KakurasuCell copy = new KakurasuCell(data, (Point) location.clone()); + copy.setIndex(index); + copy.setModifiable(isModifiable); + copy.setGiven(isGiven); + return copy; + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuCellFactory.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuCellFactory.java new file mode 100644 index 000000000..b879013ea --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuCellFactory.java @@ -0,0 +1,76 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.ElementFactory; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.save.InvalidFileFormatException; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.awt.*; + +public class KakurasuCellFactory extends ElementFactory { + /** + * Creates a puzzleElement based on the xml document Node and adds it to the board + * + * @param node node that represents the puzzleElement + * @param board board to add the newly created cell + * @return newly created cell from the xml document Node + * @throws InvalidFileFormatException if file is invalid + */ + @Override + public PuzzleElement importCell(Node node, Board board) throws InvalidFileFormatException { + try { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board; + int width = kakurasuBoard.getWidth(); + int height = kakurasuBoard.getHeight(); + NamedNodeMap attributeList = node.getAttributes(); + if (node.getNodeName().equalsIgnoreCase("cell")) { + + int value = Integer.valueOf(attributeList.getNamedItem("value").getNodeValue()); + int x = Integer.valueOf(attributeList.getNamedItem("x").getNodeValue()); + int y = Integer.valueOf(attributeList.getNamedItem("y").getNodeValue()); + if (x >= width || y >= height) { + throw new InvalidFileFormatException( + "Kakurasu Factory: cell location out of bounds"); + } + if (value < 0 || value > 3) { + throw new InvalidFileFormatException("Kakurasu Factory: cell unknown value"); + } + + KakurasuCell cell = new KakurasuCell(KakurasuType.valueOf(value), new Point(x, y)); + cell.setIndex(y * width + x); + return cell; + } else { + throw new InvalidFileFormatException( + "Kakurasu Factory: unknown puzzleElement puzzleElement"); + } + } catch (NumberFormatException e) { + throw new InvalidFileFormatException( + "Kakurasu Factory: unknown value where integer expected"); + } catch (NullPointerException e) { + throw new InvalidFileFormatException("Kakurasu Factory: could not find attribute(s)"); + } + } + + /** + * Creates a xml document puzzleElement from a cell for exporting + * + * @param document xml document + * @param puzzleElement PuzzleElement cell + * @return xml PuzzleElement + */ + public org.w3c.dom.Element exportCell(Document document, PuzzleElement puzzleElement) { + org.w3c.dom.Element cellElement = document.createElement("cell"); + + KakurasuCell cell = (KakurasuCell) puzzleElement; + Point loc = cell.getLocation(); + + cellElement.setAttribute("value", String.valueOf(cell.getValue())); + cellElement.setAttribute("x", String.valueOf(loc.x)); + cellElement.setAttribute("y", String.valueOf(loc.y)); + + return cellElement; + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuClue.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuClue.java new file mode 100644 index 000000000..445ef3f20 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuClue.java @@ -0,0 +1,35 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.gameboard.PuzzleElement; + +public class KakurasuClue extends PuzzleElement { + private KakurasuType type; + private int clueIndex; + + public KakurasuClue(int value, int clueIndex, KakurasuType type) { + super(value); + this.index = -2; + this.clueIndex = clueIndex; + this.type = type; + } + + public int getClueIndex() { + return clueIndex; + } + + public void setClueIndex(int clueIndex) { + this.clueIndex = clueIndex; + } + + public KakurasuType getType() { + return type; + } + + public void setType(KakurasuType type) { + this.type = type; + } + + public KakurasuClue copy() { + return new KakurasuClue(data, clueIndex, type); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuClueView.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuClueView.java new file mode 100644 index 000000000..63e24738c --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuClueView.java @@ -0,0 +1,44 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.ui.boardview.ElementView; + +import java.awt.*; + +public class KakurasuClueView extends ElementView { + + private static final Font FONT = new Font("TimesRoman", Font.BOLD, 16); + private static final Color FONT_COLOR = Color.BLACK; + + public KakurasuClueView(KakurasuClue clue) { + super(clue); + } + + /** + * Gets the PuzzleElement associated with this view + * + * @return PuzzleElement associated with this view + */ + @Override + public KakurasuClue getPuzzleElement() { + return (KakurasuClue) super.getPuzzleElement(); + } + + @Override + public void drawElement(Graphics2D graphics2D) { + graphics2D.setColor(FONT_COLOR); + graphics2D.setFont(FONT); + FontMetrics metrics = graphics2D.getFontMetrics(FONT); + String value; + + KakurasuClue clue = getPuzzleElement(); + value = switch (clue.getType()) { + case CLUE_NORTH, CLUE_WEST -> String.valueOf(clue.getData() + 1); + case CLUE_EAST, CLUE_SOUTH -> String.valueOf(clue.getData()); + default -> ""; + }; + + int xText = location.x + (size.width - metrics.stringWidth(value)) / 2; + int yText = location.y + ((size.height - metrics.getHeight()) / 2) + metrics.getAscent(); + graphics2D.drawString(value, xText, yText); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuController.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuController.java new file mode 100644 index 000000000..cd166081a --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuController.java @@ -0,0 +1,50 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.app.GameBoardFacade; +import edu.rpi.legup.controller.ElementController; +import edu.rpi.legup.history.AutoCaseRuleCommand; +import edu.rpi.legup.history.EditDataCommand; +import edu.rpi.legup.history.ICommand; +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.ui.boardview.BoardView; +import edu.rpi.legup.ui.boardview.ElementView; +import edu.rpi.legup.ui.proofeditorui.treeview.TreePanel; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeView; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeViewSelection; + +import java.awt.event.MouseEvent; + +import static edu.rpi.legup.app.GameBoardFacade.getInstance; + +public class KakurasuController extends ElementController { + + @Override + public void changeCell(MouseEvent e, PuzzleElement element) { + KakurasuCell cell = (KakurasuCell) element; + if (e.getButton() == MouseEvent.BUTTON1) { + if (cell.getData() == KakurasuType.UNKNOWN) { + element.setData(KakurasuType.FILLED); + } else { + if (cell.getData() == KakurasuType.FILLED) { + element.setData(KakurasuType.EMPTY); + } else { + element.setData(KakurasuType.UNKNOWN); + } + } + } else { + if (e.getButton() == MouseEvent.BUTTON3) { + if (cell.getData() == KakurasuType.UNKNOWN) { + element.setData(KakurasuType.EMPTY); + } else { + if (cell.getData() == KakurasuType.EMPTY) { + element.setData(KakurasuType.FILLED); + } else { + element.setData(KakurasuType.UNKNOWN); + } + } + } + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuElementView.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuElementView.java new file mode 100644 index 000000000..b179908f3 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuElementView.java @@ -0,0 +1,57 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.ui.boardview.GridElementView; + +import java.awt.*; +import java.awt.geom.Rectangle2D; + +public class KakurasuElementView extends GridElementView { + public KakurasuElementView(KakurasuCell cell) { + super(cell); + } + + /** + * Draws on the given frame based on the type of the cell of the current puzzleElement + * + * @param graphics2D the frame to be drawn on + */ + @Override + public void drawElement(Graphics2D graphics2D) { + KakurasuCell cell = (KakurasuCell) puzzleElement; + KakurasuType type = cell.getType(); + graphics2D.setStroke(new BasicStroke(0)); + if (type == KakurasuType.UNKNOWN) { + graphics2D.setStroke(new BasicStroke(1)); + graphics2D.setColor(Color.LIGHT_GRAY); + graphics2D.fill( + new Rectangle2D.Double( + location.x + 0.5f, location.y + 0.5f, size.width - 1, size.height - 1)); + graphics2D.setColor(Color.BLACK); + graphics2D.draw( + new Rectangle2D.Double( + location.x + 0.5f, location.y + 0.5f, size.width - 1, size.height - 1)); + } else { + if (type == KakurasuType.FILLED) { + graphics2D.drawImage( + KakurasuView.FILLED, + location.x, + location.y, + size.width, + size.height, + null, + null); + } else { + graphics2D.drawImage( + KakurasuView.EMPTY, + location.x, + location.y, + size.width, + size.height, + null, + null); + } + graphics2D.setColor(Color.BLACK); + graphics2D.drawRect(location.x, location.y, size.width, size.height); + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuExporter.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuExporter.java new file mode 100644 index 000000000..e3c671a95 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuExporter.java @@ -0,0 +1,65 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.PuzzleExporter; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.w3c.dom.Document; + +public class KakurasuExporter extends PuzzleExporter { + + public KakurasuExporter(Kakurasu kakurasu) { + super(kakurasu); + } + + /** + * Creates and returns a new board element in the XML document specified + * + * @param newDocument the XML document to append to + * @return the new board element + */ + @Override + protected org.w3c.dom.Element createBoardElement(Document newDocument) { + KakurasuBoard board; + if (puzzle.getTree() != null) { + board = (KakurasuBoard) puzzle.getTree().getRootNode().getBoard(); + } else { + board = (KakurasuBoard) puzzle.getBoardView().getBoard(); + } + + org.w3c.dom.Element boardElement = newDocument.createElement("board"); + boardElement.setAttribute("width", String.valueOf(board.getWidth())); + boardElement.setAttribute("height", String.valueOf(board.getHeight())); + + org.w3c.dom.Element cellsElement = newDocument.createElement("cells"); + for (PuzzleElement puzzleElement : board.getPuzzleElements()) { + KakurasuCell cell = (KakurasuCell) puzzleElement; + if (cell.getData() != KakurasuType.UNKNOWN) { + org.w3c.dom.Element cellElement = + puzzle.getFactory().exportCell(newDocument, puzzleElement); + cellsElement.appendChild(cellElement); + } + } + boardElement.appendChild(cellsElement); + + org.w3c.dom.Element axisEast = newDocument.createElement("axis"); + axisEast.setAttribute("side", "east"); + for (KakurasuClue clue : board.getRowClues()) { + org.w3c.dom.Element clueElement = newDocument.createElement("clue"); + clueElement.setAttribute("value", String.valueOf(clue.getData())); + clueElement.setAttribute("index", String.valueOf(clue.getClueIndex())); + axisEast.appendChild(clueElement); + } + boardElement.appendChild(axisEast); + + org.w3c.dom.Element axisSouth = newDocument.createElement("axis"); + axisSouth.setAttribute("side", "south"); + for (KakurasuClue clue : board.getColClues()) { + org.w3c.dom.Element clueElement = newDocument.createElement("clue"); + clueElement.setAttribute("value", String.valueOf(clue.getData())); + clueElement.setAttribute("index", String.valueOf(clue.getClueIndex())); + axisSouth.appendChild(clueElement); + } + boardElement.appendChild(axisSouth); + + return boardElement; + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuImporter.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuImporter.java new file mode 100644 index 000000000..3188343ac --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuImporter.java @@ -0,0 +1,209 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.model.PuzzleImporter; +import edu.rpi.legup.save.InvalidFileFormatException; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class KakurasuImporter extends PuzzleImporter { + public KakurasuImporter(Kakurasu kakurasu) { + super(kakurasu); + } + + @Override + public boolean acceptsRowsAndColumnsInput() { + return true; + } + + @Override + public boolean acceptsTextInput() { + return false; + } + + /** + * Creates an empty board for building + * + * @param rows the number of rows on the board + * @param columns the number of columns on the board + * @throws RuntimeException if board can not be created + */ + @Override + public void initializeBoard(int rows, int columns) { + KakurasuBoard kakurasuBoard = new KakurasuBoard(columns, rows); + + for (int y = 0; y < rows; y++) { + for (int x = 0; x < columns; x++) { + if (kakurasuBoard.getCell(x, y) == null) { + KakurasuCell cell = new KakurasuCell(KakurasuType.UNKNOWN, new Point(x, y)); + cell.setIndex(y * columns + x); + cell.setModifiable(true); + kakurasuBoard.setCell(x, y, cell); + } + } + } + + for (int i = 0; i < rows; i++) { + kakurasuBoard.getRowClues().set(i, new KakurasuClue(0, i + 1, KakurasuType.CLUE_EAST)); + } + + for (int i = 0; i < columns; i++) { + kakurasuBoard.getColClues().set(i, new KakurasuClue(0, i + 1, KakurasuType.CLUE_SOUTH)); + } + + puzzle.setCurrentBoard(kakurasuBoard); + } + + /** + * Creates the board for building + * + * @param node xml document node + * @throws InvalidFileFormatException if file is invalid + */ + @Override + public void initializeBoard(Node node) throws InvalidFileFormatException { + try { + if (!node.getNodeName().equalsIgnoreCase("board")) { + throw new InvalidFileFormatException( + "Kakurasu Importer: cannot find board puzzleElement"); + } + Element boardElement = (Element) node; + if (boardElement.getElementsByTagName("cells").getLength() == 0) { + throw new InvalidFileFormatException( + "Kakurasu Importer: no puzzleElement found for board"); + } + Element dataElement = (Element) boardElement.getElementsByTagName("cells").item(0); + NodeList elementDataList = dataElement.getElementsByTagName("cell"); + + KakurasuBoard kakurasuBoard = null; + if (!boardElement.getAttribute("size").isEmpty()) { + int size = Integer.valueOf(boardElement.getAttribute("size")); + kakurasuBoard = new KakurasuBoard(size); + } else { + if (!boardElement.getAttribute("width").isEmpty() + && !boardElement.getAttribute("height").isEmpty()) { + int width = Integer.valueOf(boardElement.getAttribute("width")); + int height = Integer.valueOf(boardElement.getAttribute("height")); + kakurasuBoard = new KakurasuBoard(width, height); + } + } + + if (kakurasuBoard == null) { + throw new InvalidFileFormatException("Kakurasu Importer: invalid board dimensions"); + } + + int width = kakurasuBoard.getWidth(); + int height = kakurasuBoard.getHeight(); + + for (int i = 0; i < elementDataList.getLength(); i++) { + KakurasuCell cell = + (KakurasuCell) + puzzle.getFactory() + .importCell(elementDataList.item(i), kakurasuBoard); + Point loc = cell.getLocation(); + if (cell.getData() != KakurasuType.UNKNOWN) { + cell.setModifiable(false); + cell.setGiven(true); + } + kakurasuBoard.setCell(loc.x, loc.y, cell); + } + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (kakurasuBoard.getCell(x, y) == null) { + KakurasuCell cell = new KakurasuCell(KakurasuType.UNKNOWN, new Point(x, y)); + cell.setIndex(y * width + x); + cell.setModifiable(true); + kakurasuBoard.setCell(x, y, cell); + } + } + } + + NodeList axes = boardElement.getElementsByTagName("axis"); + if (axes.getLength() != 2) { + throw new InvalidFileFormatException("Kakurasu Importer: cannot find axes"); + } + + Element axis1 = (Element) axes.item(0); + Element axis2 = (Element) axes.item(1); + + if (!axis1.hasAttribute("side") || !axis1.hasAttribute("side")) { + throw new InvalidFileFormatException( + "Kakurasu Importer: side attribute of axis not specified"); + } + String side1 = axis1.getAttribute("side"); + String side2 = axis2.getAttribute("side"); + if (side1.equalsIgnoreCase(side2) + || !(side1.equalsIgnoreCase("east") || side1.equalsIgnoreCase("south")) + || !(side2.equalsIgnoreCase("east") || side2.equalsIgnoreCase("south"))) { + throw new InvalidFileFormatException( + "Kakurasu Importer: axes must be different and be {east | south}"); + } + NodeList eastClues = + side1.equalsIgnoreCase("east") + ? axis1.getElementsByTagName("clue") + : axis2.getElementsByTagName("clue"); + NodeList southClues = + side1.equalsIgnoreCase("south") + ? axis1.getElementsByTagName("clue") + : axis2.getElementsByTagName("clue"); + + if (eastClues.getLength() != kakurasuBoard.getHeight() + || southClues.getLength() != kakurasuBoard.getWidth()) { + throw new InvalidFileFormatException( + "Kakurasu Importer: there must be same number of clues as the dimension of" + + " the board"); + } + + for (int i = 0; i < eastClues.getLength(); i++) { + Element clue = (Element) eastClues.item(i); + int value = Integer.valueOf(clue.getAttribute("value")); + int index = Integer.valueOf(clue.getAttribute("index")); + + if (index - 1 < 0 || index - 1 > kakurasuBoard.getHeight()) { + throw new InvalidFileFormatException( + "Kakurasu Importer: clue index out of bounds"); + } + + if (kakurasuBoard.getRowClues().get(index - 1) != null) { + throw new InvalidFileFormatException("Kakurasu Importer: duplicate clue index"); + } + kakurasuBoard + .getRowClues() + .set(index - 1, new KakurasuClue(value, index, KakurasuType.CLUE_EAST)); + } + + for (int i = 0; i < southClues.getLength(); i++) { + Element clue = (Element) southClues.item(i); + int value = Integer.valueOf(clue.getAttribute("value")); + int index = Integer.valueOf(clue.getAttribute("index")); + + if (index - 1 < 0 || index - 1 > kakurasuBoard.getWidth()) { + throw new InvalidFileFormatException( + "Kakurasu Importer: clue index out of bounds"); + } + + if (kakurasuBoard.getColClues().get(index - 1) != null) { + throw new InvalidFileFormatException("Kakurasu Importer: duplicate clue index"); + } + kakurasuBoard + .getColClues() + .set(index - 1, new KakurasuClue(value, index, KakurasuType.CLUE_SOUTH)); + } + + puzzle.setCurrentBoard(kakurasuBoard); + } catch (NumberFormatException e) { + throw new InvalidFileFormatException( + "Kakurasu Importer: unknown value where integer expected"); + } + } + + @Override + public void initializeBoard(String[] statements) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Kakurasu cannot accept text input"); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuType.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuType.java new file mode 100644 index 000000000..4773fbcc0 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuType.java @@ -0,0 +1,19 @@ +package edu.rpi.legup.puzzle.kakurasu; + +public enum KakurasuType { + UNKNOWN, + FILLED, + EMPTY, + CLUE_NORTH, + CLUE_EAST, + CLUE_SOUTH, + CLUE_WEST; + + public static KakurasuType valueOf(int num) { + return switch (num) { + case 1 -> FILLED; + case 2 -> EMPTY; + default -> UNKNOWN; + }; + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuView.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuView.java new file mode 100644 index 000000000..ccd9efd7a --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/KakurasuView.java @@ -0,0 +1,201 @@ +package edu.rpi.legup.puzzle.kakurasu; + +import edu.rpi.legup.controller.BoardController; +import edu.rpi.legup.model.gameboard.CaseBoard; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.tree.TreeElement; +import edu.rpi.legup.ui.boardview.ElementView; +import edu.rpi.legup.ui.boardview.GridBoardView; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.io.IOException; +import java.util.ArrayList; + +public class KakurasuView extends GridBoardView { + private static final Logger LOGGER = LogManager.getLogger(KakurasuView.class.getName()); + + static Image FILLED, EMPTY, UNKNOWN; + + static { + try { + FILLED = + ImageIO.read( + ClassLoader.getSystemResourceAsStream( + "edu/rpi/legup/images/kakurasu/tiles/FilledTile.png")); + EMPTY = + ImageIO.read( + ClassLoader.getSystemResourceAsStream( + "edu/rpi/legup/images/kakurasu/tiles/EmptyTile.png")); + UNKNOWN = + ImageIO.read( + ClassLoader.getSystemResourceAsStream( + "edu/rpi/legup/images/kakurasu/tiles/UnknownTile.png")); + } catch (IOException e) { + LOGGER.error("Failed to open Kakurasu images"); + } + } + + private ArrayList northClues; + private ArrayList eastClues; + private ArrayList southClues; + private ArrayList westClues; + + public KakurasuView(KakurasuBoard board) { + super(new BoardController(), new KakurasuController(), board.getDimension()); + + this.northClues = new ArrayList<>(); + this.eastClues = new ArrayList<>(); + this.southClues = new ArrayList<>(); + this.westClues = new ArrayList<>(); + + for (PuzzleElement puzzleElement : board.getPuzzleElements()) { + KakurasuCell cell = (KakurasuCell) puzzleElement; + Point loc = cell.getLocation(); + KakurasuElementView elementView = new KakurasuElementView(cell); + elementView.setIndex(cell.getIndex()); + elementView.setSize(elementSize); + elementView.setLocation( + new Point((loc.x + 1) * elementSize.width, (loc.y + 1) * elementSize.height)); + elementViews.add(elementView); + } + + for (int i = 0; i < gridSize.height; i++) { + KakurasuClueView row = + new KakurasuClueView(new KakurasuClue(i, i, KakurasuType.CLUE_WEST)); + row.setLocation(new Point(0, (i + 1) * elementSize.height)); + row.setSize(elementSize); + + KakurasuClueView clue = new KakurasuClueView(board.getRowClues().get(i)); + clue.setLocation( + new Point( + (gridSize.width + 1) * elementSize.width, + (i + 1) * elementSize.height)); + clue.setSize(elementSize); + + westClues.add(row); + eastClues.add(clue); + } + + for (int i = 0; i < gridSize.width; i++) { + KakurasuClueView col = + new KakurasuClueView(new KakurasuClue(i, i, KakurasuType.CLUE_NORTH)); + col.setLocation(new Point((i + 1) * elementSize.width, 0)); + col.setSize(elementSize); + + KakurasuClueView clue = new KakurasuClueView(board.getColClues().get(i)); + clue.setLocation( + new Point( + (i + 1) * elementSize.width, + (gridSize.height + 1) * elementSize.height)); + clue.setSize(elementSize); + + northClues.add(col); + southClues.add(clue); + } + } + + /** + * Gets the ElementView from the location specified or null if one does not exists at that + * location + * + * @param point location on the viewport + * @return ElementView at the specified location + */ + @Override + public ElementView getElement(Point point) { + Point scaledPoint = + new Point( + (int) Math.round(point.x / getScale()), + (int) Math.round(point.y / getScale())); + for (ElementView element : elementViews) { + if (element.isWithinBounds(scaledPoint)) { + return element; + } + } + for (KakurasuClueView clueView : northClues) { + if (clueView.isWithinBounds(scaledPoint)) { + return clueView; + } + } + for (KakurasuClueView clueView : eastClues) { + if (clueView.isWithinBounds(scaledPoint)) { + return clueView; + } + } + for (KakurasuClueView clueView : southClues) { + if (clueView.isWithinBounds(scaledPoint)) { + return clueView; + } + } + for (KakurasuClueView clueView : westClues) { + if (clueView.isWithinBounds(scaledPoint)) { + return clueView; + } + } + return null; + } + + public ArrayList getNorthClues() { + return northClues; + } + + public ArrayList getEastClues() { + return eastClues; + } + + public ArrayList getSouthClues() { + return southClues; + } + + public ArrayList getWestClues() { + return westClues; + } + + @Override + protected Dimension getProperSize() { + Dimension boardViewSize = new Dimension(); + boardViewSize.width = (gridSize.width + 2) * elementSize.width; + boardViewSize.height = (gridSize.height + 2) * elementSize.height; + return boardViewSize; + } + + /** + * Called when the tree element has changed. + * + * @param treeElement tree element + */ + @Override + public void onTreeElementChanged(TreeElement treeElement) { + super.onTreeElementChanged(treeElement); + KakurasuBoard kakurasuBoard; + if (board instanceof CaseBoard) { + kakurasuBoard = (KakurasuBoard) ((CaseBoard) board).getBaseBoard(); + } else { + kakurasuBoard = (KakurasuBoard) board; + } + } + + @Override + public void drawBoard(Graphics2D graphics2D) { + super.drawBoard(graphics2D); + + for (KakurasuClueView clueView : northClues) { + clueView.draw(graphics2D); + } + + for (KakurasuClueView clueView : eastClues) { + clueView.draw(graphics2D); + } + + for (KakurasuClueView clueView : southClues) { + clueView.draw(graphics2D); + } + + for (KakurasuClueView clueView : westClues) { + clueView.draw(graphics2D); + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/ClueTile.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/ClueTile.java new file mode 100644 index 000000000..560c442e1 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/ClueTile.java @@ -0,0 +1,12 @@ +package edu.rpi.legup.puzzle.kakurasu.elements; + +import edu.rpi.legup.model.elements.PlaceableElement; + +public class ClueTile extends PlaceableElement { + public ClueTile() { + super("KAKU-ELEM-0004", + "Clue Tile", + "Kakurasu clue tile", + "edu/rpi/legup/images/kakurasu/tiles/ClueTile.png"); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/EmptyTile.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/EmptyTile.java new file mode 100644 index 000000000..0aaacdcd9 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/EmptyTile.java @@ -0,0 +1,12 @@ +package edu.rpi.legup.puzzle.kakurasu.elements; + +import edu.rpi.legup.model.elements.PlaceableElement; + +public class EmptyTile extends PlaceableElement { + public EmptyTile() { + super("KAKU-ELEM-0002", + "Empty Tile", + "Kakurasu empty tile", + "edu/rpi/legup/images/kakurasu/tiles/EmptyTile.png"); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/FilledTile.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/FilledTile.java new file mode 100644 index 000000000..6b0685879 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/FilledTile.java @@ -0,0 +1,12 @@ +package edu.rpi.legup.puzzle.kakurasu.elements; + +import edu.rpi.legup.model.elements.PlaceableElement; + +public class FilledTile extends PlaceableElement { + public FilledTile() { + super("KAKU-ELEM-0001", + "Filled Tile", + "Kakurasu filled tile", + "edu/rpi/legup/images/kakurasu/tiles/FilledTile.png"); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/UnknownTile.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/UnknownTile.java new file mode 100644 index 000000000..dc678aa72 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/UnknownTile.java @@ -0,0 +1,12 @@ +package edu.rpi.legup.puzzle.kakurasu.elements; + +import edu.rpi.legup.model.elements.PlaceableElement; + +public class UnknownTile extends PlaceableElement { + public UnknownTile() { + super("KAKU-ELEM-0003", + "Unknown Tile", + "Kakurasu unknown tile", + "edu/rpi/legup/images/kakurasu/tiles/UnknownTile.png"); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/kakurasu_elements_reference_sheet.txt b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/kakurasu_elements_reference_sheet.txt new file mode 100644 index 000000000..d3ba4ff3c --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/elements/kakurasu_elements_reference_sheet.txt @@ -0,0 +1,4 @@ +KAKU-ELEM-0001 : FilledTile +KAKU-ELEM-0002 : EmptyTile +KAKU-ELEM-0003 : UnknownTile +KAKU-ELEM-0004 : ClueTile \ No newline at end of file diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/ExceededSumContradictionRule.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/ExceededSumContradictionRule.java new file mode 100644 index 000000000..7dd2f0faa --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/ExceededSumContradictionRule.java @@ -0,0 +1,60 @@ +package edu.rpi.legup.puzzle.kakurasu.rules; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.ContradictionRule; +import edu.rpi.legup.model.tree.TreeNode; +import edu.rpi.legup.model.tree.TreeTransition; +import edu.rpi.legup.puzzle.kakurasu.KakurasuBoard; +import edu.rpi.legup.puzzle.kakurasu.KakurasuCell; +import edu.rpi.legup.puzzle.kakurasu.KakurasuType; +import edu.rpi.legup.puzzle.treetent.TreeTentBoard; +import edu.rpi.legup.puzzle.treetent.TreeTentCell; +import edu.rpi.legup.puzzle.treetent.TreeTentType; + +import java.awt.*; +import java.util.List; + +public class ExceededSumContradictionRule extends ContradictionRule { + public ExceededSumContradictionRule() { + super( + "KAKU-CONT-0001", + "Exceeded Sum", + "The sum of this row or column exceeds one of the clues.", + "edu/rpi/legup/images/kakurasu/temp.png"); + } + + /** + * Checks whether the transition has a contradiction at the specific puzzleElement index using + * this rule + * + * @param board board to check contradiction + * @param puzzleElement equivalent puzzleElement + * @return null if the transition contains a contradiction at the specified puzzleElement, + * otherwise error message + */ + @Override + public String checkContradictionAt(Board board, PuzzleElement puzzleElement) { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board; + KakurasuCell cell = (KakurasuCell) puzzleElement; + + Point loc = cell.getLocation(); + List filledRow = kakurasuBoard.getRowCol(loc.y, KakurasuType.FILLED, true); + List filledCol = kakurasuBoard.getRowCol(loc.x, KakurasuType.FILLED, false); + int rowSum = 0; + for(KakurasuCell kc : filledRow) { + rowSum += kc.getLocation().x + 1; + } + int colSum = 0; + for(KakurasuCell kc : filledCol) { + colSum += kc.getLocation().y + 1; + } + + if (rowSum > kakurasuBoard.getClue(kakurasuBoard.getWidth(), loc.y).getData() + || colSum > kakurasuBoard.getClue(loc.x, kakurasuBoard.getHeight()).getData()) { + return null; + } else { + return super.getNoContradictionMessage(); + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/FilledOrEmptyCaseRule.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/FilledOrEmptyCaseRule.java new file mode 100644 index 000000000..3780cfd7d --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/FilledOrEmptyCaseRule.java @@ -0,0 +1,114 @@ +package edu.rpi.legup.puzzle.kakurasu.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.kakurasu.KakurasuBoard; +import edu.rpi.legup.puzzle.kakurasu.KakurasuCell; +import edu.rpi.legup.puzzle.kakurasu.KakurasuType; + +import java.util.ArrayList; +import java.util.List; + +public class FilledOrEmptyCaseRule extends CaseRule { + + public FilledOrEmptyCaseRule() { + super( + "KAKU-CASE-0001", + "Filled or Empty", + "Each blank cell is either filled or empty.", + "edu/rpi/legup/images/kakurasu/temp.png"); + } + + @Override + public CaseBoard getCaseBoard(Board board) { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board.copy(); + kakurasuBoard.setModifiable(false); + CaseBoard caseBoard = new CaseBoard(kakurasuBoard, this); + for (PuzzleElement element : kakurasuBoard.getPuzzleElements()) { + if (((KakurasuCell) element).getType() == KakurasuType.UNKNOWN) { + caseBoard.addPickableElement(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 + */ + @SuppressWarnings("unchecked") + @Override + public ArrayList getCases(Board board, PuzzleElement puzzleElement) { + ArrayList cases = new ArrayList<>(); + Board case1 = board.copy(); + PuzzleElement data1 = case1.getPuzzleElement(puzzleElement); + data1.setData(KakurasuType.FILLED); + case1.addModifiedData(data1); + cases.add(case1); + + Board case2 = board.copy(); + PuzzleElement data2 = case2.getPuzzleElement(puzzleElement); + data2.setData(KakurasuType.EMPTY); + case2.addModifiedData(data2); + cases.add(case2); + + return cases; + } + + /** + * Checks whether the transition logically follows from the parent node using this rule + * + * @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) { + List childTransitions = transition.getParents().get(0).getChildren(); + if (childTransitions.size() != 2) { + return super.getInvalidUseOfRuleMessage() + ": This case rule must have 2 children."; + } + + TreeTransition case1 = childTransitions.get(0); + TreeTransition case2 = childTransitions.get(1); + if (case1.getBoard().getModifiedData().size() != 1 + || case2.getBoard().getModifiedData().size() != 1) { + return super.getInvalidUseOfRuleMessage() + + ": This case rule must have 1 modified cell for each case."; + } + + KakurasuCell mod1 = (KakurasuCell) case1.getBoard().getModifiedData().iterator().next(); + KakurasuCell mod2 = (KakurasuCell) case2.getBoard().getModifiedData().iterator().next(); + if (!mod1.getLocation().equals(mod2.getLocation())) { + return super.getInvalidUseOfRuleMessage() + + ": This case rule must modify the same cell for each case."; + } + + if (!((mod1.getType() == KakurasuType.FILLED && mod2.getType() == KakurasuType.EMPTY) + || (mod2.getType() == KakurasuType.FILLED && mod1.getType() == KakurasuType.EMPTY))) { + return super.getInvalidUseOfRuleMessage() + + ": This case rule must have a filled and an empty cell."; + } + + return null; + } + + /** + * 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 checkRuleRaw(transition); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/IncompleteSumContradictionRule.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/IncompleteSumContradictionRule.java new file mode 100644 index 000000000..eb0911d11 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/IncompleteSumContradictionRule.java @@ -0,0 +1,64 @@ +package edu.rpi.legup.puzzle.kakurasu.rules; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.ContradictionRule; +import edu.rpi.legup.puzzle.kakurasu.KakurasuBoard; +import edu.rpi.legup.puzzle.kakurasu.KakurasuCell; +import edu.rpi.legup.puzzle.kakurasu.KakurasuType; + +import java.awt.*; +import java.util.List; + +public class IncompleteSumContradictionRule extends ContradictionRule { + public IncompleteSumContradictionRule() { + super( + "KAKU-CONT-0002", + "Incomplete Sum", + "The sum of this row or column cannot fulfill one of the clues.", + "edu/rpi/legup/images/kakurasu/temp.png"); + } + + /** + * Checks whether the transition has a contradiction at the specific puzzleElement index using + * this rule + * + * @param board board to check contradiction + * @param puzzleElement equivalent puzzleElement + * @return null if the transition contains a contradiction at the specified puzzleElement, + * otherwise error message + */ + @Override + public String checkContradictionAt(Board board, PuzzleElement puzzleElement) { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board; + KakurasuCell cell = (KakurasuCell) puzzleElement; + + Point loc = cell.getLocation(); + List filledRow = kakurasuBoard.getRowCol(loc.y, KakurasuType.FILLED, true); + List unknownRow = kakurasuBoard.getRowCol(loc.y, KakurasuType.UNKNOWN, true); + List filledCol = kakurasuBoard.getRowCol(loc.x, KakurasuType.FILLED, false); + List unknownCol = kakurasuBoard.getRowCol(loc.x, KakurasuType.UNKNOWN, false); + + int rowSum = 0; + for(KakurasuCell kc : filledRow) { + rowSum += kc.getLocation().x + 1; + } + for(KakurasuCell kc : unknownRow) { + rowSum += kc.getLocation().x + 1; + } + int colSum = 0; + for(KakurasuCell kc : filledCol) { + colSum += kc.getLocation().y + 1; + } + for(KakurasuCell kc : unknownCol) { + colSum += kc.getLocation().y + 1; + } + + if (rowSum < kakurasuBoard.getClue(kakurasuBoard.getWidth(), loc.y).getData() + || colSum < kakurasuBoard.getClue(loc.x, kakurasuBoard.getHeight()).getData()) { + return null; + } else { + return super.getNoContradictionMessage(); + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/RequiredEmptyDirectRule.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/RequiredEmptyDirectRule.java new file mode 100644 index 000000000..df6264890 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/RequiredEmptyDirectRule.java @@ -0,0 +1,104 @@ +package edu.rpi.legup.puzzle.kakurasu.rules; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.DirectRule; +import edu.rpi.legup.model.tree.TreeNode; +import edu.rpi.legup.model.tree.TreeTransition; +import edu.rpi.legup.puzzle.kakurasu.KakurasuBoard; +import edu.rpi.legup.puzzle.kakurasu.KakurasuCell; +import edu.rpi.legup.puzzle.kakurasu.KakurasuType; + +import java.awt.*; +import java.util.List; + +public class RequiredEmptyDirectRule extends DirectRule { + public RequiredEmptyDirectRule() { + super( + "KAKU-BASC-0002", + "Required Empty", + "The only way to satisfy the clue in a row or column are these empty tiles.", + "edu/rpi/legup/images/kakurasu/temp.png"); + } + + /** + * 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) { + KakurasuBoard initialBoard = (KakurasuBoard) transition.getParents().get(0).getBoard(); + KakurasuCell initCell = (KakurasuCell) initialBoard.getPuzzleElement(puzzleElement); + KakurasuBoard finalBoard = (KakurasuBoard) transition.getBoard(); + KakurasuCell finalCell = (KakurasuCell) finalBoard.getPuzzleElement(puzzleElement); + if (!(finalCell.getType() == KakurasuType.EMPTY + && initCell.getType() == KakurasuType.UNKNOWN)) { + return super.getInvalidUseOfRuleMessage() + ": This cell must be empty to apply this rule."; + } + + if (isForced(initialBoard, initCell)) { + return null; + } else { + return super.getInvalidUseOfRuleMessage() + ": This cell is not forced to be empty."; + } + } + + /** + * Is used to determine if the cell being passed in is forced to exist in the + * position it is in, given the board that was passed in + * + * @param board board to check the cell against + * @param cell the cell whose legitimacy is in question + * @return if the cell is forced to be at its position on the board + */ + private boolean isForced(KakurasuBoard board, KakurasuCell cell) { + Point loc = cell.getLocation(); + List filledRow = board.getRowCol(loc.y, KakurasuType.FILLED, true); + List filledCol = board.getRowCol(loc.x, KakurasuType.FILLED, false); + + // Check if the remaining locations available must be filled to fulfill the clue value + int rowSum = 0; + for(KakurasuCell kc : filledRow) { + rowSum += kc.getLocation().x + 1; + } + // If setting this cell to filled causes the clue to fail, this cell must be empty + if(rowSum + loc.x + 1 > board.getClue(board.getWidth(), loc.y).getData()) return true; + + int colSum = 0; + for(KakurasuCell kc : filledCol) { + colSum += kc.getLocation().y + 1; + } + // Return true if the clue is exceeded if this cell is filled, + // Return false if setting the cell to filled keeps the col total to under the clue value + return (colSum + loc.y + 1> board.getClue(loc.x, board.getHeight()).getData()); + } + + /** + * Creates a transition {@link Board} that has this rule applied to it using the {@link + * TreeNode}. + * + * @param node tree node used to create default transition board + * @return default board or null if this rule cannot be applied to this tree node + */ + @Override + public Board getDefaultBoard(TreeNode node) { + KakurasuBoard KakurasuBoard = (KakurasuBoard) node.getBoard().copy(); + for (PuzzleElement element : KakurasuBoard.getPuzzleElements()) { + KakurasuCell cell = (KakurasuCell) element; + if (cell.getType() == KakurasuType.UNKNOWN && isForced(KakurasuBoard, cell)) { + cell.setData(KakurasuType.EMPTY); + KakurasuBoard.addModifiedData(cell); + } + } + if (KakurasuBoard.getModifiedData().isEmpty()) { + return null; + } else { + return KakurasuBoard; + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/RequiredFilledDirectRule.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/RequiredFilledDirectRule.java new file mode 100644 index 000000000..6aa952085 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/RequiredFilledDirectRule.java @@ -0,0 +1,136 @@ +package edu.rpi.legup.puzzle.kakurasu.rules; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.DirectRule; +import edu.rpi.legup.model.tree.TreeNode; +import edu.rpi.legup.model.tree.TreeTransition; +import edu.rpi.legup.puzzle.kakurasu.KakurasuBoard; +import edu.rpi.legup.puzzle.kakurasu.KakurasuCell; +import edu.rpi.legup.puzzle.kakurasu.KakurasuType; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class RequiredFilledDirectRule extends DirectRule { + public RequiredFilledDirectRule() { + super( + "KAKU-BASC-0001", + "Required Filled", + "The only way to satisfy the clue in a row or column are these filled tiles.", + "edu/rpi/legup/images/kakurasu/temp.png"); + } + + /** + * 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) { + KakurasuBoard initialBoard = (KakurasuBoard) transition.getParents().get(0).getBoard(); + KakurasuCell initCell = (KakurasuCell) initialBoard.getPuzzleElement(puzzleElement); + KakurasuBoard finalBoard = (KakurasuBoard) transition.getBoard(); + KakurasuCell finalCell = (KakurasuCell) finalBoard.getPuzzleElement(puzzleElement); + if (!(finalCell.getType() == KakurasuType.FILLED + && initCell.getType() == KakurasuType.UNKNOWN)) { + return super.getInvalidUseOfRuleMessage() + ": This cell must be filled to apply this rule."; + } + + if (isForced(initialBoard, initCell)) { + return null; + } else { + return super.getInvalidUseOfRuleMessage() + ": This cell is not forced to be filled."; + } + } + + /** + * Is used to determine if the cell being passed in is forced to exist in the + * position it is in, given the board that was passed in + * + * @param board board to check the cell against + * @param cell the cell whose legitimacy is in question + * @return if the cell is forced to be at its position on the board + */ + private boolean isForced(KakurasuBoard board, KakurasuCell cell) { + // TODO: Fix this so it doesn't only work if all are filled + Point loc = cell.getLocation(); + List filledRow = board.getRowCol(loc.y, KakurasuType.FILLED, true); + List unknownRow = board.getRowCol(loc.y, KakurasuType.UNKNOWN, true); + List filledCol = board.getRowCol(loc.x, KakurasuType.FILLED, false); + List unknownCol = board.getRowCol(loc.x, KakurasuType.UNKNOWN, false); + + // Check if the remaining locations available must be filled to fulfill the clue value + int rowValueRemaining = board.getClue(board.getWidth(), loc.y).getData(); + for(KakurasuCell kc : filledRow) { + rowValueRemaining -= kc.getLocation().x + 1; + } + ArrayList rowValues = new ArrayList<>(); + // Add all the unknown row values to the Arraylist except for the one being checked by the function + for(KakurasuCell kc : unknownRow) { + if(kc.getLocation() != loc) rowValues.add(kc.getLocation().x + 1); + } + // If the clue is not reachable without the current cell being filled, but is possible with it filled, + // then that means the current cell is a required fill on this board + if(!isReachable(rowValueRemaining, 0, rowValues) && + isReachable(rowValueRemaining-(loc.x+1), 0, rowValues)) return true; + + int colValueRemaining = board.getClue(loc.x, board.getHeight()).getData(); + for(KakurasuCell kc : filledCol) { + colValueRemaining -= kc.getLocation().y + 1; + } + ArrayList colValues = new ArrayList<>(); + // Add all the unknown col values to the Arraylist except for the one being checked by the function + for(KakurasuCell kc : unknownCol) { + if(kc.getLocation() != loc) colValues.add(kc.getLocation().y + 1); + } + // Return true if the clue is fulfilled, false if it isn't + return (!isReachable(colValueRemaining, 0, rowValues) && + isReachable(colValueRemaining-(loc.y+1), 0, colValues)); + } + + /** + * Helper function that checks if the target clue is reachable given a list of KakurasuCells + * This function only works if the list of values are given in increasing index order (which it currently is) + * + * @param target The integer that we are trying to add up to, given the values + * @param currentIndex The index of the next value that we are considering + * @param values Values that we are given to try to sum up to the target + * @return If it's possible to sum the values in a way to get the target value + */ + private boolean isReachable(int target, int currentIndex, ArrayList values) { + if(target == 0) return true; + if(target < 0 || currentIndex >= values.size()) return false; + return (isReachable(target, currentIndex+1, values) || + isReachable(target - values.get(currentIndex), currentIndex+1, values)); + } + + /** + * Creates a transition {@link Board} that has this rule applied to it using the {@link + * TreeNode}. + * + * @param node tree node used to create default transition board + * @return default board or null if this rule cannot be applied to this tree node + */ + @Override + public Board getDefaultBoard(TreeNode node) { + KakurasuBoard KakurasuBoard = (KakurasuBoard) node.getBoard().copy(); + for (PuzzleElement element : KakurasuBoard.getPuzzleElements()) { + KakurasuCell cell = (KakurasuCell) element; + if (cell.getType() == KakurasuType.UNKNOWN && isForced(KakurasuBoard, cell)) { + cell.setData(KakurasuType.FILLED); + KakurasuBoard.addModifiedData(cell); + } + } + if (KakurasuBoard.getModifiedData().isEmpty()) { + return null; + } else { + return KakurasuBoard; + } + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/UnreachableSumContradictionRule.java b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/UnreachableSumContradictionRule.java new file mode 100644 index 000000000..9096fc189 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/UnreachableSumContradictionRule.java @@ -0,0 +1,106 @@ +package edu.rpi.legup.puzzle.kakurasu.rules; + +import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.gameboard.PuzzleElement; +import edu.rpi.legup.model.rules.ContradictionRule; +import edu.rpi.legup.puzzle.kakurasu.KakurasuBoard; +import edu.rpi.legup.puzzle.kakurasu.KakurasuCell; +import edu.rpi.legup.puzzle.kakurasu.KakurasuType; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class UnreachableSumContradictionRule extends ContradictionRule { + public UnreachableSumContradictionRule() { + super( + "KAKU-CONT-0003", + "Unreachable Sum", + "The combination of available values cannot exactly land on the clue's value.", + "edu/rpi/legup/images/kakurasu/temp.png"); + } + + /** + * Checks whether the transition has a contradiction at the specific puzzleElement index using + * this rule + * + * @param board board to check contradiction + * @param puzzleElement equivalent puzzleElement + * @return null if the transition contains a contradiction at the specified puzzleElement, + * otherwise error message + */ + @Override + public String checkContradictionAt(Board board, PuzzleElement puzzleElement) { + KakurasuBoard kakurasuBoard = (KakurasuBoard) board; + KakurasuCell cell = (KakurasuCell) puzzleElement; + + Point loc = cell.getLocation(); + List filledRow = kakurasuBoard.getRowCol(loc.y, KakurasuType.FILLED, true); + List unknownRow = kakurasuBoard.getRowCol(loc.y, KakurasuType.UNKNOWN, true); + List filledCol = kakurasuBoard.getRowCol(loc.x, KakurasuType.FILLED, false); + List unknownCol = kakurasuBoard.getRowCol(loc.x, KakurasuType.UNKNOWN, false); + + int rowValueRemaining = kakurasuBoard.getClue(kakurasuBoard.getWidth(), loc.y).getData(); + for(KakurasuCell kc : filledRow) { + rowValueRemaining -= kc.getLocation().x + 1; + } + int colValueRemaining = kakurasuBoard.getClue(loc.x, kakurasuBoard.getHeight()).getData(); + for(KakurasuCell kc : filledCol) { + colValueRemaining -= kc.getLocation().y + 1; + } + + // If the value for either the row or col is already exceeded, this is the wrong rule to call. + if(rowValueRemaining < 0 || colValueRemaining < 0) return super.getNoContradictionMessage(); + + // If either value is already 0, then it is already possible to fulfill + // If it isn't 0, then it's possible for the remaining values to not be able to fulfill it + boolean rowPossible = (rowValueRemaining==0), colPossible = (colValueRemaining==0); + + int rowTotal = 0, colTotal = 0; + // No need to sort the values as the KakurasuCells are given in increasing index order + if(!rowPossible) { + ArrayList rowValues = new ArrayList<>(); + for(KakurasuCell kc : unknownRow) { + rowValues.add(kc.getLocation().x + 1); + rowTotal += kc.getLocation().x + 1; + } + // If the remaining unknown cells' values is less than the remaining clue value, + // this requires the usage of a different contradiction rule, not this one. + if(rowTotal < rowValueRemaining) return super.getNoContradictionMessage(); + rowPossible = isReachable(rowValueRemaining, 0, rowValues); + } + if(!colPossible) { + ArrayList colValues = new ArrayList<>(); + for(KakurasuCell kc : unknownCol) { + colValues.add(kc.getLocation().y + 1); + colTotal += kc.getLocation().y + 1; + } + // If the remaining unknown cells' values is less than the remaining clue value, + // this requires the usage of a different contradiction rule, not this one. + if(colTotal < colValueRemaining) return super.getNoContradictionMessage(); + colPossible = isReachable(colValueRemaining, 0, colValues); + } + + if (!rowPossible || !colPossible) { + return null; + } else { + return super.getNoContradictionMessage(); + } + } + + /** + * Helper function that checks if the target clue is reachable given a list of KakurasuCells + * This function only works if the list of values are given in increasing index order (which it currently is) + * + * @param target The integer that we are trying to add up to, given the values + * @param currentIndex The index of the next value that we are considering + * @param values Values that we are given to try to sum up to the target + * @return If it's possible to sum the values in a way to get the target value + */ + private boolean isReachable(int target, int currentIndex, ArrayList values) { + if(target == 0) return true; + if(target < 0 || currentIndex >= values.size()) return false; + return (isReachable(target, currentIndex+1, values) || + isReachable(target - values.get(currentIndex), currentIndex+1, values)); + } +} diff --git a/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/kakurasu_reference_sheet.txt b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/kakurasu_reference_sheet.txt new file mode 100644 index 000000000..8dfccc616 --- /dev/null +++ b/src/main/java/edu/rpi/legup/puzzle/kakurasu/rules/kakurasu_reference_sheet.txt @@ -0,0 +1,8 @@ +KAKU-BASC-0001 : RequiredFilledDirectRule +KAKU-BASC-0002 : RequiredEmptyDirectRule + +KAKU-CONT-0001 : ExceededSumContradictionRule +KAKU-CONT-0002 : IncompleteSumContradictionRule +KAKU-CONT-0003 : UnreachableSumContradictionRule + +KAKU-CASE-0001 : FilledOrEmptyCaseRule \ No newline at end of file diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/rulesview/RulePanel.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/rulesview/RulePanel.java index 4c9ebf882..700e9c75e 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/rulesview/RulePanel.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/rulesview/RulePanel.java @@ -348,9 +348,7 @@ public ImageIcon getIcon() { } /** - * Sets the icon for this panel - * - * @return the ImageIcon associated with this panel + * Sets the ImageIcon associated with this panel */ public void setIcon(ImageIcon icon) { this.icon = icon; diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java index 228e69950..bddc5f876 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java @@ -141,7 +141,7 @@ public void setHover(boolean isHovered) { } /** - * Gets the visibility of the tree puzzleElement. Tells the TreeView whether or not to draw the + * Gets the visibility of the tree puzzleElement. Tells the TreeView whether to draw the * tree puzzleElement * * @return visibility of the tree puzzleElement diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java index 0e2a31bbf..578f08b4d 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java @@ -61,17 +61,20 @@ public TreeNodeView(TreeNode treeNode) { } /** - * Draws the TreeNodeView - * + * Draws the TreeNodeView as long as isVisible() is true + * and there is TreeNode attached to this TreeNodeView * @param graphics2D graphics2D used for drawing */ public void draw(Graphics2D graphics2D) { if (isVisible() && treeElement != null) { + // If the logical statement correctly leads to a proof by contradiction, + // draw the X that marks the end of this logical sequence. if (getTreeElement().getParent() != null && getTreeElement().getParent().isJustified() && getTreeElement().getParent().getRule().getRuleType() == RuleType.CONTRADICTION) { isContradictoryState = true; + // Draw two lines that make up the X with the contradiction color graphics2D.setColor(NODE_COLOR_CONTRADICTION); graphics2D.drawLine( location.x - RADIUS, @@ -84,11 +87,14 @@ && getTreeElement().getParent().getRule().getRuleType() location.x - RADIUS, location.y + RADIUS); } else { + // Else the node being drawn is not a contradiction isContradictoryState = false; graphics2D.setStroke(MAIN_STROKE); boolean isContraBranch = getTreeElement().isContradictoryBranch(); if (isSelected) { + // If the TreeNode is selected, draw it on the TreePanel with specified colors, + // outline, and special outline for selected nodes graphics2D.setColor(SELECTION_COLOR); graphics2D.fillOval( location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); @@ -105,7 +111,10 @@ && getTreeElement().getParent().getRule().getRuleType() DIAMETER + 8, DIAMETER + 8); } else { + // Else the current node is not being selected if (isHover) { + // Checks if the current Node is being hovered over. + // If it is, then draw the Node with specified properties graphics2D.setColor(HOVER_COLOR); graphics2D.fillOval( location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); @@ -122,6 +131,8 @@ && getTreeElement().getParent().getRule().getRuleType() DIAMETER + 8, DIAMETER + 8); } else { + // Otherwise, this is a normal Node that isn't a contradiction, selected, or hovered Node + // Set color to contradiction color if this Node leads to a contradiction Node, or default color otherwise graphics2D.setColor( isContraBranch ? NODE_COLOR_CONTRADICTION : NODE_COLOR_DEFAULT); graphics2D.fillOval( @@ -215,6 +226,10 @@ public Point getLocation() { /** * Sets the location of the tree node * + * This function is never used; the only call that sets the location for where the Node will be drawn + * is currently implemented in edu.rpi.legup.ui.proofeditorui.treeview.TreeView, which uses the setX() + * and setY() functions to update location. (As of October 22, 2024) + * * @param location location of the tree node */ public void setLocation(Point location) { diff --git a/src/main/java/edu/rpi/legup/utility/Logger.java b/src/main/java/edu/rpi/legup/utility/Logger.java index 67048e5b4..244161442 100644 --- a/src/main/java/edu/rpi/legup/utility/Logger.java +++ b/src/main/java/edu/rpi/legup/utility/Logger.java @@ -10,6 +10,11 @@ import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.layout.PatternLayout; +/** + * {@code Logger} is a class that exists only to initialize the imported logger objects + * from apache, and to initialize the files that record the action history. + */ + public class Logger { private static final String LEGUP_HOME = diff --git a/src/main/resources/edu/rpi/legup/images/kakurasu/temp.png b/src/main/resources/edu/rpi/legup/images/kakurasu/temp.png new file mode 100644 index 000000000..850fbf127 Binary files /dev/null and b/src/main/resources/edu/rpi/legup/images/kakurasu/temp.png differ diff --git a/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/ClueTile.png b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/ClueTile.png new file mode 100644 index 000000000..7a86827f9 Binary files /dev/null and b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/ClueTile.png differ diff --git a/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/EmptyTile.png b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/EmptyTile.png new file mode 100644 index 000000000..fc2c683eb Binary files /dev/null and b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/EmptyTile.png differ diff --git a/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/FilledTile.png b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/FilledTile.png new file mode 100644 index 000000000..93e169df8 Binary files /dev/null and b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/FilledTile.png differ diff --git a/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/UnknownTile.png b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/UnknownTile.png new file mode 100644 index 000000000..850fbf127 Binary files /dev/null and b/src/main/resources/edu/rpi/legup/images/kakurasu/tiles/UnknownTile.png differ diff --git a/src/main/resources/edu/rpi/legup/legup/config b/src/main/resources/edu/rpi/legup/legup/config index e01767677..09e27a43f 100644 --- a/src/main/resources/edu/rpi/legup/legup/config +++ b/src/main/resources/edu/rpi/legup/legup/config @@ -51,5 +51,9 @@ qualifiedClassName="edu.rpi.legup.puzzle.minesweeper.Minesweeper" fileType=".xml" fileCreationDisabled="true"/> + diff --git a/src/test/java/puzzles/lightup/rules/TooFewBulbsContradictionRuleTest.java b/src/test/java/puzzles/lightup/rules/TooFewBulbsContradictionRuleTest.java index fe994baa6..06090a796 100644 --- a/src/test/java/puzzles/lightup/rules/TooFewBulbsContradictionRuleTest.java +++ b/src/test/java/puzzles/lightup/rules/TooFewBulbsContradictionRuleTest.java @@ -21,7 +21,7 @@ public static void setUp() { } @Test - public void TooFewBulbsContradictionRule() throws InvalidFileFormatException { + public void FullTooFewTest() throws InvalidFileFormatException { TestUtilities.importTestBoard( "puzzles/lightup/rules/TooFewBulbsContradictionRule/FullTooFewTest", lightUp); TreeNode rootNode = lightUp.getTree().getRootNode(); diff --git a/src/test/java/puzzles/lightup/rules/TooManyBulbsContradictionRuleTest.java b/src/test/java/puzzles/lightup/rules/TooManyBulbsContradictionRuleTest.java index e27fa3323..63557d3d1 100644 --- a/src/test/java/puzzles/lightup/rules/TooManyBulbsContradictionRuleTest.java +++ b/src/test/java/puzzles/lightup/rules/TooManyBulbsContradictionRuleTest.java @@ -22,7 +22,7 @@ public static void setUp() { @Test // complex extensive toofew test - public void TooFewBulbsContradictionRule() throws InvalidFileFormatException { + public void FullTooManyTest() throws InvalidFileFormatException { TestUtilities.importTestBoard( "puzzles/lightup/rules/TooManyBulbsContradictionRule/FullTooManyTest", lightUp); TreeNode rootNode = lightUp.getTree().getRootNode();