The SudokuBoard record contains the unsolved grid, solved grid, and the difficulty level of + * the Sudoku board. + */ +public record SudokuBoard( + byte[][] unsolvedGrid, byte[][] solvedGrid, DifficultyLevel difficultyLevel) { + + /** + * Constructs a SudokuBoard with the specified unsolved grid, solved grid, and difficulty level. + * + * @param unsolvedGrid The unsolved grid of the Sudoku board. + * @param solvedGrid The solved grid of the Sudoku board. + * @param difficultyLevel The difficulty level of the Sudoku board. + */ + public SudokuBoard( + final byte[][] unsolvedGrid, + final byte[][] solvedGrid, + final DifficultyLevel difficultyLevel) { + this.unsolvedGrid = requireNonNull(unsolvedGrid); + this.solvedGrid = requireNonNull(solvedGrid); + this.difficultyLevel = requireNonNull(difficultyLevel); + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param o The object to compare. + * @return {@code true} if this object is the same as the o argument; {@code false} otherwise. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SudokuBoard that = (SudokuBoard) o; + return Arrays.deepEquals(unsolvedGrid, that.unsolvedGrid) + && Arrays.deepEquals(solvedGrid, that.solvedGrid) + && difficultyLevel == that.difficultyLevel; + } + + /** + * Returns a hash code value for the object. This method does not consider the DifficultyLevel to + * provide a unique hash code for each SudokuBoard, which can be used when persisting the files. + * + * @return The hash code of this {@link SudokuBoard}. + */ + @Override + public int hashCode() { + int result = Arrays.deepHashCode(unsolvedGrid); + result = 31 * result + Arrays.deepHashCode(solvedGrid); + return result; + } + + /** + * Returns a string representation of the SudokuBoard. + * + * @return A string representation of the SudokuBoard. + */ + @Override + public String toString() { + return "SudokuBoard{" + + "unsolvedGrid=" + + Arrays.toString(unsolvedGrid) + + ", solvedGrid=" + + Arrays.toString(solvedGrid) + + ", difficultyLevel=" + + difficultyLevel + + '}'; + } +} diff --git a/src/main/java/ch/zhaw/pm2/amongdigits/SudokuGui.java b/src/main/java/ch/zhaw/pm2/amongdigits/SudokuGui.java new file mode 100644 index 0000000..32d9009 --- /dev/null +++ b/src/main/java/ch/zhaw/pm2/amongdigits/SudokuGui.java @@ -0,0 +1,215 @@ +package ch.zhaw.pm2.amongdigits; + +import static ch.zhaw.pm2.amongdigits.PropertyType.SETTINGS; +import static ch.zhaw.pm2.amongdigits.ScreenType.CHALLENGES; +import static ch.zhaw.pm2.amongdigits.ScreenType.MAIN_MENU; +import static ch.zhaw.pm2.amongdigits.ScreenType.STATISTICS; +import static ch.zhaw.pm2.amongdigits.ScreenType.SUDOKU; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static javafx.scene.control.Alert.AlertType.ERROR; +import static javafx.scene.media.MediaPlayer.INDEFINITE; + +import ch.zhaw.pm2.amongdigits.controller.SudokuGameController; +import ch.zhaw.pm2.amongdigits.exception.InvalidFileFormatException; +import ch.zhaw.pm2.amongdigits.exception.InvalidSudokuException; +import ch.zhaw.pm2.amongdigits.utils.PropertiesHandler; +import ch.zhaw.pm2.amongdigits.utils.alert.AlertBuilder; +import ch.zhaw.pm2.amongdigits.utils.alert.AlertOptions; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.*; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.stage.Stage; +import lombok.extern.slf4j.Slf4j; + +/** + * The graphical user interface for the Sudoku game. + * + *
The SudokuGui class extends the JavaFX Application class and provides methods for managing the
+ * game screens, changing the stylesheet, initializing new games, and reloading screens.
+ */
+@Slf4j
+public class SudokuGui extends Application {
+
+ private static final String LANGUAGE = "language";
+ private static final double GAME_MUSIC_VOLUME = 0.2;
+ private final Map The ChallengesModelTest class contains unit tests to verify the functionality of the
+ * ChallengesModel class. It tests the loading of challenges and verifies that the loaded challenges
+ * match the expected challenges.
+ */
+class ChallengesModelTest {
+
+ private ChallengesModel model;
+
+ /** Sets up the test environment before each test method is executed. */
+ @BeforeEach
+ void setUp() {
+ model = new ChallengesModel();
+ }
+
+ /**
+ * Tests the loading of challenges and verifies that the loaded challenges match the expected
+ * challenges.
+ */
+ @Test
+ void testLoad() {
+ model.load();
+ final Map The SettingsModelTest class contains unit tests to verify the functionality of the
+ * SettingsModel class. It tests the toggling of various settings, changing the language, and
+ * retrieving properties related to settings.
+ */
+class SettingsModelTest {
+
+ private SettingsModel settingsModel;
+
+ /** Sets up the test environment before each test method is executed. */
+ @BeforeEach
+ void setUp() {
+ settingsModel = new SettingsModel();
+ }
+
+ /** Tests toggling the dark mode setting and verifies that the value has changed. */
+ @Test
+ void toggleDarkMode() {
+ String darkModeBefore = getPropertyString(SETTINGS, "darkMode");
+ settingsModel.toggleDarkMode();
+ assertNotEquals(darkModeBefore, getPropertyString(SETTINGS, "darkMode"));
+ }
+
+ /** Tests toggling the check mistakes setting and verifies that the value has changed. */
+ @Test
+ void toggleCheckMistakes() {
+ String showMistakesBefore = getPropertyString(SETTINGS, "checkMistakes");
+ settingsModel.toggleCheckMistakes();
+ assertNotEquals(showMistakesBefore, getPropertyString(SETTINGS, "showMistakes"));
+ }
+
+ /** Tests toggling the check time setting and verifies that the value has changed. */
+ @Test
+ void toggleCheckTime() {
+ String showTimeBefore = getPropertyString(SETTINGS, "checkTime");
+ settingsModel.toggleCheckTime();
+ assertNotEquals(showTimeBefore, getPropertyString(SETTINGS, "showTime"));
+ }
+
+ /** Tests toggling the realtime feedback setting and verifies that the value has changed. */
+ @Test
+ void toggleRealtimeFeedback() {
+ String realtimeFeedbackBefore = getPropertyString(SETTINGS, "realtimeFeedback");
+ settingsModel.toggleRealtimeFeedback();
+ assertNotEquals(realtimeFeedbackBefore, getPropertyString(SETTINGS, "realtimeFeedback"));
+ }
+
+ /** Tests changing the language setting and verifies that the value has changed accordingly. */
+ @Test
+ void changeLanguage() {
+ settingsModel.changeLanguage("de");
+ assertEquals("de", getPropertyString(SETTINGS, "language"));
+ settingsModel.changeLanguage("en");
+ assertNotEquals("de", getPropertyString(SETTINGS, "language"));
+ }
+
+ /** Tests getting the dark mode property and verifies its value. */
+ @Test
+ void getDarkModeProperty() {
+ if (getPropertyString(SETTINGS, "darkMode").equals("true")) {
+ assertTrue(settingsModel.getDarkModeProperty().getValue().contains(ENABLED_SYMBOL));
+ } else {
+ assertTrue(settingsModel.getDarkModeProperty().getValue().contains(DISABLED_SYMBOL));
+ }
+ }
+
+ /** Tests getting the check mistakes property and verifies its value. */
+ @Test
+ void getCheckMistakesProperty() {
+ if (getPropertyString(SETTINGS, "checkMistakes").equals("true")) {
+ assertTrue(settingsModel.getCheckMistakesProperty().getValue().contains(ENABLED_SYMBOL));
+ } else {
+ assertTrue(settingsModel.getCheckMistakesProperty().getValue().contains(DISABLED_SYMBOL));
+ }
+ }
+
+ /** Tests getting the check time property and verifies its value. */
+ @Test
+ void getCheckTimeProperty() {
+ if (getPropertyString(SETTINGS, "checkTime").equals("true")) {
+ assertTrue(settingsModel.getCheckTimeProperty().getValue().contains(ENABLED_SYMBOL));
+ } else {
+ assertTrue(settingsModel.getCheckTimeProperty().getValue().contains(DISABLED_SYMBOL));
+ }
+ }
+
+ /** Tests getting the realtime feedback property and verifies its value. */
+ @Test
+ void getRealtimeFeedbackProperty() {
+ if (getPropertyString(SETTINGS, "realtimeFeedback").equals("true")) {
+ assertTrue(settingsModel.getRealtimeFeedbackProperty().getValue().contains(ENABLED_SYMBOL));
+ } else {
+ assertTrue(settingsModel.getRealtimeFeedbackProperty().getValue().contains(DISABLED_SYMBOL));
+ }
+ }
+
+ /**
+ * Tests getting the dark mode value property and verifies its value after toggling the dark mode
+ * setting.
+ */
+ @Test
+ void getDarkModeValueProperty() {
+ StringProperty darkModeValueProperty = settingsModel.getDarkModeValueProperty();
+ for (int i = 0; i < 3; i++) {
+ settingsModel.toggleDarkMode();
+ if (getPropertyString(SETTINGS, "darkMode").equals("true")) {
+ assertTrue(darkModeValueProperty.getValue().contains("darkMode"));
+ } else {
+ assertTrue(darkModeValueProperty.getValue().contains("lightMode"));
+ }
+ }
+ }
+}
diff --git a/src/test/java/ch/zhaw/pm2/amongdigits/model/StatisticsModelTest.java b/src/test/java/ch/zhaw/pm2/amongdigits/model/StatisticsModelTest.java
new file mode 100644
index 0000000..50bebcb
--- /dev/null
+++ b/src/test/java/ch/zhaw/pm2/amongdigits/model/StatisticsModelTest.java
@@ -0,0 +1,170 @@
+package ch.zhaw.pm2.amongdigits.model;
+
+import static ch.zhaw.pm2.amongdigits.PropertyType.STATISTICS;
+import static ch.zhaw.pm2.amongdigits.utils.PropertiesHandler.getPropertyString;
+import static ch.zhaw.pm2.amongdigits.utils.SudokuConstants.EMPTY_CLOCK_FORMAT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** The StatisticsModelTest class tests the StatisticsModel class. */
+class StatisticsModelTest {
+ StatisticsModel statisticsModel;
+
+ /**
+ * The setUp function is used to initialize the statisticsModel object before each test. This
+ * ensures that each test starts with a fresh, empty model.
+ */
+ @BeforeEach
+ void setUp() {
+ statisticsModel = new StatisticsModel();
+ }
+
+ /**
+ * The getEasyGameStartedPropertyProperty function returns the easyGameStartedProperty property.
+ */
+ @Test
+ void getEasyGameStartedPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "easyGameStarted"),
+ statisticsModel.getEasyGameStartedPropertyProperty().getValue());
+ }
+
+ /** The getEasyGameWonPropertyProperty function returns the easyGameWonProperty property. */
+ @Test
+ void getEasyGameWonPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "easyGameWon"),
+ statisticsModel.getEasyGameWonPropertyProperty().getValue());
+ }
+
+ /**
+ * The getEasyGameMistakesPropertyProperty function returns the easyGameMistakesProperty property.
+ */
+ @Test
+ void getEasyGameMistakesPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "easyGameMistakes"),
+ statisticsModel.getEasyGameMistakesPropertyProperty().getValue());
+ }
+
+ /**
+ * The getEasyGameTimePlayedPropertyProperty function returns the
+ * easyGameTimePlayedPropertyProperty property of the statisticsModel.
+ */
+ @Test
+ void getEasyGameTimePlayedPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "easyGameTimePlayed"),
+ statisticsModel.getEasyGameTimePlayedPropertyProperty().getValue());
+ }
+
+ /**
+ * The getEasyGameBestTimePropertyProperty function returns the easyGameBestTimeProperty property.
+ */
+ @Test
+ void getEasyGameBestTimePropertyProperty() {
+ assertEquals(
+ EMPTY_CLOCK_FORMAT, statisticsModel.getEasyGameBestTimePropertyProperty().getValue());
+ }
+
+ /**
+ * The getMediumGameStartedPropertyProperty function returns the mediumGameStartedProperty
+ * property.
+ */
+ @Test
+ void getMediumGameStartedPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "mediumGameStarted"),
+ statisticsModel.getMediumGameStartedPropertyProperty().getValue());
+ }
+
+ /** The getMediumGameWonPropertyProperty function returns the mediumGameWonProperty property. */
+ @Test
+ void getMediumGameWonPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "mediumGameWon"),
+ statisticsModel.getMediumGameWonPropertyProperty().getValue());
+ }
+
+ /**
+ * The getMediumGameMistakesPropertyProperty function returns the mediumGameMistakesProperty
+ * property.
+ */
+ @Test
+ void getMediumGameMistakesPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "mediumGameMistakes"),
+ statisticsModel.getMediumGameMistakesPropertyProperty().getValue());
+ }
+
+ /**
+ * The getMediumGameTimePlayedPropertyProperty function returns the mediumGameTimePlayedProperty
+ * property.
+ */
+ @Test
+ void getMediumGameTimePlayedPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "mediumGameTimePlayed"),
+ statisticsModel.getMediumGameTimePlayedPropertyProperty().getValue());
+ }
+
+ /**
+ * The getMediumGameBestTimePropertyProperty function returns the mediumGameBestTimeProperty
+ * property.
+ */
+ @Test
+ void getMediumGameBestTimePropertyProperty() {
+ assertEquals(
+ EMPTY_CLOCK_FORMAT, statisticsModel.getMediumGameBestTimePropertyProperty().getValue());
+ }
+
+ /**
+ * The getHardGameStartedPropertyProperty function returns the hardGameStartedProperty property.
+ */
+ @Test
+ void getHardGameStartedPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "hardGameStarted"),
+ statisticsModel.getHardGameStartedPropertyProperty().getValue());
+ }
+
+ /** The getHardGameWonPropertyProperty function returns the hardGameWonProperty property. */
+ @Test
+ void getHardGameWonPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "hardGameWon"),
+ statisticsModel.getHardGameWonPropertyProperty().getValue());
+ }
+
+ /**
+ * The getHardGameMistakesPropertyProperty function returns the hardGameMistakesProperty property.
+ */
+ @Test
+ void getHardGameMistakesPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "hardGameMistakes"),
+ statisticsModel.getHardGameMistakesPropertyProperty().getValue());
+ }
+
+ /**
+ * The getHardGameTimePlayedPropertyProperty function returns the hardGameTimePlayedProperty
+ * property.
+ */
+ @Test
+ void getHardGameTimePlayedPropertyProperty() {
+ assertEquals(
+ getPropertyString(STATISTICS, "hardGameTimePlayed"),
+ statisticsModel.getHardGameTimePlayedPropertyProperty().getValue());
+ }
+
+ /**
+ * The getHardGameBestTimePropertyProperty function returns the hardGameBestTimeProperty property.
+ */
+ @Test
+ void getHardGameBestTimePropertyProperty() {
+ assertEquals(
+ EMPTY_CLOCK_FORMAT, statisticsModel.getHardGameBestTimePropertyProperty().getValue());
+ }
+}
diff --git a/src/test/java/ch/zhaw/pm2/amongdigits/model/SudokuGameModelTest.java b/src/test/java/ch/zhaw/pm2/amongdigits/model/SudokuGameModelTest.java
new file mode 100644
index 0000000..6fa3ced
--- /dev/null
+++ b/src/test/java/ch/zhaw/pm2/amongdigits/model/SudokuGameModelTest.java
@@ -0,0 +1,84 @@
+package ch.zhaw.pm2.amongdigits.model;
+
+import static ch.zhaw.pm2.amongdigits.utils.SudokuConstants.SUDOKU_GRID_SIZE;
+import static java.util.Arrays.deepEquals;
+import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.zhaw.pm2.amongdigits.DifficultyLevel;
+import ch.zhaw.pm2.amongdigits.exception.InvalidFileFormatException;
+import ch.zhaw.pm2.amongdigits.exception.InvalidSudokuException;
+import java.io.File;
+import java.util.ResourceBundle;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Test class for the SudokuGameModel. */
+class SudokuGameModelTest {
+
+ private static final String BASE_NAME = "MessagesBundle";
+
+ private static final byte[][] EXPECTED_VALID_UNSOLVED_SUDOKU = {
+ {9, 0, 0, 0, 8, 0, 3, 0, 0},
+ {0, 0, 0, 2, 5, 0, 7, 0, 0},
+ {0, 2, 0, 3, 0, 0, 0, 0, 4},
+ {0, 9, 4, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 7, 3, 0, 5, 6, 0},
+ {7, 0, 5, 0, 6, 0, 4, 0, 0},
+ {0, 0, 7, 8, 0, 3, 9, 0, 0},
+ {0, 0, 1, 0, 0, 0, 0, 0, 3},
+ {3, 0, 0, 0, 0, 0, 0, 0, 2}
+ };
+
+ private SudokuGameModel sudokuGameModel;
+
+ @BeforeEach
+ void setUp() {
+ ResourceBundle bundle = ResourceBundle.getBundle(BASE_NAME);
+ sudokuGameModel = new SudokuGameModel(bundle);
+ }
+
+ /** Test if the sudoku board is null after initialization. */
+ @Test
+ void getSudokuBoard() {
+ assertNull(sudokuGameModel.getSudokuBoard());
+ }
+
+ /** Test if the mistkes property get initialized properly. */
+ @Test
+ void getMistakesProperty() {
+ assertEquals(0, sudokuGameModel.getMistakesProperty().get());
+ }
+
+ /** Test if the solved property get initialized properly. */
+ @Test
+ void isSolvedProperty() {
+ assertFalse(sudokuGameModel.isSolvedProperty().get());
+ }
+
+ /**
+ * Tests if the createSudoku Method using Random generation adds a new SudokuBoard to the model.
+ */
+ @Test
+ void createRandomSudoku() {
+ sudokuGameModel.createSudoku(DifficultyLevel.EASY);
+ assertNotNull(sudokuGameModel.getSudokuBoard());
+ assertFalse(
+ deepEquals(
+ new byte[SUDOKU_GRID_SIZE][SUDOKU_GRID_SIZE],
+ sudokuGameModel.getSudokuBoard().unsolvedGrid()));
+ }
+
+ /** Tests if the createSudoku Method using File generation adds a new SudokuBoard to the model. */
+ @Test
+ void createFileSudoku() throws InvalidFileFormatException, InvalidSudokuException {
+ sudokuGameModel.createSudoku(
+ new File(
+ requireNonNull(
+ requireNonNull(getClass().getResource("/upload/validSudoku.txt")).getFile())));
+ assertNotNull(sudokuGameModel.getSudokuBoard());
+ assertTrue(
+ deepEquals(
+ EXPECTED_VALID_UNSOLVED_SUDOKU, sudokuGameModel.getSudokuBoard().unsolvedGrid()));
+ }
+}
diff --git a/src/test/java/ch/zhaw/pm2/amongdigits/upload/FileValidatorTest.java b/src/test/java/ch/zhaw/pm2/amongdigits/upload/FileValidatorTest.java
new file mode 100644
index 0000000..8a00687
--- /dev/null
+++ b/src/test/java/ch/zhaw/pm2/amongdigits/upload/FileValidatorTest.java
@@ -0,0 +1,187 @@
+package ch.zhaw.pm2.amongdigits.upload;
+
+import static ch.zhaw.pm2.amongdigits.utils.SudokuConstants.EMPTY_GRID_CELL;
+import static ch.zhaw.pm2.amongdigits.utils.SudokuConstants.GRID_SEPARATOR;
+import static ch.zhaw.pm2.amongdigits.utils.SudokuConstants.SUDOKU_GRID_SIZE;
+import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** The unit test class for FileValidator. */
+class FileValidatorTest {
+
+ private FileValidator fileValidator;
+
+ private static Stream
+ *
+ */
+ public enum FreeCellResult {
+ FOUND,
+ NONE_FREE,
+ CONTRADICTION
+ }
+}
diff --git a/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/Schema.java b/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/Schema.java
new file mode 100644
index 0000000..f095a18
--- /dev/null
+++ b/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/Schema.java
@@ -0,0 +1,91 @@
+package ch.zhaw.pm2.amongdigits.utils.schema;
+
+/**
+ * An interface representing a schema for a sudoku-like game. A schema defines the size and
+ * structure of the grid, as well as the valid values that can be entered in each field of the grid.
+ */
+public interface Schema {
+
+ /**
+ * Returns the minimum valid value that can be entered in any field of the schema.
+ *
+ * @return the minimum valid value
+ */
+ byte getMinimumValue();
+
+ /**
+ * Returns the maximum valid value that can be entered in any field of the schema.
+ *
+ * @return the maximum valid value
+ */
+ byte getMaximumValue();
+
+ /**
+ * Returns the value that represents an unset field in the schema.
+ *
+ * @return the value that represents an unset field
+ */
+ byte getUnsetValue();
+
+ /**
+ * Returns the width of the schema, i.e. the number of fields in each row and column of the grid.
+ *
+ * @return the width of the schema
+ */
+ int getWidth();
+
+ /**
+ * Returns the width of a block in the schema, i.e. the number of fields in each row and column of
+ * a block of the grid.
+ *
+ * @return the width of a block in the schema
+ */
+ int getBlockWidth();
+
+ /**
+ * Returns the total number of fields in the schema, i.e. the number of fields in the entire grid.
+ *
+ * @return the total number of fields in the schema
+ */
+ int getTotalFields();
+
+ /**
+ * Returns the number of blocks in the schema, i.e. the number of sub-grids in the grid.
+ *
+ * @return the number of blocks in the schema
+ */
+ int getBlockCount();
+
+ /**
+ * Returns a bit mask representing the valid values for the fields in the schema. The i-th bit of
+ * the mask is set if the value i+1 is a valid value for the schema.
+ *
+ * @return a bit mask representing the valid values for the fields in the schema
+ */
+ int getBitMask();
+
+ /**
+ * Returns whether the given value is a valid value for the schema.
+ *
+ * @param value the value to check
+ * @return true if the value is valid, false otherwise
+ */
+ boolean isValueValid(byte value);
+
+ /**
+ * Returns whether the given bit mask is a valid bit mask for the schema.
+ *
+ * @param bitMask the bit mask to check
+ * @return true if the bit mask is valid, false otherwise
+ */
+ boolean isBitMaskValid(int bitMask);
+
+ /**
+ * Returns whether the given coordinates are valid for the schema.
+ *
+ * @param row the row index (0-based)
+ * @param column the column index (0-based)
+ * @return true if the coordinates are valid, false otherwise
+ */
+ boolean areCoordsValid(int row, int column);
+}
diff --git a/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/SchemaManager.java b/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/SchemaManager.java
new file mode 100644
index 0000000..af9b857
--- /dev/null
+++ b/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/SchemaManager.java
@@ -0,0 +1,117 @@
+package ch.zhaw.pm2.amongdigits.utils.schema;
+
+class SchemaManager implements Schema {
+
+ private final byte unsetValue;
+ private final byte minimumValue;
+ private final byte maximumValue;
+ private final int width;
+ private final int blockWidth;
+ private final int totalFields;
+ private final int blockCount;
+ private final int bitMask;
+
+ SchemaManager(
+ final byte unsetValue,
+ final byte minimumValue,
+ final byte maximumValue,
+ final int width,
+ final int blockWidth) {
+ if (minimumValue <= unsetValue && unsetValue <= maximumValue) {
+ throw new IllegalArgumentException(
+ "Maximum value must be greater than unset value and unset value must be greater than minimum value");
+ }
+ this.unsetValue = unsetValue;
+
+ if (maximumValue - minimumValue + 1 != width) {
+ throw new IllegalArgumentException(
+ "Maximum value minus minimum value plus one must be equal to width");
+ }
+ this.minimumValue = minimumValue;
+ this.maximumValue = maximumValue;
+
+ if (width != blockWidth * blockWidth) {
+ throw new IllegalArgumentException("Width must be equal to block width squared");
+ }
+ if (width <= 0) {
+ throw new IllegalArgumentException("Width must be greater than zero");
+ }
+ if (width % blockWidth != 0) {
+ throw new IllegalArgumentException("Width must be a multiple of block width");
+ }
+ this.width = width;
+ this.blockWidth = blockWidth;
+ this.totalFields = width * width;
+ this.blockCount = width / blockWidth;
+
+ int bitMaskCounter = 0;
+ for (int i = minimumValue; i <= maximumValue; i++) {
+ bitMaskCounter |= 1 << i;
+ }
+ this.bitMask = bitMaskCounter;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public byte getMinimumValue() {
+ return minimumValue;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public byte getMaximumValue() {
+ return maximumValue;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public byte getUnsetValue() {
+ return unsetValue;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getBlockWidth() {
+ return blockWidth;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getTotalFields() {
+ return totalFields;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getBlockCount() {
+ return blockCount;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getBitMask() {
+ return bitMask;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isValueValid(final byte value) {
+ return value == unsetValue || (value >= minimumValue && value <= maximumValue);
+ }
+
+ @Override
+ public boolean isBitMaskValid(final int bitMask) {
+ return (bitMask & (~this.bitMask)) == 0;
+ }
+
+ @Override
+ public boolean areCoordsValid(final int row, final int column) {
+ return row >= 0 && row < width && column >= 0 && column < width;
+ }
+}
diff --git a/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/SchemaTypes.java b/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/SchemaTypes.java
new file mode 100644
index 0000000..cbc7e28
--- /dev/null
+++ b/src/main/java/ch/zhaw/pm2/amongdigits/utils/schema/SchemaTypes.java
@@ -0,0 +1,23 @@
+package ch.zhaw.pm2.amongdigits.utils.schema;
+
+import java.util.List;
+
+/** This class provides a set of predefined {@link Schema} types. */
+public final class SchemaTypes {
+
+ /** A 9x9 {@link Schema} type. */
+ public static final Schema SCHEMA_9X9 = new SchemaManager((byte) 0, (byte) 1, (byte) 9, 9, 3);
+
+ private SchemaTypes() {
+ throw new UnsupportedOperationException("Utility class");
+ }
+
+ /**
+ * Returns a list of all available {@link Schema} types.
+ *
+ * @return a list of all available {@link Schema} types
+ */
+ public static List