forked from CIS-UO/Duck_Sudoku
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsdk_board.py
301 lines (248 loc) · 9.25 KB
/
sdk_board.py
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
"""
Project: Sudoku Solver
Author: William Qiu
For CIS 211, Spring 2022, University of Oregon
"""
"""
A Sudoku board holds a matrix of tiles.
Each row, column, and sub-block is treated as a group.
When solved, each group must contain exactly one occurrence
of each of the symbol choices.
"""
from sdk_config import CHOICES, UNKNOWN, ROOT
from sdk_config import NROWS, NCOLS
import enum
from typing import List, Sequence, Set
import logging
logging.basicConfig()
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
class Event:
"""Abstract base class for events."""
pass
class Listener:
"""Abstract base class for listeners."""
def __init__(self):
pass
def notify(self, event: Event):
raise NotImplementedError("You must override Listener.notify")
class EventKind(enum.Enum):
TileChanged = 1
TileGuessed = 2
class TileEvent(Event):
"""Abstract base class for tile-related events."""
def __init__(self, tile: "Tile", kind: EventKind):
self.tile = tile
self.kind = kind
def __str__(self):
return f"{repr(self.tile)}"
class TileListener(Listener):
def notify(self, event: TileEvent):
raise NotImplementedError("TileListener subclass needs to override `notify(TileEvent)`")
class Listenable:
"""Objects to which listeners (like a view component) can be attached."""
def __init__(self):
self.listeners = []
def add_listener(self, listener: Listener):
self.listeners.append(listener)
def notify_all(self, event: Event):
for listener in self.listeners:
listener.notify(event)
class Tile(Listenable):
"""One tile on the Sudoku grid."""
def __init__(self, row: int, col: int, value=UNKNOWN):
super().__init__()
assert value == UNKNOWN or value in CHOICES
self.row = row
self.col = col
self.candidates = None
self.value = None
self.set_value(value)
def set_value(self, value: str):
if value in CHOICES:
self.value = value
self.candidates = {value}
else:
self.value = UNKNOWN
self.candidates = set(CHOICES)
self.notify_all(TileEvent(self, EventKind.TileChanged))
def could_be(self, value: str) -> bool:
"""True if value is a candidate value for this tile"""
return value in self.candidates
def remove_candidate(self, used_values: Set[str]) -> bool:
new_candidates = self.candidates.difference(used_values)
if new_candidates == self.candidates:
return False
self.candidates = new_candidates
if len(self.candidates) == 1:
self.set_value(new_candidates.pop())
self.notify_all(TileEvent(self, EventKind.TileChanged))
return True
def __str__(self) -> str:
return self.value
def __repr__(self) -> str:
return f"Tile({self.row}, {self.col}, {repr(self.value)})"
def __hash__(self) -> int:
"""Hash on position only (not value)"""
return hash((self.row, self.col))
class Board:
"""A Sudoku board has a matrix of tiles."""
def __init__(self):
self.tiles: List[List[Tile]] = []
for row in range(NROWS):
cols = []
for col in range(NCOLS):
cols.append(Tile(row, col))
self.tiles.append(cols)
self.groups: List[List[Tile]] = []
for row in self.tiles:
self.groups.append(row)
for col_n in range(NCOLS):
col = []
for row in self.tiles:
element = row[col_n]
col.append(element)
self.groups.append(col)
for block_row in range(ROOT):
for block_col in range(ROOT):
group = []
for row in range(ROOT):
for col in range(ROOT):
row_addr = (ROOT * block_row) + row
col_addr = (ROOT * block_col) + col
group.append(self.tiles[row_addr][col_addr])
self.groups.append(group)
def set_tiles(self, tile_values: Sequence[Sequence[str]]):
"""Set the tile values a list of lists or a list of strings"""
for row_num in range(NROWS):
for col_num in range(NCOLS):
tile = self.tiles[row_num][col_num]
tile.set_value(tile_values[row_num][col_num])
def is_consistent(self) -> bool:
"""Returns true if the board state is valid by Sudoku rules"""
for group in self.groups:
used_symbols = set()
for tile in group:
if tile.value != UNKNOWN:
if tile.value in used_symbols:
return False # The board is inconsistent if there is a duplicate here.
else:
used_symbols.add(tile.value)
return True
def is_complete(self) -> bool:
"""Returns true if each tile is filled; in other words none are UNKNOWN"""
for row in self.tiles:
for tile in row:
if tile.value == UNKNOWN:
return False
return True
def min_choice_tile(self) -> Tile:
"""Returns a tile with value UNKNOWN and
minimum number of candidates.
Precondition: There is at least one tile
with value UNKNOWN.
"""
current_min_choice = None
for row in self.tiles:
for tile in row:
if tile.value == UNKNOWN:
if current_min_choice is None or len(current_min_choice.candidates) > len(tile.candidates):
current_min_choice = tile
return current_min_choice
def naked_single(self) -> bool:
"""Eliminate candidates and check for sole remaining possibilities.
Return value True means we crossed off at least one candidate.
Return value False means we made no progress.
"""
progress = False
for group in self.groups:
used_symbols = set()
for tile in group:
if tile.value != UNKNOWN:
used_symbols.add(tile.value)
for tile in group:
if tile.value == UNKNOWN:
if tile.remove_candidate(used_symbols):
progress = True
return progress
def hidden_single(self) -> bool:
"""Return true if we filled a tile, otherwise the tile is not filled"""
progress = False
for group in self.groups:
leftovers = set(CHOICES)
for tile in group:
if tile.value in leftovers:
leftovers.discard(tile.value)
for symbol in leftovers:
possible_tiles = set()
for tile in group:
if tile.value == UNKNOWN and symbol in tile.candidates:
possible_tiles.add(tile)
if len(possible_tiles) == 1:
tile = possible_tiles.pop()
tile.set_value(symbol)
progress = True
return progress
def solve(self) -> bool:
"""General solver; guess-and-check
combined with constraint propagation.
"""
self.propagate()
if self.is_complete():
if self.is_consistent():
return True
else:
return False
elif not self.is_consistent():
return False
else:
state_saved = self.as_list()
a_guess = self.min_choice_tile()
for i in a_guess.candidates:
a_guess.set_value(i)
if self.solve():
return True
else:
self.set_tiles(state_saved)
# Tried all the possibilities; none worked!
return False
def propagate(self):
"""Repeat solution tactics until we
don't make any progress, whether or not
the board is solved.
"""
progress = True
while progress:
progress = self.naked_single()
progress = progress or self.hidden_single()
return
def as_list(self) -> List[str]:
"""Tile values in a format compatible with set_tiles."""
row_syms = []
for row in self.tiles:
values = [tile.value for tile in row]
row_syms.append("".join(values))
return row_syms
def __str__(self) -> str:
"""In Sadman Sudoku format"""
row_syms = self.as_list()
return "\\n".join(row_syms)
if __name__ == "__main__":
board = Board()
# Add a test case to see if the code works
board.set_tiles([
"53..7....",
"6..195...",
".98....6.",
"8...6...3",
"4..8.3..1",
"7...2...6",
".6....28.",
"...419..5",
"....8..79",
])
if board.solve():
print("Sudoku solved successfully!")
print(board)
else:
print("Couldn't solve the Sudoku.")