multi-puzzle-solver 0.9.26__py3-none-any.whl → 0.9.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multi-puzzle-solver might be problematic. Click here for more details.

@@ -0,0 +1,523 @@
1
+ import numpy as np
2
+ from typing import Union, Callable, Optional, List, Sequence, Literal
3
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_next_pos, in_bounds, set_char, get_char, Direction
4
+
5
+
6
+ def render_grid(cell_flags: np.ndarray,
7
+ center_char: Union[np.ndarray, str, None] = None,
8
+ show_axes: bool = True,
9
+ scale_x: int = 2) -> str:
10
+ """
11
+ most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
12
+ cell_flags: np.ndarray of shape (N, N) with characters 'U', 'D', 'L', 'R' to represent the edges of the cells.
13
+ center_char: np.ndarray of shape (N, N) with the center of the cells, or a string to use for all cells, or None to not show centers.
14
+ scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
15
+ """
16
+ assert cell_flags is not None and cell_flags.ndim == 2
17
+ R, C = cell_flags.shape
18
+
19
+ # Edge presence arrays (note the rectangular shapes)
20
+ H = np.zeros((R+1, C), dtype=bool) # horizontal edges between rows
21
+ V = np.zeros((R, C+1), dtype=bool) # vertical edges between cols
22
+ for r in range(R):
23
+ for c in range(C):
24
+ s = cell_flags[r, c]
25
+ if 'U' in s: H[r, c] = True
26
+ if 'D' in s: H[r+1, c] = True
27
+ if 'L' in s: V[r, c] = True
28
+ if 'R' in s: V[r, c+1] = True
29
+
30
+ # Bitmask for corner connections
31
+ U, Rb, D, Lb = 1, 2, 4, 8
32
+ JUNCTION = {
33
+ 0: ' ',
34
+ U: '│', D: '│', U|D: '│',
35
+ Lb: '─', Rb: '─', Lb|Rb: '─',
36
+ U|Rb: '└', Rb|D: '┌', D|Lb: '┐', Lb|U: '┘',
37
+ U|D|Lb: '┤', U|D|Rb: '├', Lb|Rb|U: '┴', Lb|Rb|D: '┬',
38
+ U|Rb|D|Lb: '┼',
39
+ }
40
+
41
+ assert scale_x >= 1
42
+ assert H.shape == (R+1, C) and V.shape == (R, C+1)
43
+
44
+ rows = 2*R + 1
45
+ cols = 2*C*scale_x + 1
46
+ canvas = [[' ']*cols for _ in range(rows)]
47
+
48
+ def x_corner(c): # x of corner column c (0..C)
49
+ return (2*c) * scale_x
50
+ def x_between(c,k): # kth in-between col (1..2*scale_x-1) between corners c and c+1
51
+ return (2*c) * scale_x + k
52
+
53
+ # horizontal edges: fill the stretched band between corners with '─'
54
+ for r in range(R+1):
55
+ rr = 2*r
56
+ for c in range(C):
57
+ if H[r, c]:
58
+ for k in range(1, scale_x*2): # 1..(2*scale_x-1)
59
+ canvas[rr][x_between(c, k)] = '─'
60
+
61
+ # vertical edges: at the corner columns
62
+ for r in range(R):
63
+ rr = 2*r + 1
64
+ for c in range(C+1):
65
+ if V[r, c]:
66
+ canvas[rr][x_corner(c)] = '│'
67
+
68
+ # junctions at every corner grid point
69
+ for r in range(R+1):
70
+ rr = 2*r
71
+ for c in range(C+1):
72
+ m = 0
73
+ if r > 0 and V[r-1, c]: m |= U
74
+ if c < C and H[r, c]: m |= Rb
75
+ if r < R and V[r, c]: m |= D
76
+ if c > 0 and H[r, c-1]: m |= Lb
77
+ canvas[rr][x_corner(c)] = JUNCTION[m]
78
+
79
+ # centers (safe for multi-character strings)
80
+ def put_center_text(rr: int, c: int, text: str):
81
+ left = x_corner(c) + 1
82
+ right = x_corner(c+1) - 1
83
+ if right < left:
84
+ return
85
+ span_width = right - left + 1
86
+ s = str(text)
87
+ if len(s) > span_width:
88
+ s = s[:span_width] # truncate to protect borders
89
+ start = left + (span_width - len(s)) // 2
90
+ for i, ch in enumerate(s):
91
+ canvas[rr][start + i] = ch
92
+
93
+ if center_char is not None:
94
+ for r in range(R):
95
+ rr = 2*r + 1
96
+ for c in range(C):
97
+ val = center_char if isinstance(center_char, str) else center_char[r, c]
98
+ put_center_text(rr, c, '' if val is None else str(val))
99
+
100
+ # rows -> strings
101
+ art_rows = [''.join(row) for row in canvas]
102
+ if not show_axes:
103
+ return '\n'.join(art_rows)
104
+
105
+ # Axes labels: row indices on the left, column indices on top (handle C, not R)
106
+ gut = max(2, len(str(R-1))) # gutter width based on row index width
107
+ gutter = ' ' * gut
108
+ top_tens = list(gutter + ' ' * cols)
109
+ top_ones = list(gutter + ' ' * cols)
110
+
111
+ for c in range(C):
112
+ xc_center = x_corner(c) + scale_x
113
+ if C >= 10:
114
+ top_tens[gut + xc_center] = str((c // 10) % 10)
115
+ top_ones[gut + xc_center] = str(c % 10)
116
+
117
+ if gut >= 2:
118
+ top_tens[gut-2:gut] = list(' ')
119
+ top_ones[gut-2:gut] = list(' ')
120
+
121
+ labeled = []
122
+ for r, line in enumerate(art_rows):
123
+ if r % 2 == 1: # cell-center row
124
+ label = str(r//2).rjust(gut)
125
+ else:
126
+ label = ' ' * gut
127
+ labeled.append(label + line)
128
+
129
+ return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
130
+
131
+ def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.array:
132
+ """In many instances, we have a 2d array where cell values are arbitrary ids
133
+ and we want to convert it to a 2d array where cell values are walls "U", "D", "L", "R" to represent the edges that separate me from my neighbors that have different ids.
134
+ Args:
135
+ id_board: np.array of shape (N, N) with arbitrary ids.
136
+ border_is_wall: if True, the edges of the board are considered to be walls.
137
+ Returns:
138
+ np.array of shape (N, N) with walls "U", "D", "L", "R".
139
+ """
140
+ res = np.full((id_board.shape[0], id_board.shape[1]), '', dtype=object)
141
+ V, H = id_board.shape
142
+ def append_char(pos: Pos, s: str):
143
+ set_char(res, pos, get_char(res, pos) + s)
144
+ def handle_pos_direction(pos: Pos, direction: Direction, s: str):
145
+ pos2 = get_next_pos(pos, direction)
146
+ if in_bounds(pos2, V, H):
147
+ if get_char(id_board, pos2) != get_char(id_board, pos):
148
+ append_char(pos, s)
149
+ else:
150
+ if border_is_wall:
151
+ append_char(pos, s)
152
+ for pos in get_all_pos(V, H):
153
+ handle_pos_direction(pos, Direction.LEFT, 'L')
154
+ handle_pos_direction(pos, Direction.RIGHT, 'R')
155
+ handle_pos_direction(pos, Direction.UP, 'U')
156
+ handle_pos_direction(pos, Direction.DOWN, 'D')
157
+ return res
158
+
159
+ def render_shaded_grid(V: int,
160
+ H: int,
161
+ is_shaded: Callable[[int, int], bool],
162
+ *,
163
+ scale_x: int = 2,
164
+ scale_y: int = 1,
165
+ fill_char: str = '▒',
166
+ empty_char: str = ' ',
167
+ empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,
168
+ show_axes: bool = True) -> str:
169
+ """
170
+ Most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
171
+ Visualize a V x H grid where each cell is shaded if is_shaded(r, c) is True.
172
+ The grid lines are always present.
173
+
174
+ scale_x: horizontal stretch (>=1). Interior width per cell = 2*scale_x - 1.
175
+ scale_y: vertical stretch (>=1). Interior height per cell = scale_y.
176
+ fill_char: character to fill shaded cell interiors (single char).
177
+ empty_char: background character for unshaded interiors (single char).
178
+ empty_text: Optional text for unshaded cells. If a string, used for all unshaded
179
+ cells. If a callable (r, c) -> str|None, used per cell. Text is
180
+ centered within the interior row and truncated to fit.
181
+ """
182
+ assert V >= 1 and H >= 1
183
+ assert scale_x >= 1 and scale_y >= 1
184
+ assert len(fill_char) == 1 and len(empty_char) == 1
185
+
186
+ # ── Layout helpers ─────────────────────────────────────────────────────
187
+ def x_corner(c: int) -> int: # column of vertical border at grid column c (0..H)
188
+ return (2 * c) * scale_x
189
+ def y_border(r: int) -> int: # row of horizontal border at grid row r (0..V)
190
+ return (scale_y + 1) * r
191
+
192
+ rows = y_border(V) + 1
193
+ cols = x_corner(H) + 1
194
+ canvas = [[empty_char] * cols for _ in range(rows)]
195
+
196
+ # ── Shading first (borders will overwrite as needed) ───────────────────
197
+ shaded_map = [[False]*H for _ in range(V)]
198
+ for r in range(V):
199
+ top = y_border(r) + 1
200
+ bottom = y_border(r + 1) - 1 # inclusive
201
+ if top > bottom:
202
+ continue
203
+ for c in range(H):
204
+ left = x_corner(c) + 1
205
+ right = x_corner(c + 1) - 1 # inclusive
206
+ if left > right:
207
+ continue
208
+ shaded = bool(is_shaded(r, c))
209
+ shaded_map[r][c] = shaded
210
+ ch = fill_char if shaded else empty_char
211
+ for yy in range(top, bottom + 1):
212
+ for xx in range(left, right + 1):
213
+ canvas[yy][xx] = ch
214
+
215
+ # ── Grid lines ─────────────────────────────────────────────────────────
216
+ U, Rb, D, Lb = 1, 2, 4, 8
217
+ JUNCTION = {
218
+ 0: ' ',
219
+ U: '│', D: '│', U | D: '│',
220
+ Lb: '─', Rb: '─', Lb | Rb: '─',
221
+ U | Rb: '└', Rb | D: '┌', D | Lb: '┐', Lb | U: '┘',
222
+ U | D | Lb: '┤', U | D | Rb: '├', Lb | Rb | U: '┴', Lb | Rb | D: '┬',
223
+ U | Rb | D | Lb: '┼',
224
+ }
225
+
226
+ # Horizontal borders (every y_border row)
227
+ for r in range(V + 1):
228
+ yy = y_border(r)
229
+ for c in range(H):
230
+ base = x_corner(c)
231
+ for k in range(1, 2 * scale_x): # 1..(2*scale_x-1)
232
+ canvas[yy][base + k] = '─'
233
+
234
+ # Vertical borders (every x_corner col)
235
+ for c in range(H + 1):
236
+ xx = x_corner(c)
237
+ for r in range(V):
238
+ for ky in range(1, scale_y + 1):
239
+ canvas[y_border(r) + ky][xx] = '│'
240
+
241
+ # Junctions at intersections
242
+ for r in range(V + 1):
243
+ yy = y_border(r)
244
+ for c in range(H + 1):
245
+ xx = x_corner(c)
246
+ m = 0
247
+ if r > 0: m |= U
248
+ if r < V: m |= D
249
+ if c > 0: m |= Lb
250
+ if c < H: m |= Rb
251
+ canvas[yy][xx] = JUNCTION[m]
252
+
253
+ # ── Optional per-cell text for UNshaded cells ──────────────────────────
254
+ def put_center_text(r_cell: int, c_cell: int, s: str):
255
+ # interior box
256
+ left = x_corner(c_cell) + 1
257
+ right = x_corner(c_cell + 1) - 1
258
+ top = y_border(r_cell) + 1
259
+ bottom= y_border(r_cell + 1) - 1
260
+ if left > right or top > bottom:
261
+ return
262
+ span_w = right - left + 1
263
+ # choose middle interior row for text
264
+ yy = top + (bottom - top) // 2
265
+ s = '' if s is None else str(s)
266
+ if len(s) > span_w:
267
+ s = s[:span_w]
268
+ start = left + (span_w - len(s)) // 2
269
+ for i, ch in enumerate(s):
270
+ canvas[yy][start + i] = ch
271
+
272
+ if empty_text is not None:
273
+ for r in range(V):
274
+ for c in range(H):
275
+ if not shaded_map[r][c]:
276
+ s = empty_text(r, c) if callable(empty_text) else empty_text
277
+ if s:
278
+ put_center_text(r, c, s)
279
+
280
+ # ── Stringify ──────────────────────────────────────────────────────────
281
+ art_rows = [''.join(row) for row in canvas]
282
+ if not show_axes:
283
+ return '\n'.join(art_rows)
284
+
285
+ # Axes labels: columns on top; rows on left
286
+ gut = max(2, len(str(V - 1)))
287
+ gutter = ' ' * gut
288
+ top_tens = list(gutter + ' ' * cols)
289
+ top_ones = list(gutter + ' ' * cols)
290
+ for c in range(H):
291
+ xc_center = x_corner(c) + scale_x
292
+ if H >= 10:
293
+ top_tens[gut + xc_center] = str((c // 10) % 10)
294
+ top_ones[gut + xc_center] = str(c % 10)
295
+ if gut >= 2:
296
+ top_tens[gut - 2:gut] = list(' ')
297
+ top_ones[gut - 2:gut] = list(' ')
298
+
299
+ labeled = []
300
+ for y, line in enumerate(art_rows):
301
+ mod = y % (scale_y + 1)
302
+ if 1 <= mod <= scale_y:
303
+ r = y // (scale_y + 1)
304
+ mid = (scale_y + 1) // 2
305
+ label = (str(r).rjust(gut) if mod == mid else ' ' * gut)
306
+ else:
307
+ label = ' ' * gut
308
+ labeled.append(label + line)
309
+
310
+ return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
311
+
312
+ CellVal = Literal["B", "W", "TL", "TR", "BL", "BR"]
313
+ GridLike = Sequence[Sequence[CellVal]]
314
+
315
+ def render_bw_tiles_split(
316
+ grid: GridLike,
317
+ cell_w: int = 6,
318
+ cell_h: int = 3,
319
+ borders: bool = False,
320
+ mode: Literal["ansi", "text"] = "ansi",
321
+ text_palette: Literal["solid", "hatch"] = "solid",
322
+ cell_text: Optional[Callable[[int, int], str]] = None) -> str:
323
+ """
324
+ Render a VxH grid with '/' or '\\' splits and optional per-cell centered text.
325
+
326
+ `cell_text(r, c) -> str`: if returns non-empty, its first character is drawn
327
+ near the geometric center of cell (r,c), nudged to the black side for halves.
328
+ """
329
+
330
+ V = len(grid)
331
+ if V == 0:
332
+ return ""
333
+ H = len(grid[0])
334
+ if any(len(row) != H for row in grid):
335
+ raise ValueError("All rows must have the same length")
336
+ if cell_w < 1 or cell_h < 1:
337
+ raise ValueError("cell_w and cell_h must be >= 1")
338
+
339
+ allowed = {"B","W","TL","TR","BL","BR"}
340
+ for r in range(V):
341
+ for c in range(H):
342
+ if grid[r][c] not in allowed:
343
+ raise ValueError(f"Invalid cell value at ({r},{c}): {grid[r][c]}")
344
+
345
+ # ── Mode setup ─────────────────────────────────────────────────────────
346
+ use_color = (mode == "ansi")
347
+
348
+ def sgr(bg: int | None = None, fg: int | None = None) -> str:
349
+ if not use_color:
350
+ return ""
351
+ parts = []
352
+ if fg is not None: parts.append(str(fg))
353
+ if bg is not None: parts.append(str(bg))
354
+ return ("\x1b[" + ";".join(parts) + "m") if parts else ""
355
+
356
+ RESET = "\x1b[0m" if use_color else ""
357
+
358
+ BG_BLACK, BG_WHITE = 40, 47
359
+ FG_BLACK, FG_WHITE = 30, 37
360
+
361
+ if text_palette == "solid":
362
+ TXT_BLACK, TXT_WHITE = " ", "█"
363
+ elif text_palette == "hatch":
364
+ TXT_BLACK, TXT_WHITE = "░", "▓"
365
+ else:
366
+ raise ValueError("text_palette must be 'solid' or 'hatch'")
367
+
368
+ def diag_kind_and_slash(val: CellVal):
369
+ if val in ("TR", "BL"):
370
+ return "main", "\\"
371
+ elif val in ("TL", "BR"):
372
+ return "anti", "/"
373
+ return None, "?"
374
+
375
+ def is_black(val: CellVal, fx: float, fy: float) -> bool:
376
+ if val == "B":
377
+ return True
378
+ if val == "W":
379
+ return False
380
+ kind, _ = diag_kind_and_slash(val)
381
+ if kind == "main": # y = x
382
+ return (fy < fx) if val == "TR" else (fy > fx)
383
+ else: # y = 1 - x
384
+ return (fy < 1 - fx) if val == "TL" else (fy > 1 - fx)
385
+
386
+ def on_boundary(val: CellVal, fx: float, fy: float) -> bool:
387
+ if val in ("B","W"):
388
+ return False
389
+ kind, _ = diag_kind_and_slash(val)
390
+ eps = 0.5 / max(cell_w, cell_h) # thin boundary
391
+ if kind == "main":
392
+ return abs(fy - fx) <= eps
393
+ else:
394
+ return abs(fy - (1 - fx)) <= eps
395
+
396
+ # Build one tile as a matrix of 1-char tokens (already colorized if ANSI)
397
+ def make_tile(val: CellVal) -> List[List[str]]:
398
+ rows: List[List[str]] = []
399
+ kind, slash_ch = diag_kind_and_slash(val)
400
+
401
+ for y in range(cell_h):
402
+ fy = (y + 0.5) / cell_h
403
+ line: List[str] = []
404
+ for x in range(cell_w):
405
+ fx = (x + 0.5) / cell_w
406
+
407
+ if val == "B":
408
+ line.append(sgr(bg=BG_BLACK) + " " + RESET if use_color else TXT_BLACK)
409
+ continue
410
+ if val == "W":
411
+ line.append(sgr(bg=BG_WHITE) + " " + RESET if use_color else TXT_WHITE)
412
+ continue
413
+
414
+ black_side = is_black(val, fx, fy)
415
+ boundary = on_boundary(val, fx, fy)
416
+
417
+ if use_color:
418
+ bg = BG_BLACK if black_side else BG_WHITE
419
+ if boundary:
420
+ fg = FG_WHITE if bg == BG_BLACK else FG_BLACK
421
+ line.append(sgr(bg=bg, fg=fg) + slash_ch + RESET)
422
+ else:
423
+ line.append(sgr(bg=bg) + " " + RESET)
424
+ else:
425
+ if boundary:
426
+ line.append(slash_ch)
427
+ else:
428
+ line.append(TXT_BLACK if black_side else TXT_WHITE)
429
+ rows.append(line)
430
+ return rows
431
+
432
+ # Overlay a single character centered (nudged into black side if needed)
433
+ def overlay_center_char(tile: List[List[str]], val: CellVal, ch: str):
434
+ if not ch:
435
+ return
436
+ ch = ch[0] # keep one character (user said single number)
437
+ cx, cy = cell_w // 2, cell_h // 2
438
+ fx = (cx + 0.5) / cell_w
439
+ fy = (cy + 0.5) / cell_h
440
+
441
+ # If center is boundary or not black, nudge horizontally toward black side
442
+ if val in ("TL","TR","BL","BR"):
443
+ kind, _ = diag_kind_and_slash(val)
444
+ # Determine which side is black relative to x at this y
445
+ if kind == "main": # boundary y=x → compare fx vs fy
446
+ want_right = (val == "TR") # black is to the right of boundary
447
+ if on_boundary(val, fx, fy) or (is_black(val, fx, fy) is False):
448
+ if want_right and cx + 1 < cell_w: cx += 1
449
+ elif not want_right and cx - 1 >= 0: cx -= 1
450
+ else: # boundary y=1-x → compare fx vs 1-fy
451
+ want_left = (val == "TL") # black is to the left of boundary
452
+ if on_boundary(val, fx, fy) or (is_black(val, fx, fy) is False):
453
+ if want_left and cx - 1 >= 0: cx -= 1
454
+ elif not want_left and cx + 1 < cell_w: cx += 1
455
+
456
+ # Compose the glyph for that spot
457
+ if use_color:
458
+ # Force black bg + white fg so it pops
459
+ token = sgr(bg=BG_BLACK, fg=FG_WHITE) + ch + RESET
460
+ else:
461
+ # In text mode, just put the raw character
462
+ token = ch
463
+ tile[cy][cx] = token
464
+
465
+ # Optional borders
466
+ if borders:
467
+ horiz = "─" * cell_w
468
+ top = "┌" + "┬".join(horiz for _ in range(H)) + "┐"
469
+ mid = "├" + "┼".join(horiz for _ in range(H)) + "┤"
470
+ bot = "└" + "┴".join(horiz for _ in range(H)) + "┘"
471
+
472
+ out_lines: List[str] = []
473
+ if borders:
474
+ out_lines.append(top)
475
+
476
+ for r in range(V):
477
+ # Build tiles for this row (so we can overlay per-cell text)
478
+ row_tiles: List[List[List[str]]] = []
479
+ for c in range(H):
480
+ t = make_tile(grid[r][c])
481
+ if cell_text is not None:
482
+ label = cell_text(r, c)
483
+ if label:
484
+ overlay_center_char(t, grid[r][c], label)
485
+ row_tiles.append(t)
486
+
487
+ # Emit tile rows
488
+ for y in range(cell_h):
489
+ if borders:
490
+ parts = ["│"]
491
+ for c in range(H):
492
+ parts.append("".join(row_tiles[c][y]))
493
+ parts.append("│")
494
+ out_lines.append("".join(parts))
495
+ else:
496
+ out_lines.append("".join("".join(row_tiles[c][y]) for c in range(H)))
497
+
498
+ if borders and r < V - 1:
499
+ out_lines.append(mid)
500
+
501
+ if borders:
502
+ out_lines.append(bot)
503
+
504
+ return "\n".join(out_lines) + (RESET if use_color else "")
505
+
506
+
507
+
508
+
509
+ # demo = [
510
+ # ["TL","TR","BL","BR","B","W","BL","BR","B","W","TL","TR","BL","BR","B","W","BL","BR","B","W","W","BL","BR","B","W"],
511
+ # ["W","BL","TR","BL","TL","BR","BL","BR","W","W","W","B","TR","BL","TL","BR","BL","BR","B","W","BR","BL","BR","B","W"],
512
+ # ["BR","BL","TR","TL","W","B","BL","BR","B","W","BR","BL","TR","TL","W","B","BL","BR","B","W","B","BL","BR","B","W"],
513
+ # ]
514
+ # print(render_bw_tiles_split(demo, cell_w=8, cell_h=4, borders=True, mode="ansi"))
515
+ # art = render_bw_tiles_split(
516
+ # demo,
517
+ # cell_w=8,
518
+ # cell_h=4,
519
+ # borders=True,
520
+ # mode="text", # ← key change
521
+ # text_palette="solid" # try "solid" for stark black/white
522
+ # )
523
+ # print("```text\n" + art + "\n```")
@@ -1,17 +1,32 @@
1
+ from typing import Optional
2
+
1
3
  import numpy as np
2
4
  from ortools.sat.python import cp_model
3
5
  from ortools.sat.python.cp_model import LinearExpr as lxp
4
6
 
5
- from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, in_bounds, set_char, get_char, get_neighbors8, get_row_pos, get_col_pos
7
+ from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char, get_row_pos, get_col_pos
6
8
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
9
+ from puzzle_solver.core.utils_visualizer import render_shaded_grid
7
10
 
8
11
 
9
12
  class Board:
10
- def __init__(self, board: np.array):
13
+ def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True):
11
14
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
12
15
  assert all(c.item() in [' ', 'B', 'W'] for c in np.nditer(board)), 'board must contain only space or B'
13
16
  self.board = board
14
17
  self.V, self.H = board.shape
18
+ if arith_rows is not None:
19
+ assert arith_rows.ndim == 2, f'arith_rows must be 2d, got {arith_rows.ndim}'
20
+ assert arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {arith_rows.shape} for {board.shape}'
21
+ assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
22
+ if arith_cols is not None:
23
+ assert arith_cols.ndim == 2, f'arith_cols must be 2d, got {arith_cols.ndim}'
24
+ assert arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one column and row less than board, got {arith_cols.shape} for {board.shape}'
25
+ assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
26
+ self.arith_rows = arith_rows
27
+ self.arith_cols = arith_cols
28
+ self.force_unique = force_unique
29
+
15
30
  self.model = cp_model.CpModel()
16
31
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
17
32
 
@@ -40,11 +55,32 @@ class Board:
40
55
  for pos in get_all_pos(self.V, self.H):
41
56
  self.disallow_three_in_a_row(pos, Direction.RIGHT)
42
57
  self.disallow_three_in_a_row(pos, Direction.DOWN)
58
+
43
59
  # 3. Each row and column is unique.
44
- # a list per row
45
- self.force_unique([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
46
- # a list per column
47
- self.force_unique([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
60
+ if self.force_unique:
61
+ # a list per row
62
+ self.force_unique_double_list([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
63
+ # a list per column
64
+ self.force_unique_double_list([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
65
+
66
+ # if arithmetic is provided, add constraints for it
67
+ if self.arith_rows is not None:
68
+ assert self.arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {self.arith_rows.shape} for {self.board.shape}'
69
+ for pos in get_all_pos(self.V, self.H-1):
70
+ c = get_char(self.arith_rows, pos)
71
+ if c == 'x':
72
+ self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.RIGHT)])
73
+ elif c == '=':
74
+ self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.RIGHT)])
75
+ if self.arith_cols is not None:
76
+ assert self.arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one row less than board, got {self.arith_cols.shape} for {self.board.shape}'
77
+ for pos in get_all_pos(self.V-1, self.H):
78
+ c = get_char(self.arith_cols, pos)
79
+ if c == 'x':
80
+ self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.DOWN)])
81
+ elif c == '=':
82
+ self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.DOWN)])
83
+
48
84
 
