Sūdoku
Mix.install([
{:kino, "~> 0.11.1"}
])
Introduction
Sūdoku is a puzzle-style game rooted in a grid that has $n^2 \times n^2$ cells. The fields are split into $n$ rectangular groups of $n \times n$ cells. These groups are perfectly overlaid the grid in a $n \times n$ pattern.
A solved puzzle adheres to the following three rules:
- Each row must have all numbers $1..n^2$ present.
- Each column must have all numbers $1..n^2$ present.
- Each group must have all numbers $1..n^2$ present.
An unsolved puzzle is a solved puzzle where a subset of the cells have been blanked out.
This LiveBook implements the common 3-Sūdoku. That is a Sūdoku where $n=3$. Such puzzles deal with a $9 \times 9$ grid where 9 occurrences of each $1..9$ number needs to be placed.
Sample Puzzles
sample_puzzles = [
"4...3.......6..8..........1....5..9..8....6...7.2........1.27..5.3....4.9........",
"7.8...3.....2.1...5.........4.....263...8.......1...9..9.6....4....7.5...........",
"7.8...3.....6.1...5.........4.....263...8.......1...9..9.2....4....7.5...........",
"3.7.4...........918........4.....7.....16.......25..........38..9....5...2.6.....",
"5..7..6....38...........2..62.4............917............35.8.4.....1......9...."
]
Puzzle Module
Module for loading, representing, solving, validation of solution and visualizing of Sūdoku puzzles:
defmodule Puzzle do
@size 600
@border 10
@fontsize 28
defstruct data: nil
# interface functions
def from_string(string) do
data =
string
|> String.graphemes()
|> Enum.map(fn g -> if g == ".", do: nil, else: Integer.parse(g) |> elem(0) end)
|> Enum.chunk_every(9)
%Puzzle{data: data}
end
def as_svg(puzzle) do
lines =
1..8
|> Enum.map(fn i ->
dyn = @border + i * (@size - 2 * @border) / 9
sw = if rem(i, 3) == 0, do: 4, else: 2
~c"\n" ++
~c"\n"
end)
labels =
puzzle.data
|> Enum.with_index(fn row, y ->
row
|> Enum.with_index(fn cell, x ->
if cell == "." do
nil
else
"""
#{cell}
"""
end
end)
end)
|> List.flatten()
|> Enum.filter(fn label -> not (label == nil) end)
|> Enum.join()
"""
#{lines}
#{labels}
"""
end
def solve(puzzle) do
data = puzzle.data
unsolved = find_unsolved(data)
result = solve(data, unsolved)
case result do
{:solved, data} ->
{:ok, %{puzzle | data: data}}
nil ->
{:failure, puzzle}
end
end
def solved?(puzzle) do
data = puzzle.data
0..8
|> Enum.all?(fn i ->
[
get_occupation(data, nil, i, :horizontal),
get_occupation(data, i, nil, :vertical),
get_occupation(data, 3 * rem(i, 3), 3 * div(i, 3), :block)
]
|> Enum.all?(fn occupation -> MapSet.size(occupation) == 9 end)
end)
# {:todo, puzzle}
end
# utility functions
defp solve(data, []) do
{:solved, data}
end
defp solve(data, [{x, y} | rest]) do
get_candidates(data, x, y)
|> Enum.find_value(fn candidate ->
result =
set_index(data, x, y, candidate)
|> solve(rest)
case result do
{:solved, data} ->
{:solved, data}
failure ->
failure
end
end)
end
defp find_unsolved(data) do
data
|> Enum.with_index(fn element, y ->
element
|> Enum.with_index(fn cell, x ->
if cell == nil do
{x, y}
else
nil
end
end)
end)
|> List.flatten()
|> Enum.filter(fn e -> not (e == nil) end)
end
defp get_index(data, x, y) do
data
|> Enum.fetch!(y)
|> Enum.fetch!(x)
end
defp set_index(data, x, y, value) do
data
|> List.replace_at(
y,
List.replace_at(
Enum.fetch!(data, y),
x,
value
)
)
end
defp get_candidates(data, x, y, candidates \\ nil) do
candidates = if candidates == nil, do: MapSet.new(1..9), else: candidates
horizontal_occupation = get_occupation(data, x, y, :horizontal)
vertical_occupation = get_occupation(data, x, y, :vertical)
block_occupation = get_occupation(data, x, y, :block)
MapSet.difference(
candidates,
MapSet.union(horizontal_occupation, MapSet.union(vertical_occupation, block_occupation))
)
|> MapSet.to_list()
end
defp get_occupation(data, x, y, rule) do
0..8
|> Enum.map(fn i ->
{x, y} =
case rule do
:horizontal ->
{i, y}
:vertical ->
{x, i}
:block ->
{xorig, yorig} = {3 * div(x, 3), 3 * div(y, 3)}
{xdiff, ydiff} = {rem(i, 3), div(i, 3)}
{xorig + xdiff, yorig + ydiff}
end
get_index(data, x, y)
end)
|> Enum.filter(fn entry -> not (entry == nil) end)
|> MapSet.new()
end
end
Load a single puzzle into p0
and illustrate it:
p0 = Puzzle.from_string(Enum.fetch!(sample_puzzles, 0))
p0
|> Puzzle.as_svg()
|> Kino.Image.new(:svg)
Solving
Attempt to solve and measure the number of seconds it takes:
prev = System.monotonic_time(:millisecond)
result = Puzzle.solve(p0)
next = System.monotonic_time(:millisecond)
diff = (next - prev) / 1000
Was the result an :ok
or a :failure
?
{status, p0solved} = result
status
What does the solution look like?
p0solved
|> Puzzle.as_svg()
|> Kino.Image.new(:svg)
Validation
Does the resulting solution adhere to the rules of Sūdoku?
Puzzle.solved?(p0solved)
Generation
This is left as an exercise 😏