Introduction
This article is on the implementation of a Sudoku game in Java. This version includes an intuitive interface with the ability to use help and to check for errors. Turning on help will mark all possible fields for the selected number. After checking for errors, the program marks valid fields green and invalid fields red. The rules used in this implementation are as follows:
- An integer may only appear once in ...
- ... the same row.
- ... the same column.
- ... the same 3x3 region.
- A game has only one solution.
Implementation
Model
The most important part of this application is in the Game
class, which includes the following functionality:
- Generate a new solution;
- Generate a new game from a solution;
- Keep track of user input;
- Check user input against generated solution;
- Keep track of selected number;
- Keep track of help is on or off.
Because the Game
class extends Observable
, it can and does notify observers when certain changes have been performed. This particular application contains two observers, ButtonPanel
and SudokuPanel
. When the Game
class executes setChanged()
followed by notifyObservers(...)
, the observers will execute their update(...)
method.
Besides the Game
class, the model consists of an enumeration called UpdateAction
which will tell observers what kind of update has taken place.
Generate Solution
Before we can start generating a game, we must first generate a solution. This is achieved by the method below, which needs to be called by the user as generateSudoku(new int[9][9], 0)
. The following steps are taken:
- Check if a solution is found.
- Found -> solution is returned.
- Not found -> continue.
- X (of current field) is found by finding the remainder of the division of the current index by the count of fields in a row using the modulo operation.
- Y (of current field) is found by dividing the current index by the count of fields in a row.
- An
ArrayList
is filled with the numbers 1 to 9 and shuffled. Shuffling is important because otherwise you always get the same solution. - As long as there are numbers in the
ArrayList
, the following will be executed:
- The next possible number is obtained by the method
getNextPossibleNumber(int[][], int, int, List<Integer>)
, which will be explained later on. If there's no next possible number (return value of -1), null
is returned. - Found number is placed on the current location.
- The method is recursively called with an increase of the index, and the returning value is stored in a variable.
- If this variable is not
null
, it is returned; otherwise, the current location is put back to 0 (meaning the field is a blank).
null
is returned. This part will never be reached, hopefully.
private int[][] generateSolution(int[][] game, int index) {
if (index > 80)
return game;
int x = index % 9;
int y = index / 9;
List<Integer> numbers = new ArrayList<Integer>();
for (int i = 1; i <= 9; i++)
numbers.add(i);
Collections.shuffle(numbers);
while (numbers.size() > 0) {
int number = getNextPossibleNumber(game, x, y, numbers);
if (number == -1)
return null;
game[y][x] = number;
int[][] tmpGame = generateSolution(game, index + 1);
if (tmpGame != null)
return tmpGame;
game[y][x] = 0;
}
return null;
}
As previously said, the method getNextPossibleNumber(int[][], int, int, List<Integer>)
is used for obtaining the next possible number. It takes a number from the list and checks whether it is possible at the given x and y position in the given game. When found possible, the number is returned. If the list is empty and thus no number is possible at this location, -1 is returned.
private int getNextPossibleNumber(int[][] game, int x, int y, List<Integer> numbers) {
while (numbers.size() > 0) {
int number = numbers.remove(0);
if (isPossibleX(game, y, number)
&& isPossibleY(game, x, number)
&& isPossibleBlock(game, x, y, number))
return number;
}
return -1;
}
Generate Game
Generating a game is simply achieved by constantly removing a random field and making sure the game is still valid. Valid means there is only one solution. This is achieved by the methods below. The user should call the first method, which uses the second method. I will describe the steps again.
- A list is filled with all possible positions.
- The list is shuffled. I do not know why. I suspect that this way, the blanks are better distributed. With the result that the game is harder.
- The list is passed to the method
generateGame(int[][], List<Integer>)
and the return value will be returned.
private int[][] generateGame(int[][] game) {
List<Integer> positions = new ArrayList<Integer>();
for (int i = 0; i < 81; i++)
positions.add(i);
Collections.shuffle(positions);
return generateGame(game, positions);
}
- As long as there are positions in the list, the following will be executed:
- A position is taken from the list and stored in a variable.
- x and y are calculated from this position.
- Value at this position is stored in the variable
temp
. - Value at this position is set to 0 (meaning the field is a blank).
- This step is critical. As the removal of the value at the position means that the game is no longer valid, the value at the position is put back. Otherwise the game stays the same.
- The game is returned.
private int[][] generateGame(int[][] game, List<Integer> positions) {
while (positions.size() > 0) {
int position = positions.remove(0);
int x = position % 9;
int y = position / 9;
int temp = game[y][x];
game[y][x] = 0;
if (!isValid(game))
game[y][x] = temp;
}
return game;
}
As you can see, this method is used to pass default values. So why the new int[] { 0 }
? This is the most common way of passing an integer by reference, instead of by value.
private boolean isValid(int[][] game) {
return isValid(game, 0, new int[] { 0 });
}
A valid game has in every row, every column, and every region the numbers 1 to 9. Additionally, there should only be one solution existing. To achieve this, all open fields are filled with the first valid value. Even after finding a solution, the search continues by putting the next valid value in an open field. If a second solution is found, then the search will be stopped and the method returns false
. There will always be at least one solution (hence game
is an incomplete solution), so if there are less than two solutions, the game is valid and the method returns true
. Step by step:
- Check if a solution is found.
- Found -> increase
numberOfSolutions
and return true
if it equals 1; false
otherwise. - Not found -> continue.
- Calculate x and y from index.
- Check if current field is a blank (equals 0).
- True
- Fill a list with numbers 1 to 9.
- While the list contains numbers, execute the following:
- Get the next possible number. If the returned value equals -1, perform a break resulting a return of
true
. - Set this number at current field.
- Call this method recursively and instantly check against the returned value.
- True -> one or no solution found, continue search.
- False -> more than one solution found, stop searching. Restore game and return
false
.
- Restore value of current field to 0 (meaning it is blank).
- False
- Call this method recursively and instantly check against the returned value.
- True -> continue (resulting in returning
true
). - False -> return
false
.
- Return
true
.
private boolean isValid(int[][] game, int index, int[] numberOfSolutions) {
if (index > 80)
return ++numberOfSolutions[0] == 1;
int x = index % 9;
int y = index / 9;
if (game[y][x] == 0) {
List<Integer> numbers = new ArrayList<Integer>();
for (int i = 1; i <= 9; i++)
numbers.add(i);
while (numbers.size() > 0) {
int number = getNextPossibleNumber(game, x, y, numbers);
if (number == -1)
break;
game[y][x] = number;
if (!isValid(game, index + 1, numberOfSolutions)) {
game[y][x] = 0;
return false;
}
game[y][x] = 0;
}
} else if (!isValid(game, index + 1, numberOfSolutions))
return false;
return true;
}
Check Game
With checking user input, we compare each field in the game against the corresponding field in the solution. The result is stored in a two dimensional boolean array. The selected number is also set to 0 (meaning no number is selected). All observers are notified that the model has changed with the corresponding UpdateAction
.
public void checkGame() {
selectedNumber = 0;
for (int y = 0; y < 9; y++) {
for (int x = 0; x < 9; x++)
check[y][x] = game[y][x] == solution[y][x];
}
setChanged();
notifyObservers(UpdateAction.CHECK);
}
View & Controller
There is not much to say about the view part, only the structure of the panels. The controllers react to user input and implement changes to the model. The model notifies it has been changed. The view responses to this notification and updates. These updates of the view are limited to changing colors and changing the number of a field. There is no rocket science involved.
Sudoku
The view also is the entry point of this application; the Sudoku
class contains the main method. This class builds up the user interface by creating a JFrame
and placing SudokuPanel
and ButtonPanel
inside this frame. It also creates the Game
class, and adds SudokuPanel
and ButtonPanel
as observers to it.
SudokuPanel and Fields
SudokuPanel
contains 9 sub panels each containing 9 fields. All sub panels and fields are placed in a GridLayout
of 3x3. Each sub panel represents a region of a Sudoku game, and is primarily used for drawing a border visually separating each region. They also get the SudokuController
added to them, which is in charge of processing user input by mouse.
ButtonPanel
ButtonPanel
contains two panels with a titled border. The first panel contains three buttons. Button New for starting a new game, button Check for checking user input, and button Exit for exiting the application. The second panel contains 9 toggle buttons placed inside a button group. This way, they react like radio buttons. Clicking one of these toggle buttons will set the corresponding selected number in the Game
class.
Points of Interest
- For passing an integer by reference instead of by value, use an integer array.
isValid(game, 0, new int[] { 0 });
private boolean isValid(int[][] game, int index, int[] numberOfSolutions);
Finding the base x and y values of a region, use the following:
int x1 = x < 3 ? 0 : x < 6 ? 3 : 6;
int y1 = y < 3 ? 0 : y < 6 ? 3 : 6;
History
Apologies
One, if I have driven you mad with my bad English, I apologize. Two, if I have driven you mad before because usually my source doesn't contain comments, I apologize, and I can tell you, in this case, it does. Not because I acknowledge their usefulness, but to avoid discussions. Three, if I didn't mention before that you can use your right mouse button to clear a field, I apologize.