OverTheWire Advent Bonanza 2019 Challenge 5 - Sudo Sudoku Writeup

Another writeup for a fairly simple challenge as a part of OverTheWire’s Advent Bonanza 2019. Day 5 of the Advent Bonanza challenges was a straightforward Sudoku challenge with constraints.

Sudo Sudoku
Points: 86
Solves: 229
Santa’s little helpers are notoriously good at solving Sudoku puzzles, so they made a special variant…
Download: 2a3fad4ea8987eb63b5abdea1c8bdc75d4f2e6b087388c5e33cec99136b4257a-sudosudoku.tar.xz


Downloading and extracting the file gave the following:

challenge.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Santa's little helpers are notoriously good at solving Sudoku puzzles.
Because regular Sudoku puzzles are too trivial, they have invented a variant.

1 2 3 4 5 6 7 8 9
+-------+-------+-------+
A | . . . | . . . | . . 1 |
B | . 1 2 | . . . | . . . |
C | . . . | . . . | 2 . . |
+-------+-------+-------+
D | . . . | . . . | . . 2 |
E | . 2 . | . . . | . . . |
F | . . . | . . . | . . . |
+-------+-------+-------+
G | . . . | . . . | 1 2 . |
H | 1 . . | . . 2 | . . . |
I | . . . | 1 . . | . . . |
+-------+-------+-------+

In addition to the standard Sudoku puzzle above,
the following equations must also hold:

B9 + B8 + C1 + H4 + H4 = 23
A5 + D7 + I5 + G8 + B3 + A5 = 19
I2 + I3 + F2 + E9 = 15
I7 + H8 + C2 + D9 = 26
I6 + A5 + I3 + B8 + C3 = 20
I7 + D9 + B6 + A8 + A3 + C4 = 27
C7 + H9 + I7 + B2 + H8 + G3 = 31
D3 + I8 + A4 + I6 = 27
F5 + B8 + F8 + I7 + F1 = 33
A2 + A8 + D7 + E4 = 21
C1 + I4 + C2 + I1 + A4 = 20
F8 + C1 + F6 + D3 + B6 = 25

If you then read the numbers clockwise starting from A1 to A9, to I9, to I1 and
back to A1, you end up with a number with 32 digits. Enclose that in AOTW{...}
to get the flag.

Looks like all I have to do is solve the Sudoku puzzle with the constraints. What I could do is sit down and actually do the puzzle by hand, but I wanted to take this opportunity to show off an awesome tool known as The Z3 Theorem Prover from Microsoft Research. Z3 uses a variety of solvers to solve equations under constraints. This is perfect here as there are already open source sudoku solvers online that use Z3. A quick online search for sudoku solver z3 and the first link come up with this Github project by Github user ppmx. Credits to this user for a very easy to use and easy to understand sudoku solver using Z3! Here’s what their sudoku solver script looks like:

sudoku-z3.py from Github user ppmx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3

def cross(A, B):
return [(a + b) for a in A for b in B]

class Sudoku:
@staticmethod
def parse_grid(puzzle):
"""
A1 A2 A3 | A4 A5 A6 | A7 A8 A9
B1 B2 B3 | B4 B5 B6 | B7 B8 B9
C1 C2 C3 | C4 C5 C6 | C7 C8 C9
–––––––––+––––––––––+–––––––––
D1 D2 D3 | D4 D5 D6 | D7 D8 D9
E1 E2 E3 | E4 E5 E6 | E7 E8 E9
F1 F2 F3 | F4 F5 F6 | F7 F8 F9
–––––––––+––––––––––+–––––––––
G1 G2 G3 | G4 G5 G6 | G7 G8 G9
H1 H2 H3 | H4 H5 H6 | H7 H8 H9
I1 I2 I3 | I4 I5 I6 | I7 I8 I9
puzzle = 'A1A2A3A4...' and every element holds a value of '123456789.'
where the dot represents an empty cell.
"""

s = Sudoku()

if any(c not in "123456789." for c in puzzle) or len(puzzle) != 81:
raise Exception("got invalid puzzle format")

elements = cross("ABCDEFGHI", "123456789")
s.values = {e: v for e,v in zip(elements, puzzle)}
return s

def __init__(self, values=dict()):
# mapping cells -> "123456789." where the dot represents an empty cell
# cells = cross product of "ABCDEFGHI" and "123456789"
self.values = values

# we define some additional informations that may be used by a solving function:
rows, cols = "ABCDEFGHI", "123456789"
self.elements = cross(rows, cols)