49
85
  def disallow_three_in_a_row(self, p1: Pos, direction: Direction):
50
86
  p2 = get_next_pos(p1, direction)
@@ -62,7 +98,7 @@ class Board:
62
98
  self.model_vars[p3].Not(),
63
99
  ])
64
100
 
65
- def force_unique(self, model_vars: list[list[cp_model.IntVar]]):
101
+ def force_unique_double_list(self, model_vars: list[list[cp_model.IntVar]]):
66
102
  if not model_vars or len(model_vars) < 2:
67
103
  return
68
104
  m = len(model_vars[0])
@@ -86,13 +122,5 @@ class Board:
86
122
  return SingleSolution(assignment=assignment)
87
123
  def callback(single_res: SingleSolution):
88
124
  print("Solution found")
89
- res = np.full((self.V, self.H), ' ', dtype=object)
90
- for pos in get_all_pos(self.V, self.H):
91
- c = get_char(self.board, pos)
92
- c = 'B' if single_res.assignment[pos] == 1 else 'W'
93
- set_char(res, pos, c)
94
- print('[')
95
- for row in res:
96
- print(" [ '" + "', '".join(row.tolist()) + "' ],")
97
- print(']')
125
+ print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
98
126
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,7 @@
1
+ import numpy as np
2
+
3
+ from . import binairo
4
+
5
+ class Board(binairo.Board):
6
+ def __init__(self, board: np.array, arith_rows: np.array, arith_cols: np.array):
7
+ super().__init__(board=board, arith_rows=arith_rows, arith_cols=arith_cols, force_unique=False)