Data update
This commit is contained in:
parent
4bb20c9b71
commit
cbaf4c4b64
12390 changed files with 318560 additions and 27248 deletions
285
Task/Dominoes/Python/dominoes.py
Normal file
285
Task/Dominoes/Python/dominoes.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
from typing import Final, Generator, Iterator, Optional
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Use a cryptographically secure random number generator
|
||||
rnd = random.SystemRandom()
|
||||
|
||||
# Sentinel value to represent an empty/unfilled cell in the grid during solving
|
||||
EMPTY: Final = -1
|
||||
|
||||
# Predefined 8x8 grid of integers (0–6), representing a domino puzzle board
|
||||
tableau = [
|
||||
[0, 5, 1, 3, 2, 2, 3, 1],
|
||||
[0, 5, 5, 0, 5, 2, 4, 6],
|
||||
[4, 3, 0, 3, 6, 6, 2, 0],
|
||||
[0, 6, 2, 3, 5, 1, 2, 6],
|
||||
[1, 1, 3, 0, 0, 2, 4, 5],
|
||||
[2, 1, 4, 3, 3, 4, 6, 6],
|
||||
[6, 4, 5, 1, 5, 4, 1, 4]
|
||||
]
|
||||
# Randomly generated 8x8 grid using values 0–6 for testing alternative puzzles
|
||||
customTableau = [[rnd.randint(0, 6) for _ in range(8)] for _ in range(8)]
|
||||
|
||||
# # Represents a domino tile with two ends (a and b).
|
||||
# It is treated as unordered (i.e., (1,2) == (2,1)) via custom hash and equality.
|
||||
@dataclass(frozen=True)
|
||||
class Domino:
|
||||
a: int
|
||||
b: int
|
||||
|
||||
# Hash based on sorted tuple so (a,b) and (b,a) are considered the same domino
|
||||
def __hash__(self) -> int:
|
||||
return hash(tuple(sorted((self.a, self.b))))
|
||||
|
||||
# String representation for debugging/printing
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.a},{self.b})"
|
||||
|
||||
# Represents a coordinate (x, y) on the grid
|
||||
@dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.x},{self.y})"
|
||||
|
||||
# Bundles a solved layout: the filled grid, list of placed dominoes, and their positions
|
||||
@dataclass
|
||||
class Pattern:
|
||||
tableau: list[list[int]] # Final filled grid (copied from solver state)
|
||||
dominoes: list[Domino] # List of dominoes used (in placement order)
|
||||
points: list[Point] # Paired list of points indicating domino placements
|
||||
|
||||
def findPatterns(
|
||||
source: list[list[int]],
|
||||
*,
|
||||
maxSolutions: Optional[int] = None,
|
||||
asGenerator: bool = False
|
||||
) -> Iterator[Pattern] | list[Pattern]:
|
||||
"""
|
||||
Solves a domino tiling puzzle where each domino must be unique (unordered pair),
|
||||
and every cell must be covered exactly once.
|
||||
|
||||
Args:
|
||||
source: 2D grid of integers (0-6) representing the puzzle.
|
||||
maxSolutions: Optional limit on number of solutions to find.
|
||||
asGenerator: If True, returns a generator yielding solutions one by one;
|
||||
otherwise returns a list of all found solutions (up to maxSolutions).
|
||||
|
||||
Returns:
|
||||
Either a list of Pattern objects or a generator yielding them.
|
||||
"""
|
||||
nRows = len(source)
|
||||
assert nRows > 0
|
||||
nCols = len(source[0])
|
||||
# Ensure all rows have the same length
|
||||
for row in source:
|
||||
if len(row) != nCols:
|
||||
raise ValueError("All rows must have same length")
|
||||
|
||||
# Total number of dominoes needed to cover the board
|
||||
dominoGoal = (nRows * nCols) // 2
|
||||
# If total cells is odd, tiling is impossible
|
||||
if (nRows * nCols) % 2 != 0:
|
||||
return []
|
||||
|
||||
# Working grid: EMPTY (-1) means not yet covered
|
||||
grid = [[EMPTY for _ in range(nCols)] for _ in range(nRows)]
|
||||
usedDominoes: set[Domino] = set() # For O(1) uniqueness checks
|
||||
usedDominoesList: list[Domino] = [] # To preserve order of placement
|
||||
points: list[Point] = [] # Stores paired coordinates of domino placements
|
||||
solutions: list[Pattern] = [] # Accumulates found solutions
|
||||
|
||||
def findFirstEmpty(startIndex: int = 0) -> Optional[int]:
|
||||
"""Find the first empty cell in row-major order starting from `startIndex`."""
|
||||
total = nRows * nCols
|
||||
for i in range(startIndex, total):
|
||||
r = i // nCols
|
||||
c = i % nCols
|
||||
if grid[r][c] == EMPTY:
|
||||
return i
|
||||
return # No empty cell found
|
||||
|
||||
def collectSolution() -> Pattern:
|
||||
"""Create a deep copy of the current solution state."""
|
||||
tableauCopy = [row[:] for row in grid]
|
||||
return Pattern(tableauCopy, usedDominoesList.copy(), points.copy())
|
||||
|
||||
def dfs(startIndex: int = 0):
|
||||
"""
|
||||
Depth-first search to place dominoes recursively.
|
||||
Stops early if `maxSolutions` is reached.
|
||||
"""
|
||||
if maxSolutions is not None and len(solutions) >= maxSolutions:
|
||||
return
|
||||
|
||||
index = findFirstEmpty(startIndex)
|
||||
if index is None:
|
||||
# All cells filled — check if we used exactly the right number of dominoes
|
||||
if len(usedDominoesList) == dominoGoal:
|
||||
solutions.append(collectSolution())
|
||||
return
|
||||
|
||||
r = index // nCols
|
||||
c = index % nCols
|
||||
|
||||
# Try placing a vertical domino (downwards)
|
||||
if r + 1 < nRows and grid[r+1][c] == EMPTY:
|
||||
d = Domino(source[r][c], source[r+1][c])
|
||||
if d not in usedDominoes:
|
||||
# Place domino
|
||||
grid[r][c] = source[r][c]
|
||||
grid[r+1][c] = source[r+1][c]
|
||||
usedDominoes.add(d)
|
||||
usedDominoesList.append(d)
|
||||
points.extend([Point(r, c), Point(r+1, c)])
|
||||
|
||||
dfs(index+1)
|
||||
|
||||
# Backtrack
|
||||
points.pop()
|
||||
points.pop()
|
||||
usedDominoesList.pop()
|
||||
usedDominoes.remove(d)
|
||||
grid[r][c] = EMPTY
|
||||
grid[r+1][c] = EMPTY
|
||||
|
||||
# Early exit if enough solutions found
|
||||
if maxSolutions is not None and len(solutions) >= maxSolutions:
|
||||
return
|
||||
|
||||
# Try placing a horizontal domino (rightwards)
|
||||
if c + 1 < nCols and grid[r][c+1] == EMPTY:
|
||||
d = Domino(source[r][c], source[r][c+1])
|
||||
if d not in usedDominoes:
|
||||
# Place domino
|
||||
grid[r][c] = source[r][c]
|
||||
grid[r][c+1] = source[r][c+1]
|
||||
usedDominoes.add(d)
|
||||
usedDominoesList.append(d)
|
||||
points.extend([Point(r, c), Point(r, c+1)])
|
||||
|
||||
dfs(index+1)
|
||||
|
||||
# Backtrack
|
||||
points.pop()
|
||||
points.pop()
|
||||
usedDominoesList.pop()
|
||||
usedDominoes.remove(d)
|
||||
grid[r][c] = EMPTY
|
||||
grid[r][c+1] = EMPTY
|
||||
|
||||
# Early exit if enough solutions found
|
||||
if maxSolutions is not None and len(solutions) >= maxSolutions:
|
||||
return
|
||||
|
||||
# If requested, return a generator instead of collecting all solutions upfront
|
||||
if asGenerator:
|
||||
def gen() -> Generator:
|
||||
def dfsGen(startIndex: int = 0) -> Generator:
|
||||
"""Generator version of DFS that yields solutions as they are found."""
|
||||
index = findFirstEmpty(startIndex)
|
||||
if index is None:
|
||||
if len(usedDominoesList) == dominoGoal:
|
||||
yield collectSolution()
|
||||
return
|
||||
r = index // nCols
|
||||
c = index % nCols
|
||||
|
||||
# Vertical placement
|
||||
if r + 1 < nRows and grid[r+1][c] == EMPTY:
|
||||
d = Domino(source[r][c], source[r+1][c])
|
||||
if d not in usedDominoes:
|
||||
grid[r][c] = source[r][c]
|
||||
grid[r+1][c] = source[r+1][c]
|
||||
usedDominoes.add(d)
|
||||
usedDominoesList.append(d)
|
||||
points.extend([Point(r, c), Point(r+1, c)])
|
||||
|
||||
yield from dfsGen(index+1)
|
||||
|
||||
# Backtrack
|
||||
points.pop()
|
||||
points.pop()
|
||||
usedDominoesList.pop()
|
||||
usedDominoes.remove(d)
|
||||
grid[r][c] = EMPTY
|
||||
grid[r+1][c] = EMPTY
|
||||
|
||||
# Horizontal placement
|
||||
if c + 1 < nCols and grid[r][c+1] == EMPTY:
|
||||
d = Domino(source[r][c], source[r][c+1])
|
||||
if d not in usedDominoes:
|
||||
grid[r][c] = source[r][c]
|
||||
grid[r][c+1] = source[r][c+1]
|
||||
usedDominoes.add(d)
|
||||
usedDominoesList.append(d)
|
||||
points.extend([Point(r, c), Point(r, c+1)])
|
||||
|
||||
yield from dfsGen(index+1)
|
||||
|
||||
# Backtrack
|
||||
points.pop()
|
||||
points.pop()
|
||||
usedDominoesList.pop()
|
||||
usedDominoes.remove(d)
|
||||
grid[r][c] = EMPTY
|
||||
grid[r][c+1] = EMPTY
|
||||
|
||||
yield from dfsGen(0)
|
||||
|
||||
return gen()
|
||||
|
||||
# Run standard DFS and return collected solutions
|
||||
dfs(0)
|
||||
return solutions
|
||||
|
||||
def printLayout(pattern: Pattern):
|
||||
"""
|
||||
Pretty-prints a solved domino layout with ASCII art:
|
||||
- Numbers represent tile values.
|
||||
- '-' connects horizontally adjacent domino halves.
|
||||
- '|' connects vertically adjacent domino halves.
|
||||
"""
|
||||
nRows = len(pattern.tableau)
|
||||
nCols = len(pattern.tableau[0]) if nRows > 0 else 0
|
||||
|
||||
# Create a character grid large enough to show connections
|
||||
output = [[" " for _ in range(nCols*3-1)] for _ in range(nRows*2-1)]
|
||||
|
||||
# Place numbers in the output grid
|
||||
for i in range(nRows):
|
||||
for j in range(nCols):
|
||||
val = pattern.tableau[i][j]
|
||||
ch = "?" if val == EMPTY else str(val)
|
||||
output[i*2][j*3] = ch
|
||||
|
||||
# Draw connections between paired points (domino halves)
|
||||
for k in range(0, len(pattern.points), 2):
|
||||
if k + 1 >= len(pattern.points):
|
||||
break
|
||||
|
||||
p0 = pattern.points[k]
|
||||
p1 = pattern.points[k+1]
|
||||
|
||||
# Horizontal domino: same row, adjacent columns
|
||||
if p0.x == p1.x and p0.y + 1 == p1.y:
|
||||
output[p0.x*2][p0.y*3+1] = "-"
|
||||
output[p0.x*2][p0.y*3+2] = "-"
|
||||
# Vertical domino: same column, adjacent rows
|
||||
elif p0.y == p1.y and p0.x+1 == p1.x:
|
||||
output[p0.x*2+1][p0.y*3] = "|"
|
||||
|
||||
# Print the final layout line by line
|
||||
for line in output:
|
||||
print("".join(line))
|
||||
|
||||
# Entry point: solve both the predefined and random tableaus, print first solution if found
|
||||
if __name__ == "__main__":
|
||||
for t in [tableau, customTableau]:
|
||||
sols = findPatterns(t, maxSolutions=1)
|
||||
print(f"Layouts found: {len(sols)}")
|
||||
if len(sols) > 0:
|
||||
printLayout(sols[0])
|
||||
Loading…
Add table
Add a link
Reference in a new issue