self.unitlist = []
self.unitlist += [cross(rows, c) for c in cols]
self.unitlist += [cross(r, cols) for r in rows]
self.unitlist += [cross(rs, cs) for rs in ["ABC", "DEF", "GHI"] for cs in ["123", "456", "789"]]

self.units = {e: [u for u in self.unitlist if e in u] for e in self.elements}

def is_solved(self):
# assure that every cell holds a single value between 1 and 9:
if not all(k in "123456789" for k in self.values.values()):
return False

# assure that every cell of every unit is unique in the proper unit:
unitsolved = lambda u: set([self.values[e] for e in u]) == set("123456789")
return all(unitsolved(u) for u in self.unitlist)

def __str__(self):
lines, elements = [], cross("ABCDEFGHI", "123456789")

print("[+] Puzzle:", ''.join(self.values[e] for e in elements))

for index_row, row in enumerate("ABCDEFGHI"):
if index_row % 3 == 0:
lines.append("+–––––––––+–––––––––+–––––––––+")

line = ''
for index_col, col in enumerate("123456789"):
line += "{1} {0} ".format(self.values[row + col], '|' if index_col % 3 == 0 else '')
lines.append(line + '|')

lines.append("+–––––––––+–––––––––+–––––––––+")
return '\n'.join(lines) + '\n'

def Z3Solving(sudoku):
from z3 import Solver, Int, Or, Distinct, sat

elements = cross("ABCDEFGHI", "123456789")
symbols = {e: Int(e) for e in elements}

# first we build a solver with the general constraints for sudoku puzzles:
s = Solver()

# assure that every cell holds a value of [1,9]
for symbol in symbols.values():
s.add(Or([symbol == i for i in range(1, 10)]))

# assure that every row covers every value:
for row in "ABCDEFGHI":
s.add(Distinct([symbols[row + col] for col in "123456789"]))

# assure that every column covers every value:
for col in "123456789":
s.add(Distinct([symbols[row + col] for row in "ABCDEFGHI"]))

# assure that every block covers every value:
for i in range(3):
for j in range(3):
s.add(Distinct([symbols["ABCDEFGHI"[m + i * 3] + "123456789"[n + j * 3]] for m in range(3) for n in range(3)]))

# now we put the assumptions of the given puzzle into the solver:
for elem, value in sudoku.values.items():
if value in "123456789":
s.add(symbols[elem] == value)

if not s.check() == sat:
raise Exception("unsolvable")

model = s.model()
values = {e: model.evaluate(s).as_string() for e, s in symbols.items()}
return Sudoku(values)

def main(puzzle):
print("[+] processing puzzle:", puzzle)

s = Sudoku.parse_grid(puzzle)
print(s)

print("[+] trying to solve it with z3")
s_solved = Z3Solving(s)
print("[+] it is solved:", s_solved.is_solved())
print(s_solved)

if __name__ == "__main__":
from sys import argv

if len(argv) != 2:
print("[!] we are using some test puzzle due to missing argument")
main("4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......")
else:
main(argv[1])

I am far from an advanced user of Z3 but the above script is straightforward enough to understand. This guide by Github user ericpony is also a great place to start to understand how to use Z3 in Python.

  • Z3 needs to know what kind of values it needs to solve for. We also give symbols for those values, basically like variables.
    • This happens on line 79-80 where values A1-A9, B1-B9, etc. are added as symbols.
  • Line 83 initializes the Z3 solver.
  • Line 77 shows imports from Z3 that can be used to define some of the relationships between variables, like Or and Distinct.
  • Constraints are added to the solver object and then Solver.check() is called to tell Z3 to try to solve for all the constraints given.

To input my own constraints into the script, I had to understand what the script was doing first. The function Z3Solving is where all the magic happens.

  • Lines 86-87 ensure that all values in the solution are between 1 and 9 inclusive.
  • Lines 90-100 ensure that every row, column, and 3x3 block include all values 1 to 9 inclusive and are distinct.
  • Lines 103-105 ensure that the starting board that is given to the program is satisfied.

For solving this problem, it seems like it would be adequate to place my custom constraints right after line 100.

To make it easier for myself, I replaced the string on line 130 with the starting sudoku problem given to me in the challenge. I saved the script as solve.py and ran it just to make sure I was able to get a solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ python3 solve.py 
[!] we are using some test puzzle due to missing argument
[+] processing puzzle: ........1.12............2..........2.2......................12.1....2......1.....
[+] Puzzle: ........1.12............2..........2.2......................12.1....2......1.....
+–––––––––+–––––––––+–––––––––+
| . . . | . . . | . . 1 |
| . 1 2 | . . . | . . . |
| . . . | . . . | 2 . . |
+–––––––––+–––––––––+–––––––––+
| . . . | . . . | . . 2 |
| . 2 . | . . . | . . . |
| . . . | . . . | . . . |
+–––––––––+–––––––––+–––––––––+
| . . . | . . . | 1 2 . |
| 1 . . | . . 2 | . . . |
| . . . | 1 . . | . . . |
+–––––––––+–––––––––+–––––––––+

