# A Custom Sudoku Widget

Creating a custom Jupyter widget for editing and displaying Sudoku puzzles.
programming
python
puzzles
Author

Aswin van Woudenberg

Published

February 26, 2021

In this post I’ll demonstrate how to build a custom Jupyter widget for displaying and editing Sudoku puzzles. I’ll also show how to create a Sudoku solver that uses this widget.

## How to play Sudoku

In Sudoku, the objective is to fill a 9x9 grid with digits so that each column, each row, and each of the nine 3x3 blocks that compose the grid contain all of the digits from 1 to 9.

An example Sudoku puzzle:

 8 5 6 1 9 4 2 3 8 4 2 7 9 5 3 8 5 8 7 1 6 4

The solution to this puzzle looks like this:

 3 8 5 9 6 1 4 2 7 9 2 4 8 7 3 1 5 6 1 6 7 5 4 2 3 9 8 5 4 3 1 8 7 9 6 2 7 1 8 2 9 6 5 4 3 2 9 6 4 3 5 8 7 1 4 7 1 6 5 8 2 3 9 8 3 9 7 2 4 6 1 5 6 5 2 3 1 9 7 8 4

The first row (3 8 5 9 6 1 4 2 7) contains all digits from 1 to 9. Also the first column (3 9 1 5 7 2 4 8 6) contains all digits from 1 to 9, as does the first subblock (3 8 5 - 9 2 4 - 1 6 7) and all the other ones.

## Creating the widget

There are two ways to create Jupyter widgets - an easy way and a more complicated way. For this post, we will be using the easy way, which involves creating two cells in a Jupyter notebook. The first cell contains the Python code for the back-end of the widget, while the second cell contains the JavaScript for the front-end.

If you want to create a proper Python package that can be installed with `pip install`, you can follow the more complicated way. A good resource is this tutorial.

For this post, we’ll stick with the easy way.

### The Python back-end

The following code defines a Python class named `Sudoku` that extends the `DOMWidget` class from the `ipywidgets` library.

``````from traitlets import Unicode, Bool, Int, List, validate, observe, TraitError, All
from ipywidgets import DOMWidget, register
import copy

@register
class Sudoku(DOMWidget):
_view_name = Unicode('SudokuView').tag(sync=True)
_view_module = Unicode('sudoku_widget').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)

