-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsdk_board-1.py
313 lines (268 loc) · 10.1 KB
/
sdk_board-1.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
302
303
304
305
306
307
308
309
310
311
312
313
"""
A Sudoku board holds a matrix of tiles.
Each row and column and also sub-blocks
are treated as a group (sometimes called
a 'nonet'); 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 Sequence, List, Set
import logging
logging.basicConfig()
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
# --------------------------------
# The events for MVC
# --------------------------------
class Event(object):
"""Abstract base class of all events, both for MVC
and for other purposes.
"""
pass
# ---------------
# Listeners (base class)
# ---------------
class Listener(object):
"""Abstract base class for listeners.
Subclass this to make the notification do
something useful.
"""
def __init__(self):
"""Default constructor for simple listeners without state"""
pass
def notify(self, event: Event):
"""The 'notify' method of the base class must be
overridden in concrete classes.
"""
raise NotImplementedError("You must override Listener.notify")
# --------------------------------------
# Events and listeners for Tile objects
# --------------------------------------
class EventKind(enum.Enum):
TileChanged = 1
TileGuessed = 2
class TileEvent(Event):
"""Abstract base class for things that happen
to tiles. We always indicate the tile. Concrete
subclasses indicate the nature of the event.
"""
def __init__(self, tile: 'Tile', kind: EventKind):
self.tile = tile
self.kind = kind
# Note 'Tile' type is a forward reference;
# Tile class is defined below
def __str__(self):
"""Printed representation includes name of concrete subclass"""
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)
# ----------------------------------------------
# Tile class
# ----------------------------------------------
class Tile(Listenable):
"""One tile on the Sudoku grid.
Public attributes (read-only): value, which will be either
UNKNOWN or an element of CHOICES; candidates, which will
be a set drawn from CHOICES. If value is an element of
CHOICES,then candidates will be the singleton containing
value. If candidates is empty, then no tile value can
be consistent with other tile values in the grid.
value is a public read-only attribute; change it
only through the access method set_value or indirectly
through method remove_candidates.
"""
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.set_value(value)
def __str__(self) -> str:
return str(self.value)
def __repr__(self) -> str:
return f"Tile({self.row}, {self.col}, '{self.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 iff value is a candidate value for this tile"""
return value in self.candidates
def remove_candidates(self, used_values: Set[str]):
"""The used values cannot be a value of this unknown tile.
We remove those possibilities from the list of candidates.
If there is exactly one candidate left, we set the
value of the tile.
Returns: True means we eliminated at least one candidate,
False means nothing changed (none of the 'used_values' was
in our candidates set).
"""
new_candidates = self.candidates.difference(used_values)
if new_candidates == self.candidates:
# Didn't remove any 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
# ------------------------------
# Board class
# ------------------------------
class Board(object):
"""A board has a matrix of tiles"""
def __init__(self):
"""The empty board"""
# Row/Column structure: Each row contains columns
self.tiles: 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 in range(NCOLS):
colgroup = []
for row in range(NROWS):
colgroup.append(self.tiles[row][col])
self.groups.append(colgroup)
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 __str__(self) -> str:
"""In Sadman Sudoku format"""
return "\n".join(self.as_list())
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 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:
for group in self.groups:
used = set()
for tile in group:
if tile.value != UNKNOWN:
if tile.value in used:
return False
else:
used.add(tile.value)
return True
def is_complete(self) -> bool:
"""None of the tiles are UNKNOWN.
Note: Does not check consistency; do that
separately with is_consistent.
"""
for row in self.tiles:
for tile in row:
if tile.value == UNKNOWN:
return False
return True
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 = set()
for tile in group:
if tile.value != UNKNOWN:
used.add(tile.value)
for tile in group:
if tile.remove_candidates(used):
progress = True
return progress
def hidden_single(self) -> bool:
progress = False
for group in self.groups:
leftovers = CHOICES
for tile in group:
if tile.value in CHOICES:
leftovers = leftovers.replace(tile.value, "")
for value in leftovers:
possibilities = []
for tile in group:
if value in tile.candidates:
possibilities.append(tile)
if len(possibilities) == 1:
possibilities[0].set_value(value)
return progress
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.
"""
min_candidates = 10
min_tile = None
for row in self.tiles:
for tile in row:
if tile.value == UNKNOWN:
if len(tile.candidates) < min_candidates:
min_candidates = len(tile.candidates)
min_tile = tile
return min_tile
def solve(self):
"""General solver; guess-and-check
combined with constraint propagation.
"""
self.propagate()
if self.is_complete():
return True
if not self.is_consistent():
return False
save = self.as_list()
guess_tile = self.min_choice_tile()
for candidate in guess_tile.candidates:
guess_tile.set_value(candidate)
if self.solve():
return True
else:
self.set_tiles(save)
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()
self.hidden_single()
return