[+] trying to solve it with z3
[+] it is solved: True
[+] Puzzle: 765284931912635874843791265439857612527916483681423597374568129196342758258179346
+–––––––––+–––––––––+–––––––––+
| 7 6 5 | 2 8 4 | 9 3 1 |
| 9 1 2 | 6 3 5 | 8 7 4 |
| 8 4 3 | 7 9 1 | 2 6 5 |
+–––––––––+–––––––––+–––––––––+
| 4 3 9 | 8 5 7 | 6 1 2 |
| 5 2 7 | 9 1 6 | 4 8 3 |
| 6 8 1 | 4 2 3 | 5 9 7 |
+–––––––––+–––––––––+–––––––––+
| 3 7 4 | 5 6 8 | 1 2 9 |
| 1 9 6 | 3 4 2 | 7 5 8 |
| 2 5 8 | 1 7 9 | 3 4 6 |
+–––––––––+–––––––––+–––––––––+

Looks like it works! Now all that is left to do is add all of the constraints from the challenge. What was also nice is that the script from ppmx and the challenge refer to each individual cell of the sudoku board in the same manner. For example, I5 on the sudoku board in the challenge is referenced by symbols['I5'] in the script. Solving this challenge was as easy as adding my own constraints after line 100. I actually wrote a script to take in the constraints and output the following Python code cause I got too lazy to type it all out myself.

1
2
3
4
5
6
7
8
9
10
11
12
13
# assure that special constraints are met
s.add(symbols['B9'] + symbols['B8'] + symbols['C1'] + symbols['H4'] + symbols['H4'] == 23)
s.add(symbols['A5'] + symbols['D7'] + symbols['I5'] + symbols['G8'] + symbols['B3'] + symbols['A5'] == 19)
s.add(symbols['I2'] + symbols['I3'] + symbols['F2'] + symbols['E9'] == 15)
s.add(symbols['I7'] + symbols['H8'] + symbols['C2'] + symbols['D9'] == 26)
s.add(symbols['I6'] + symbols['A5'] + symbols['I3'] + symbols['B8'] + symbols['C3'] == 20)
s.add(symbols['I7'] + symbols['D9'] + symbols['B6'] + symbols['A8'] + symbols['A3'] + symbols['C4'] == 27)
s.add(symbols['C7'] + symbols['H9'] + symbols['I7'] + symbols['B2'] + symbols['H8'] + symbols['G3'] == 31)
s.add(symbols['D3'] + symbols['I8'] + symbols['A4'] + symbols['I6'] == 27)
s.add(symbols['F5'] + symbols['B8'] + symbols['F8'] + symbols['I7'] + symbols['F1'] == 33)
s.add(symbols['A2'] + symbols['A8'] + symbols['D7'] + symbols['E4'] == 21)
s.add(symbols['C1'] + symbols['I4'] + symbols['C2'] + symbols['I1'] + symbols['A4'] == 20)
s.add(symbols['F8'] + symbols['C1'] + symbols['F6'] + symbols['D3'] + symbols['B6'] == 25)

Now running solve.py gave the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[+] trying to solve it with z3
[+] it is solved: True
[+] Puzzle: 864729531912453768375618249649875312721936854538241697486597123197362485253184976
+–––––––––+–––––––––+–––––––––+
| 8 6 4 | 7 2 9 | 5 3 1 |
| 9 1 2 | 4 5 3 | 7 6 8 |
| 3 7 5 | 6 1 8 | 2 4 9 |
+–––––––––+–––––––––+–––––––––+
| 6 4 9 | 8 7 5 | 3 1 2 |
| 7 2 1 | 9 3 6 | 8 5 4 |
| 5 3 8 | 2 4 1 | 6 9 7 |
+–––––––––+–––––––––+–––––––––+
| 4 8 6 | 5 9 7 | 1 2 3 |
| 1 9 7 | 3 6 2 | 4 8 5 |
| 2 5 3 | 1 8 4 | 9 7 6 |
+–––––––––+–––––––––+–––––––––+

The challenge noted that the flag was the values starting from the top left and going clockwise around the sudoku board. Thus, the final flag was:

1
AOTW{86472953189247356794813521457639}

A very simple and ideal use-case for the Z3 solver!