# Attributes
fixed = List(trait=Bool(), default_value=[False] * 81, minlen=81, maxlen=81, help="A list of booleans that indicate whether a value is part of the puzzle.").tag(sync=True)
_value = List(trait=Int(), default_value=[0] * 81, minlen=81, maxlen=81, help="A list of integers for each cell.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)

# Basic validator for value
@validate('_value')
def _valid_value(self, proposal):
for i in proposal['value']:
if i < 0 or i > 9:
raise TraitError('Invalid value: all elements must be numbers from 0 to 9')
return proposal['value']

@property
def value(self):
return copy.deepcopy(self._value)

@value.setter
def value(self, v):
self._value = v

def __init__(self,*args,**kwargs):
kwargs['_value'] = kwargs.pop('value', [0]*81)
DOMWidget.__init__(self,*args,**kwargs)

def __getitem__(self,index):
return self._value[index]

def __setitem__(self,index,val):
vals = self.value
vals[index] = val
self._value = vals``````

This Sudoku class has the following attributes:

• `value`: A list of integers that represents the current state of the puzzle.
• `fixed`: A list of booleans that indicates whether a value is part of the original puzzle and cannot be changed by the user.
• `disabled`: A boolean that enables or disables user changes to the puzzle.

The `fixed` and `value` attributes are defined using the `List` trait from the `traitlets` library. The `validate` decorator is used to define a validator for the `value` attribute that checks that all elements are numbers from 0 to 9.

The `__getitem__` and `__setitem__` methods are implemented to allow indexing and assignment of elements in the `value` attribute.

The `@register` decorator registers the `Sudoku` class as an `ipywidget`, which allows it to be displayed and interacted with in a Jupyter environment.

### The JavaScript front-end

The front-end contains a bit more code.

``````%%javascript
require.undef('sudoku_widget');

define('sudoku_widget', ["@jupyter-widgets/base"], function(widgets) {

// Define the SudokuView
class SudokuView extends widgets.DOMWidgetView {

// Render the view.
render() {
this.sudoku_table = document.createElement('table');
this.sudoku_table.style.borderCollapse = 'collapse';
this.sudoku_table.style.marginLeft = '0';

for (let i=0; i<3; i++) {
let colgroup = document.createElement('colgroup');
colgroup.style.border = 'solid medium';
for (let j=0; j<3; j++) {
let col = document.createElement('col');
col.style.border = 'solid thin';
col.style.width = '2em';
colgroup.appendChild(col);
}
this.sudoku_table.appendChild(colgroup);
}

for (let t=0; t<3; t++) {
let tbody = document.createElement('tbody');
tbody.style.border = 'solid medium';
for (let r=0; r<3; r++) {
let tr = document.createElement('tr');
tr.style.height = '2em';
tr.style.border = 'solid thin';
for (let c=0; c<9; c++) {
let td = document.createElement('td');
tr.appendChild(td);
}
tbody.appendChild(tr);
}
this.sudoku_table.appendChild(tbody);
}

this.el.appendChild(this.sudoku_table);

this.model_changed();

// Python -> JavaScript update
this.model.on('change', this.model_changed, this);
}

model_changed() {
let tds = this.sudoku_table.getElementsByTagName('td');
let disabled = this.model.get('disabled');

for (let i=0; i < 81; i++) {
let td = tds[i];
td.innerText = ''; // Delete td contents
td.style.textAlign = 'center';
td.style.height = '2em';
let value = this.model.get('_value')[i];
let fixed = this.model.get('fixed')[i];

if (fixed && value > 0) {
let b = document.createElement('b');
b.innerText = value;
td.appendChild(b);
} else if (disabled && value > 0) {
td.innerText = value;
} else if (!disabled && !fixed) {
let input = document.createElement('input');
input.type = 'text';
input.maxLength = 1;
input.style.top = 0;
input.style.left = 0;
input.style.margin = 0;
input.style.height = '100%';
input.style.width = '100%';
input.style.border = 'none';
input.style.textAlign = 'center';
input.style.marginTop = 0;
input.value = (value > 0 ? value : '');
input.oninput = this.input_input.bind(this, i);
input.onchange = this.input_changed.bind(this, i); // JavaScript -> Python update
td.appendChild(input);
}
}

}

input_input(i) {
this.sudoku_table.getElementsByTagName('td')[i].getElementsByTagName('input')[0].value =
this.sudoku_table.getElementsByTagName('td')[i].
getElementsByTagName('input')[0].value.replace(/[^1-9]/g,'');
}

input_changed(i) {
this.sudoku_table.getElementsByTagName('td')[i].getElementsByTagName('input')[0].value =
this.sudoku_table.getElementsByTagName('td')[i].
getElementsByTagName('input')[0].value.replace(/[^1-9]/g,'');
let v = parseInt(this.sudoku_table.getElementsByTagName('td')[i].getElementsByTagName('input')[0].value) || 0;
let value = this.model.get('_value').slice();
value[i] = v;
this.model.set('_value', value);
this.model.save_changes();
}

}

return {
SudokuView: SudokuView
}

});``````

The `define` function defines the `sudoku_widget` module, which depends on the `@jupyter-widgets/base` module. It creates a `SudokuView` class that extends the base class `widgets.DOMWidgetView`, which is responsible for rendering and updating the widget.

The `render` method of the `SudokuView` class creates a table element with 9 rows and 9 columns, representing the Sudoku game board. It adds the table to the widget’s HTML element, and registers a listener for model changes. The `model_changed` method is called when the model changes, and it updates the widget’s HTML to reflect the new model state.

The `input_input` and `input_changed` methods are event handlers that respond to user input on the Sudoku board. They update the model and the widget’s HTML to reflect the new user input.

## How to use this widget

Once we have executed these two cells, we’re good to use our widget.

``````import ipywidgets as widgets

puzzle = [
0,8,5, 0,6,1, 0,0,0,
9,0,4, 0,0,0, 0,0,0,
0,0,0, 0,0,2, 3,0,8,

0,4,0, 0,0,0, 0,0,2,
7,0,0, 0,9,0, 5,0,0,
0,0,0, 0,3,0, 8,0,0,

0,0,0, 0,5,8, 0,0,0,
0,0,0, 7,0,0, 0,1,0,
6,0,0, 0,0,0, 0,0,4]

fixed_digits = [v > 0 for v in puzzle]

sudoku = Sudoku(value=puzzle, fixed=fixed_digits, disabled=False)

display(sudoku)``````

The widget accepts three parameters: `value`, `fixed` and `disabled`. The parameter `value` is a list of digits. A digit of 0 means empty. The parameter `fixed` is a list of boolean values, where `True` means a digit can’t be edited and will be printed in bold. The boolean `disabled` indicates whether a user can edit digits.

One can read the values in a grid like this:

``print(sudoku.value)``
``[0, 8, 5, 0, 6, 1, 0, 0, 0, 9, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 8, 0, 4, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 0, 9, 0, 5, 0, 0, 0, 0, 0, 0, 3, 0, 8, 0, 0, 0, 0, 0, 0, 5, 8, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 1, 0, 6, 0, 0, 0, 0, 0, 0, 0, 4]``

Running the next cell would show the solution by updating the widget.

``````solution = [
3,8,5, 9,6,1, 4,2,7,
9,2,4, 8,7,3, 1,5,6,
1,6,7, 5,4,2, 3,9,8,

5,4,3, 1,8,7, 9,6,2,
7,1,8, 2,9,6, 5,4,3,
2,9,6, 4,3,5, 8,7,1,

4,7,1, 6,5,8, 2,3,9,
8,3,9, 7,2,4, 6,1,5,
6,5,2, 3,1,9, 7,8,4]

sudoku.value = solution``````

## Creating a Sudoku solver

Now that we have this widget to our disposal, we’ll create a Sudoku solver.

### Building the user interface

First, let’s tackle the easy part: creating the user interface for our Sudoku solver. We’ll use the Sudoku widget along with some other widgets to make it easy for the user to select from pre-made puzzles or enter their own.

``````puzzle1 = [
0,8,5, 0,6,1, 0,0,0,
9,0,4, 0,0,0, 0,0,0,
0,0,0, 0,0,2, 3,0,8,

0,4,0, 0,0,0, 0,0,2,
7,0,0, 0,9,0, 5,0,0,
0,0,0, 0,3,0, 8,0,0,

0,0,0, 0,5,8, 0,0,0,
0,0,0, 7,0,0, 0,1,0,
6,0,0, 0,0,0, 0,0,4]

puzzle2 = [
3,6,0, 0,0,0, 0,0,5,
0,1,0, 0,9,0, 2,0,8,
0,5,0, 1,8,0, 0,0,7,

5,0,0, 0,0,6, 4,0,0,
2,4,6, 0,5,0, 7,0,0,
0,0,0, 0,7,0, 0,0,0,

0,0,0, 0,0,7, 1,0,3,
0,0,3, 9,4,0, 0,0,0,
0,0,0, 0,0,1, 0,0,0]

puzzle3 = [
0,2,0, 0,4,0, 0,0,5,
0,5,8, 0,0,0, 0,0,0,
0,1,0, 8,0,0, 4,0,0,

7,0,0, 0,0,8, 0,4,0,
0,0,1, 9,0,5, 7,0,0,
0,3,0, 7,0,0, 0,0,2,

0,0,4, 0,0,3, 0,1,0,
0,0,0, 0,0,0, 9,6,0,
2,0,0, 0,1,0, 0,5,0
]

sudoku = Sudoku(value=puzzle1, fixed=[v > 0 for v in puzzle1], disabled=False)
example_dropdown = widgets.Dropdown(
options=[('Empty', [0] * 81), ('Example 1', puzzle1), ('Example 2', puzzle2), ('Example 3', puzzle3)],
value=puzzle1,
layout=widgets.Layout(margin='10px 0px 0px 20px', width='150px')
)
solve_button = widgets.Button(
description="Solve",
layout=widgets.Layout(margin='20px 0px 0px 20px', width='150px')
)
next_button = widgets.Button(
description="Next",
layout=widgets.Layout(margin='20px 0px 0px 20px', width='150px', display='none')
)
vbox = widgets.VBox([example_dropdown, solve_button, next_button])
hbox = widgets.HBox([sudoku, vbox])
label = widgets.Label()``````

The `Sudoku` widget displays a Sudoku board.

There is also a `Dropdown` widget for selecting pre-made puzzles or an empty board, and two `Button` widgets for solving the puzzle and showing the next solution (if there are multiple solutions).

Finally, there is a `Label` widget that can be used to display messages to the user. All of these widgets are arranged in a layout using `VBox` and `HBox` widgets.

### Writing the event handlers

The widgets are not functional on their own; we need to write code to make them responsive to user input.

``````# global variables
gen = None
solution = None

def on_example_dropdown_change(change):
if change['type'] == 'change' and change['name'] == 'value':
value = change['new']
fixed = [v > 0 for v in value]
sudoku.value = value
sudoku.fixed = fixed
label.value = ""
solve_button.layout.display = 'inline-block'
next_button.layout.display = 'none'

example_dropdown.observe(on_example_dropdown_change)

def on_solve_button_clicked(b):
global gen
global solution

val = sudoku.value.copy()
sudoku.fixed = [v > 0 for v in val]
gen = solve_sudoku(val)
try:
solution = next(gen)
sudoku.value = solution
except StopIteration:
label.value = "This sudoku has no solution."
sudoku.fixed = [False] * 81
return

try:
solution = next(gen).copy()
label.value = "This sudoku has multiple solutions."
solve_button.layout.display = 'none'
next_button.layout.display = 'inline-block'
except StopIteration:
label.value = ""
solve_button.layout.display = 'none'

solve_button.on_click(on_solve_button_clicked)

def on_next_button_clicked(b):
global gen
global solution

sudoku.value = solution
try:
solution = next(gen)
except StopIteration:
label.value = ""
next_button.layout.display = 'none'

next_button.on_click(on_next_button_clicked)``````

The `on_example_dropdown_change` function is called when the user selects an example puzzle from a dropdown menu, and it sets up the Sudoku grid with the selected puzzle and clears any previous solutions.

The `on_solve_button_clicked` function is called when the user clicks a button to solve the puzzle, and it generates a Sudoku solver object and attempts to find a solution to the puzzle. If a solution is found, it updates the Sudoku grid with the solution and enables a button to find the next solution if there are multiple solutions. If no solution is found, it displays an error message.

The `on_next_button_clicked` function is called when the user clicks the “next” button to find the next solution to a puzzle with multiple solutions, and it updates the Sudoku grid with the next solution if there is one, or disables the “next” button if there are no more solutions.

The `gen` and `solution` variables are used to keep track of the state of the Sudoku solver object and the next solution.

### The solver

We can easily solve puzzles using backtracking. The `solve_sudoku` function utilizes recursion to generate solutions. It is called by the `on_solve_button_clicked` function above.

``````def solve_sudoku(puzzle, index=0):
if index == 81:
# Solution found
yield puzzle
elif puzzle[index] > 0:
yield from solve_sudoku(puzzle, index + 1)
else:
for v in range(1,10):
# Fill in a digit and check constraints
puzzle[index] = v
if is_valid_square(puzzle, index):
yield from solve_sudoku(puzzle, index + 1)
puzzle[index] = 0``````

The `solve_sudoku` function takes in a puzzle parameter which is a list of length 81, representing the 9x9 Sudoku grid with empty squares represented as 0s. The function yields solutions as they are found.

The following functions are used to check the constraints.

``````def get_column(puzzle, k):
column = []
for i in range(9):
column.append(puzzle[i*9 + k])
return column

def get_row(puzzle, r):
return puzzle[r*9:(r+1)*9]

def get_block(puzzle, b):
block = []
for r in range(3):
for k in range(3):
block.append(puzzle[[0,3,6,27,30,33,54,57,60][b]+9*r+k])
return block

def is_valid(l):
# Check for duplicate values
digits = [v for v in l if v > 0]
s = set(digits)
return len(digits) == len(s)

def is_valid_square(puzzle, i):
k = i % 9
r = int(i / 9)
b = int(r / 3) * 3 + int(k / 3)

return is_valid(get_row(puzzle, r)) and is_valid(get_column(puzzle, k)) and is_valid(get_block(puzzle, b))``````

The `get_column`, `get_row`, and `get_block` functions are used to retrieve the values in the columns, rows, and 3x3 blocks that a given index belongs to.

The `is_valid` function checks if a list of values contains duplicate values. It returns `True` if the list contains no duplicates (excluding 0s) and `False` otherwise.

The `is_valid_square` function checks if a value can be placed in a given square of the Sudoku grid without violating the rules of the game. It uses the `get_row`, `get_column`, `get_block`, and `is_valid` functions.

### Displaying the user interface

It’s time to show the user interface.

``display(hbox, label)``

To play with an interactive version, you’ll need to run it in Jupyter Notebook. Sadly, it won’t work in JupyterLab. It does work in Voilà in case you wish to turn it into a web app. The corresponding gist can be found here.

## Conclusion

In this post, we saw how to create a custom widget in Jupyter Notebook. It’s worth noting that the approach I presented here is more of a quick fix. Originally, I developed this code as part of a Sudoku programming assignment for my students, and I had control over the environment they were using (Jupyter Notebook and Voila).

For creating Jupyter widgets, it is recommended to use widget-cookiecutter (for JavaScript) or widget-ts-cookiecutter (for TypeScript). These tools offer a more robust and reliable approach to building widgets in Jupyter.