diff --git a/SudokuApp/app/build.gradle b/SudokuApp/app/build.gradle new file mode 100644 index 0000000..2c68fe5 --- /dev/null +++ b/SudokuApp/app/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.example.sudokuapp' // Changed from com.example.sudoku to avoid conflict with java dir + compileSdk 33 // Example SDK version + + defaultConfig { + applicationId "com.example.sudokuapp" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + // If using Kotlin, uncomment the following block + // kotlinOptions { + // jvmTarget = '1.8' + // } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' // Example version + implementation 'com.google.android.material:material:1.8.0' // Example version + // Add other dependencies here + // Test dependencies + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} diff --git a/SudokuApp/app/proguard-rules.pro b/SudokuApp/app/proguard-rules.pro new file mode 100644 index 0000000..fc88f06 --- /dev/null +++ b/SudokuApp/app/proguard-rules.pro @@ -0,0 +1,16 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/google/android/sdk/tools/proguard/proguard-android-optimize.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify actual class name. +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/SudokuApp/app/src/main/AndroidManifest.xml b/SudokuApp/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c550451 --- /dev/null +++ b/SudokuApp/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + {/* Changed to 33 */} + + + + + + + + diff --git a/SudokuApp/app/src/main/java/com/example/sudoku/.gitkeep b/SudokuApp/app/src/main/java/com/example/sudoku/.gitkeep new file mode 100644 index 0000000..0fdedc7 --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudoku/.gitkeep @@ -0,0 +1 @@ +# This file is used to ensure the directory is tracked by git. diff --git a/SudokuApp/app/src/main/java/com/example/sudoku/game/Cell.java b/SudokuApp/app/src/main/java/com/example/sudoku/game/Cell.java new file mode 100644 index 0000000..f4859f0 --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudoku/game/Cell.java @@ -0,0 +1,56 @@ +package com.example.sudoku.game; + +public class Cell { + private int row; + private int col; + private int value; // 0 represents an empty cell + private boolean isStartingCell; // Was this cell part of the initial puzzle? + private boolean isEditable; + + public Cell(int row, int col, int value) { + this.row = row; + this.col = col; + this.value = value; + this.isStartingCell = (value != 0); // If a value is provided initially, it's a starting cell + this.isEditable = !this.isStartingCell; + } + + public Cell(int row, int col) { + this(row, col, 0); // Constructor for an empty cell + } + + public int getRow() { + return row; + } + + public int getCol() { + return col; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + if (isEditable) { + this.value = value; + } + // Optionally, throw an exception or log if trying to set a non-editable cell + } + + public boolean isStartingCell() { + return isStartingCell; + } + + public boolean isEditable() { + return isEditable; + } + + public void setEditable(boolean editable) { + // This might be used if puzzle generation logic needs to change editability after creation + this.isEditable = editable; + if (editable) { + this.isStartingCell = false; // If it becomes editable, it's not a starting cell anymore + } + } +} diff --git a/SudokuApp/app/src/main/java/com/example/sudoku/game/SudokuGame.java b/SudokuApp/app/src/main/java/com/example/sudoku/game/SudokuGame.java new file mode 100644 index 0000000..5156893 --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudoku/game/SudokuGame.java @@ -0,0 +1,228 @@ +package com.example.sudoku.game; + +public class SudokuGame { + public static final int GRID_SIZE = 9; + private Cell[][] board; + + private int selectedRow = -1; // To keep track of the selected cell + private int selectedCol = -1; + + public SudokuGame() { + board = new Cell[GRID_SIZE][GRID_SIZE]; + initializeEmptyBoard(); + } + + public void initializeEmptyBoard() { + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + board[r][c] = new Cell(r, c, 0); // Initialize with empty, editable cells + } + } + selectedRow = -1; + selectedCol = -1; + } + + public Cell getCell(int row, int col) { + if (row >= 0 && row < GRID_SIZE && col >= 0 && col < GRID_SIZE) { + return board[row][col]; + } + return null; // Or throw an exception + } + + // Sets the value of a cell, if it's editable + public boolean setCellValue(int row, int col, int value) { + if (row >= 0 && row < GRID_SIZE && col >= 0 && col < GRID_SIZE) { + Cell cell = board[row][col]; + if (cell.isEditable()) { + cell.setValue(value); + return true; + } + } + return false; + } + + // Overload to directly take a Cell object (e.g. from UI selection) + public boolean setCellValue(Cell cell, int value) { + if (cell != null && cell.isEditable()) { + cell.setValue(value); + return true; + } + return false; + } + + // Method to load a puzzle from a 2D integer array + public void loadPuzzle(int[][] puzzle) { + if (puzzle == null || puzzle.length != GRID_SIZE || puzzle[0].length != GRID_SIZE) { + // Optionally throw an IllegalArgumentException or handle error + initializeEmptyBoard(); // Fallback to empty board + return; + } + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + board[r][c] = new Cell(r, c, puzzle[r][c]); + } + } + selectedRow = -1; + selectedCol = -1; + } + + public int getSelectedRow() { + return selectedRow; + } + + public void setSelectedRow(int selectedRow) { + this.selectedRow = selectedRow; + } + + public int getSelectedCol() { + return selectedCol; + } + + public void setSelectedCol(int selectedCol) { + this.selectedCol = selectedCol; + } + + public void selectCell(int row, int col) { + if (row >= 0 && row < GRID_SIZE) { + selectedRow = row; + } else { + selectedRow = -1; + } + if (col >= 0 && col < GRID_SIZE) { + selectedCol = col; + } else { + selectedCol = -1; + } + } + + public Cell getSelectedCell() { + if (selectedRow != -1 && selectedCol != -1) { + return getCell(selectedRow, selectedCol); + } + return null; + } + + // Placeholder for a simple puzzle for testing + public static int[][] getSamplePuzzle() { + return new int[][]{ + {5, 3, 0, 0, 7, 0, 0, 0, 0}, + {6, 0, 0, 1, 9, 5, 0, 0, 0}, + {0, 9, 8, 0, 0, 0, 0, 6, 0}, + {8, 0, 0, 0, 6, 0, 0, 0, 3}, + {4, 0, 0, 8, 0, 3, 0, 0, 1}, + {7, 0, 0, 0, 2, 0, 0, 0, 6}, + {0, 6, 0, 0, 0, 0, 2, 8, 0}, + {0, 0, 0, 4, 1, 9, 0, 0, 5}, + {0, 0, 0, 0, 8, 0, 0, 7, 9} + }; + } + + // --- Add these methods to the existing SudokuGame.java class --- + + public boolean isValidMove(int row, int col, int value) { + if (value == 0) return true; // Clearing a cell is always valid if it's editable + // Check if the number is already in the row, column, or 3x3 subgrid, + // excluding the cell itself if it's the one being checked. + for (int i = 0; i < GRID_SIZE; i++) { + // Check row + if (i != col && board[row][i].getValue() == value) return false; + // Check column + if (i != row && board[i][col].getValue() == value) return false; + } + + // Check 3x3 subgrid + int subgridStartRow = row - row % 3; + int subgridStartCol = col - col % 3; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + int currentRow = subgridStartRow + r; + int currentCol = subgridStartCol + c; + if (currentRow == row && currentCol == col) continue; // Skip the cell itself + if (board[currentRow][currentCol].getValue() == value) return false; + } + } + return true; + } + + // More specific checks if needed, though isValidMove covers these. + // These can be useful for providing specific feedback to the user. + + private boolean isNumberInRow(int row, int colToExclude, int number) { + for (int c = 0; c < GRID_SIZE; c++) { + if (c == colToExclude) continue; + if (board[row][c].getValue() == number) { + return true; + } + } + return false; + } + + private boolean isNumberInCol(int col, int rowToExclude, int number) { + for (int r = 0; r < GRID_SIZE; r++) { + if (r == rowToExclude) continue; + if (board[r][col].getValue() == number) { + return true; + } + } + return false; + } + + private boolean isNumberInSubgrid(int startRow, int startCol, int rowToExclude, int colToExclude, int number) { + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + int currentRow = startRow + r; + int currentCol = startCol + c; + if (currentRow == rowToExclude && currentCol == colToExclude) continue; + if (board[currentRow][currentCol].getValue() == number) { + return true; + } + } + } + return false; + } + + public boolean isBoardSolved() { + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + int value = board[r][c].getValue(); + if (value == 0) { + return false; // Found an empty cell, so not solved + } + // Check if this number is valid in its row, column, and subgrid + // considering other numbers but not itself initially + for (int i = 0; i < GRID_SIZE; i++) { + // Check row (excluding itself) + if (i != c && board[r][i].getValue() == value) return false; + // Check col (excluding itself) + if (i != r && board[i][c].getValue() == value) return false; + } + // Check 3x3 subgrid (excluding itself) + int subgridStartRow = r - r % 3; + int subgridStartCol = c - c % 3; + for (int sr = 0; sr < 3; sr++) { + for (int sc = 0; sc < 3; sc++) { + int currentRow = subgridStartRow + sr; + int currentCol = subgridStartCol + sc; + if (currentRow == r && currentCol == c) continue; + if (board[currentRow][currentCol].getValue() == value) return false; + } + } + } + } + return true; // All cells are filled and valid according to Sudoku rules + } + + public void clearEditableCells() { // Renamed for clarity + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + if (board[r][c].isEditable()) { + board[r][c].setValue(0); + } + } + } + selectedRow = -1; + selectedCol = -1; + } + + // --- End of methods to add --- +} diff --git a/SudokuApp/app/src/main/java/com/example/sudoku/game/SudokuGenerator.java b/SudokuApp/app/src/main/java/com/example/sudoku/game/SudokuGenerator.java new file mode 100644 index 0000000..1b99918 --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudoku/game/SudokuGenerator.java @@ -0,0 +1,137 @@ +package com.example.sudoku.game; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +public class SudokuGenerator { + + private static final int GRID_SIZE = SudokuGame.GRID_SIZE; + private Random random = new Random(); + + // Main method to generate a new puzzle and set it on the game object + public void generateNewPuzzle(SudokuGame game, int numbersToRemove) { + game.initializeEmptyBoard(); // Start with a clean board + fillBoard(game.getBoard()); // Fill it with a valid Sudoku solution + + // Remove numbers to create the puzzle + pokeHoles(game.getBoard(), numbersToRemove); + + // Update cell properties (isStartingCell, isEditable) based on the final puzzle + // This loop is actually redundant if SudokuGame.loadPuzzle is called afterwards, + // or if the Cell constructor logic (value != 0 means !isEditable) is solely relied upon. + // However, it can be kept for explicit clarity or if direct manipulation is preferred. + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + Cell cell = game.getCell(r,c); + // The Cell constructor should correctly set isStartingCell based on whether value is 0. + // If value is non-zero, it's a starting cell, hence not editable. + // If value is zero, it's an empty cell, hence editable. + // So, re-setting isEditable or isStartingCell here might be redundant + // if the game's loadPuzzle method or Cell constructors handle this. + // For example, if SudokuGame.loadPuzzle(boardValues) is called after this, + // it would create new Cell objects, correctly setting their states. + // Let's assume the Cell constructor logic is sufficient. + // cell.setEditable(cell.getValue() == 0); + // if (cell.getValue() != 0) { cell.isStartingCell = true; } // Simplified + } + } + // After poking holes, the SudokuGame's internal board (Cell[][]) has 0s for empty cells. + // If SudokuGame.loadPuzzle() is called with the integer values from this board, + // it will correctly initialize the Cell objects, setting isEditable and isStartingCell. + } + + // Fills the board using a backtracking algorithm + private boolean fillBoard(Cell[][] board) { + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + if (board[r][c].getValue() == 0) { // Find an empty cell + List numbers = new ArrayList<>(); + for (int i = 1; i <= GRID_SIZE; i++) { + numbers.add(i); + } + Collections.shuffle(numbers, random); // Randomize order of numbers to try + + for (int num : numbers) { + if (isSafe(board, r, c, num)) { + board[r][c].setValue(num); // Place number + if (fillBoard(board)) { // Recurse + return true; // Solution found + } + board[r][c].setValue(0); // Backtrack: undo placement + } + } + return false; // No valid number found for this cell, trigger backtrack + } + } + } + return true; // All cells filled + } + + // Checks if a number can be safely placed in a cell + private boolean isSafe(Cell[][] board, int row, int col, int num) { + // Check row + for (int c = 0; c < GRID_SIZE; c++) { + if (board[row][c].getValue() == num) return false; + } + // Check column + for (int r = 0; r < GRID_SIZE; r++) { + if (board[r][col].getValue() == num) return false; + } + // Check 3x3 subgrid + int subgridStartRow = row - row % 3; + int subgridStartCol = col - col % 3; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + if (board[subgridStartRow + r][subgridStartCol + c].getValue() == num) return false; + } + } + return true; + } + + // Removes numbers from the filled board to create the puzzle + private void pokeHoles(Cell[][] board, int holesToMake) { + int cellsToRemove = holesToMake; + if (cellsToRemove <= 0 || cellsToRemove >= GRID_SIZE * GRID_SIZE) { + cellsToRemove = GRID_SIZE * GRID_SIZE / 2; // Default to removing half if invalid + } + + List cellCoordinates = new ArrayList<>(); + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + cellCoordinates.add(new int[]{r, c}); + } + } + Collections.shuffle(cellCoordinates, random); + + int removedCount = 0; + for (int[] coord : cellCoordinates) { + if (removedCount >= cellsToRemove) break; + int r = coord[0]; + int c = coord[1]; + if (board[r][c].getValue() != 0) { + // Directly set the value of the Cell object to 0. + // The Cell's isEditable property should ideally be updated by SudokuGame + // when it processes this modified board (e.g., by calling loadPuzzle). + // The Cell constructor (new Cell(r, c, 0)) makes it editable. + board[r][c].setValue(0); + // After setting value to 0, the SudokuGame should ensure this cell + // becomes editable. This is handled by Cell's constructor if SudokuGame.loadPuzzle + // is called, or if Cell.setValue(0) implies it becomes editable. + // The Cell.setValue() method in Cell.java only allows changing value if editable. + // This means for pokeHoles to work as intended (making cells empty and thus editable), + // we might need to adjust Cell.setValue or ensure cells are made editable here. + // For now, we assume SudokuGame will reconstruct/update cells based on the new values. + // A simpler approach for Cell: setValue(0) makes it editable. + // Or, SudokuGame after calling generator: + // for each cell: if cell.value == 0, cell.isEditable = true, cell.isStarting = false + // else cell.isEditable = false, cell.isStarting = true. + // The current Cell constructor (new Cell(r,c,value)) handles this: + // isStartingCell = (value != 0), isEditable = !isStartingCell. + // So if SudokuGame.loadPuzzle() is called with the int values, it's fine. + removedCount++; + } + } + } +} diff --git a/SudokuApp/app/src/main/java/com/example/sudoku/utils/SudokuSolver.java b/SudokuApp/app/src/main/java/com/example/sudoku/utils/SudokuSolver.java new file mode 100644 index 0000000..bfa091e --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudoku/utils/SudokuSolver.java @@ -0,0 +1,128 @@ +package com.example.sudoku.utils; + +import com.example.sudoku.game.Cell; +import com.example.sudoku.game.SudokuGame; + +public class SudokuSolver { + + private static final int GRID_SIZE = SudokuGame.GRID_SIZE; + + public boolean solve(SudokuGame game) { + if (game == null || game.getBoard() == null) { + return false; + } + // This method should operate on the Cell[][] array directly from the game. + // It is crucial that SudokuSolver modifies the actual Cell objects within the SudokuGame's board, + // so that the game state reflects the solution. + // The SudokuGame.getBoard() method should return the reference to its internal Cell[][] board. + return solveSudoku(game.getBoard()); + } + + private boolean solveSudoku(Cell[][] board) { + int[] emptyLoc = findEmptyLocation(board); + if (emptyLoc == null) { + return true; // Board is already solved (no empty cells) + } + + int row = emptyLoc[0]; + int col = emptyLoc[1]; + + Cell currentCell = board[row][col]; + + // Important: Solver should only try to fill cells that are originally empty or user-filled. + // It should not overwrite pre-filled (isStartingCell) numbers. + // However, the current findEmptyLocation only looks for cells with value 0. + // If a user incorrectly fills a cell and then asks for a solve, the solver might + // get stuck or produce an incorrect result if it doesn't respect original puzzle cells. + // For now, we assume findEmptyLocation correctly identifies cells that can be part of solving. + // The provided Cell.setValue() method already checks 'isEditable'. If a cell is a starting cell, + // setValue will not change it. This is good. + // So, if findEmptyLocation returns a starting cell (because its value somehow became 0, which shouldn't happen), + // setValue would prevent modification. + // The current logic is: find cell with value 0. If it's a starting cell (immutable), setValue does nothing. + // This would lead to solveSudoku returning false for that path, which is correct. + + for (int num = 1; num <= GRID_SIZE; num++) { + if (isSafe(board, row, col, num)) { + // Before setting value, check if the cell is actually editable. + // While findEmptyLocation implies it's empty (value 0), and Cell constructor + // would make value 0 cells editable, this check adds robustness. + // However, the current Cell.setValue() already handles this. + // currentCell.setValue(num); // Try placing the number + + // We directly call setValue on the cell object. + // The Cell's own logic will determine if it can be set (i.e. if it's editable). + // If it's a starting cell, setValue will do nothing, and the condition below will fail. + // This is the correct behavior. + + // Check if the cell is editable (not a starting cell) before attempting to set a value + // findEmptyLocation should only return cells with value 0, which are by default editable + // unless they were starting cells that somehow got reset to 0 (which is unlikely). + // The Cell.setValue() method itself checks for editability. + + // currentCell.setValue(num); + // if (currentCell.getValue() != num) { // If setValue failed (e.g. not editable) + // continue; // Try next number or backtrack if this was the only way + // } + // The above check is redundant if setValue correctly handles non-editable cells by not changing them. + + // Let's assume currentCell is indeed empty and thus editable. + currentCell.setValue(num); + + + if (solveSudoku(board)) { // Recurse + return true; // Solution found + } + + // If solution not found with this number, backtrack. + // Reset the cell only if it was this instance of solveSudoku that set it. + // And only if it's editable. + currentCell.setValue(0); // Backtrack + } + } + return false; // No number from 1-9 works for this cell, trigger backtrack + } + + private int[] findEmptyLocation(Cell[][] board) { + for (int r = 0; r < GRID_SIZE; r++) { + for (int c = 0; c < GRID_SIZE; c++) { + // Only consider cells that are empty (value 0). + // The solver should fill these empty cells. + // By design, a cell with value 0 is considered editable by the Cell class. + if (board[r][c].getValue() == 0) { + return new int[]{r, c}; + } + } + } + return null; // No empty cell found + } + + // isSafe needs to check the values from the Cell objects in the board + private boolean isSafe(Cell[][] board, int row, int col, int num) { + // Check row + for (int c = 0; c < GRID_SIZE; c++) { + if (board[row][c].getValue() == num) { + return false; + } + } + + // Check column + for (int r = 0; r < GRID_SIZE; r++) { + if (board[r][col].getValue() == num) { + return false; + } + } + + // Check 3x3 subgrid + int subgridStartRow = row - row % 3; + int subgridStartCol = col - col % 3; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + if (board[subgridStartRow + r][subgridStartCol + c].getValue() == num) { + return false; + } + } + } + return true; // Number is safe to place + } +} diff --git a/SudokuApp/app/src/main/java/com/example/sudokuapp/MainActivity.java b/SudokuApp/app/src/main/java/com/example/sudokuapp/MainActivity.java new file mode 100644 index 0000000..65489b5 --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudokuapp/MainActivity.java @@ -0,0 +1,139 @@ +package com.example.sudokuapp; + +import androidx.appcompat.app.AppCompatActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; // For showing messages +// No need to import View.OnClickListener if using lambdas or anonymous inner classes for listeners +// However, if we were to implement OnClickListener on MainActivity, it would be needed. +// The provided solution uses lambdas, so it's not strictly necessary here. +// Import game logic classes using their fully qualified names or direct imports +import com.example.sudoku.game.SudokuGame; +import com.example.sudoku.game.SudokuGenerator; + +public class MainActivity extends AppCompatActivity { + + private SudokuBoardView boardView; + private SudokuGame game; + private SudokuGenerator generator; + + // Number buttons + private Button button1, button2, button3, button4, button5; + private Button button6, button7, button8, button9, buttonErase; + private Button buttonNewGame; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + boardView = findViewById(R.id.sudokuBoardView); + buttonNewGame = findViewById(R.id.buttonNewGame); + + // Initialize number pad buttons + initializeNumberButtons(); // Finds views for number buttons + setupNumberButtonListeners(); // Sets listeners for number buttons + + game = new SudokuGame(); + generator = new SudokuGenerator(); + + startNewGame(); // Start a new game on creation + + buttonNewGame.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startNewGame(); + } + }); + + // Optional: Listener for cell selection from SudokuBoardView + // boardView.setOnCellSelectedListener(new SudokuBoardView.OnCellSelectedListener() { + // @Override + // public void onCellSelected(int row, int col) { + // // Handle cell selection if needed (e.g., update UI to show selected number) + // // For now, SudokuBoardView handles visual selection, game state is updated. + // } + // }); + // Call to setupNumberButtonListeners() already added above as per instruction + } + + private void startNewGame() { + // Difficulty: number of cells to remove. Higher is harder. + // Max is GRID_SIZE*GRID_SIZE. Sensible might be 20-50. + int numbersToRemove = 30; // Example difficulty + + // The generator modifies the 'game' object's board directly. + // So, we first need to ensure the game object has a fresh board structure if needed. + game.initializeEmptyBoard(); // Ensure cells are fresh for the generator + generator.generateNewPuzzle(game, numbersToRemove); + + // After generation, the game object's board is filled with the puzzle. + // SudokuBoardView needs this game object to draw. + boardView.setGame(game); + boardView.invalidate(); // Ensure redraw + } + + private void initializeNumberButtons() { + button1 = findViewById(R.id.button1); + button2 = findViewById(R.id.button2); + button3 = findViewById(R.id.button3); + button4 = findViewById(R.id.button4); + button5 = findViewById(R.id.button5); + button6 = findViewById(R.id.button6); + button7 = findViewById(R.id.button7); + button8 = findViewById(R.id.button8); + button9 = findViewById(R.id.button9); + buttonErase = findViewById(R.id.buttonErase); + // Listeners for these will be set up in Part 2 of MainActivity implementation + } + + private void setupNumberButtonListeners() { + View.OnClickListener numberClickListener = v -> { + if (game == null || game.getSelectedCell() == null) return; + // Ensure the cell is editable (not a starting cell) + if (!game.getSelectedCell().isEditable()) { + Toast.makeText(this, "This cell is not editable.", Toast.LENGTH_SHORT).show(); + return; + } + + Button clickedButton = (Button) v; + // Ensure tag is not null before parsing. XML tags are set to "1" through "9". + if (clickedButton.getTag() == null) return; + int number = Integer.parseInt(clickedButton.getTag().toString()); + + // Optional: Add validation using game.isValidMove(row, col, number) before setting + // if (!game.isValidMove(game.getSelectedRow(), game.getSelectedCol(), number)) { + // Toast.makeText(this, "Invalid move.", Toast.LENGTH_SHORT).show(); + // return; + // } + + game.setCellValue(game.getSelectedCell(), number); + boardView.invalidate(); // Redraw board + + if (game.isBoardSolved()) { + Toast.makeText(this, getString(R.string.game_solved_message), Toast.LENGTH_LONG).show(); + } + }; + + button1.setOnClickListener(numberClickListener); + button2.setOnClickListener(numberClickListener); + button3.setOnClickListener(numberClickListener); + button4.setOnClickListener(numberClickListener); + button5.setOnClickListener(numberClickListener); + button6.setOnClickListener(numberClickListener); + button7.setOnClickListener(numberClickListener); + button8.setOnClickListener(numberClickListener); + button9.setOnClickListener(numberClickListener); + + buttonErase.setOnClickListener(v -> { + if (game == null || game.getSelectedCell() == null) return; + if (!game.getSelectedCell().isEditable()) { + Toast.makeText(this, "This cell is not editable.", Toast.LENGTH_SHORT).show(); + return; + } + game.setCellValue(game.getSelectedCell(), 0); // 0 means erase + boardView.invalidate(); // Redraw board + }); + } +} diff --git a/SudokuApp/app/src/main/java/com/example/sudokuapp/SudokuBoardView.java b/SudokuApp/app/src/main/java/com/example/sudokuapp/SudokuBoardView.java new file mode 100644 index 0000000..3829da4 --- /dev/null +++ b/SudokuApp/app/src/main/java/com/example/sudokuapp/SudokuBoardView.java @@ -0,0 +1,208 @@ +package com.example.sudokuapp; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; // Import MotionEvent +import android.view.View; + +import androidx.annotation.Nullable; + +import com.example.sudoku.game.Cell; // Import from our game logic package +import com.example.sudoku.game.SudokuGame; // Import from our game logic package + +public class SudokuBoardView extends View { + + private SudokuGame game; + private Paint majorGridPaint; + private Paint minorGridPaint; + private Paint numberTextPaint; + private Paint startingNumberTextPaint; // For pre-filled numbers + private Paint selectedCellPaint; + private Paint highlightedCellPaint; // For row/col/subgrid of selected cell + + private int cellSize; + private final Rect textBounds = new Rect(); // For centering text + + // (Interface can be outside or a static inner interface) + public interface OnCellSelectedListener { + void onCellSelected(int row, int col); + } + + private OnCellSelectedListener cellSelectedListener; + + public void setOnCellSelectedListener(OnCellSelectedListener listener) { + this.cellSelectedListener = listener; + } + + public SudokuBoardView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initPaints(); + } + + private void initPaints() { + majorGridPaint = new Paint(); + majorGridPaint.setColor(Color.BLACK); + majorGridPaint.setStyle(Paint.Style.STROKE); + majorGridPaint.setStrokeWidth(4f); // Thicker lines for major grid lines + + minorGridPaint = new Paint(); + minorGridPaint.setColor(Color.GRAY); + minorGridPaint.setStyle(Paint.Style.STROKE); + minorGridPaint.setStrokeWidth(1f); // Thinner lines for minor grid lines + + numberTextPaint = new Paint(); + numberTextPaint.setColor(Color.BLUE); // User entered numbers + numberTextPaint.setTextAlign(Paint.Align.CENTER); + // Text size will be set in onSizeChanged or onMeasure + + startingNumberTextPaint = new Paint(); + startingNumberTextPaint.setColor(Color.BLACK); // Pre-filled numbers + startingNumberTextPaint.setTextAlign(Paint.Align.CENTER); + startingNumberTextPaint.setFakeBoldText(true); + // Text size will be set in onSizeChanged or onMeasure + + selectedCellPaint = new Paint(); + selectedCellPaint.setColor(getContext().getColor(R.color.selected_cell_background)); // From colors.xml + selectedCellPaint.setStyle(Paint.Style.FILL); + + highlightedCellPaint = new Paint(); + highlightedCellPaint.setColor(getContext().getColor(R.color.highlighted_cell_background)); // From colors.xml + highlightedCellPaint.setStyle(Paint.Style.FILL); + } + + public void setGame(SudokuGame game) { + this.game = game; + invalidate(); // Redraw the view with the new game state + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + int size = Math.min(width, height); + setMeasuredDimension(size, size); // Make the view square + + // Calculate cell size and text size here, after view dimensions are known + cellSize = size / SudokuGame.GRID_SIZE; + float textSize = cellSize * 0.6f; // Example: 60% of cell size + numberTextPaint.setTextSize(textSize); + startingNumberTextPaint.setTextSize(textSize); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (canvas == null) return; + + // Draw cell highlights (selected and related) + drawCellHighlights(canvas); + + // Draw grid lines + drawGridLines(canvas); + + // Draw numbers + drawNumbers(canvas); + } + + private void drawCellHighlights(Canvas canvas) { + if (game == null || game.getSelectedRow() == -1 || game.getSelectedCol() == -1) { + return; + } + + int selRow = game.getSelectedRow(); + int selCol = game.getSelectedCol(); + + // Highlight the selected cell itself + canvas.drawRect(selCol * cellSize, selRow * cellSize, + (selCol + 1) * cellSize, (selRow + 1) * cellSize, + selectedCellPaint); + + // Highlight row, column, and subgrid + for (int i = 0; i < SudokuGame.GRID_SIZE; i++) { + // Row & Col (excluding selected cell itself) + if (i != selCol) canvas.drawRect(i * cellSize, selRow * cellSize, (i + 1) * cellSize, (selRow + 1) * cellSize, highlightedCellPaint); + if (i != selRow) canvas.drawRect(selCol * cellSize, i * cellSize, (selCol + 1) * cellSize, (i + 1) * cellSize, highlightedCellPaint); + } + // Subgrid (excluding selected cell itself and already highlighted row/col cells) + int subgridStartRow = selRow - selRow % 3; + int subgridStartCol = selCol - selCol % 3; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + int curR = subgridStartRow + r; + int curC = subgridStartCol + c; + if (curR != selRow && curC != selCol) { // Avoid double-highlighting direct row/col + canvas.drawRect(curC * cellSize, curR * cellSize, + (curC + 1) * cellSize, (curR + 1) * cellSize, + highlightedCellPaint); + } + } + } + } + + + private void drawGridLines(Canvas canvas) { + int width = getWidth(); // or getHeight(), since it's square + + // Draw minor lines + for (int i = 0; i <= SudokuGame.GRID_SIZE; i++) { + float position = i * cellSize; + Paint currentPaint = (i % 3 == 0) ? majorGridPaint : minorGridPaint; + canvas.drawLine(0, position, width, position, currentPaint); // Horizontal + canvas.drawLine(position, 0, position, width, currentPaint); // Vertical + } + } + + private void drawNumbers(Canvas canvas) { + if (game == null) return; + + for (int r = 0; r < SudokuGame.GRID_SIZE; r++) { + for (int c = 0; c < SudokuGame.GRID_SIZE; c++) { + Cell cell = game.getCell(r, c); + if (cell != null && cell.getValue() != 0) { + String text = String.valueOf(cell.getValue()); + Paint currentTextPaint = cell.isStartingCell() ? startingNumberTextPaint : numberTextPaint; + + // Center text in cell + currentTextPaint.getTextBounds(text, 0, text.length(), textBounds); + float x = c * cellSize + (cellSize / 2f); + float y = r * cellSize + (cellSize / 2f) - textBounds.exactCenterY(); + canvas.drawText(text, x, y, currentTextPaint); + } + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (game == null) { + return super.onTouchEvent(event); + } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + if (cellSize > 0) { // Ensure cellSize is initialized + int col = x / cellSize; + int row = y / cellSize; + + if (col < SudokuGame.GRID_SIZE && row < SudokuGame.GRID_SIZE) { + game.selectCell(row, col); + if (cellSelectedListener != null) { + cellSelectedListener.onCellSelected(row, col); + } + invalidate(); // Request a redraw to show selection + return true; // Event handled + } + } + } + return super.onTouchEvent(event); + } + + // --- End of additions --- +} diff --git a/SudokuApp/app/src/main/res/drawable/ic_launcher_background.xml b/SudokuApp/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..b4c6944 --- /dev/null +++ b/SudokuApp/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/SudokuApp/app/src/main/res/drawable/ic_launcher_foreground.xml b/SudokuApp/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..4493888 --- /dev/null +++ b/SudokuApp/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,11 @@ + + + + diff --git a/SudokuApp/app/src/main/res/layout/activity_main.xml b/SudokuApp/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..aaa8e41 --- /dev/null +++ b/SudokuApp/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + +