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();