SudokuGame/gameplay/Sudoku_Generator.py

215 lines
6.6 KiB
Python

"""
Module that generates a valid Sudoku Puzzle
Credits for Solver : http://norvig.com/sudoku.html
Credits for Generator: http://zhangroup.aporc.org/images/files/Paper_3485.pdf
"""
import random
import re
filledcell = re.compile('(?!0)')
def cross(array1, array2):
"""Cross product of elements in A and elements in B."""
return [a+b for a in array1 for b in array2]
digits = '123456789'
rows = 'ABCDEFGHI'
cols = digits
squares = cross(rows, cols)
unitlist = ([cross(rows, c) for c in cols] +
[cross(r, cols) for r in rows] +
[cross(rs, cs) for rs in ('ABC', 'DEF', 'GHI') for cs in ('123','456','789')])
units = dict((s, [u for u in unitlist if s in u])
for s in squares)
peers = dict((s, set(sum(units[s], []))-set([s]))
for s in squares)
def parse_grid(grid):
"""Convert grid to a dict of possible values, {square: digits}, or
return False if a contradiction is detected."""
# To start, every square can be any digit; then assign values from the grid.
values = dict((s, digits) for s in squares)
for s, d in grid_values(grid).items():
if d in digits and not assign(values, s, d):
return False # (Fail if we can't assign d to square s.)
return values
def grid_values(grid):
"""Convert grid into a dict of {square: char} with '0' or '.' for empties."""
chars = [c for c in grid if c in digits or c in '0.']
assert len(chars) == 81
return dict(zip(squares, chars))
def display(values):
"""Display these values as a 2-D grid."""
width = 1+max(len(values[s]) for s in squares)
line = '+'.join(['-'*(width*3)]*3)
for r in rows:
print(''.join(values[r+c].center(width)+('|' if c in '36' else '')
for c in cols))
if r in 'CF':
print(line)
print('')
def assign(values, s, d):
"""Eliminate all the other values (except d) from values[s] and propagate.
Return values, except return False if a contradiction is detected."""
other_values = values[s].replace(str(d), '')
if all(eliminate(values, s, d2) for d2 in other_values):
return values
else:
return False
def eliminate(values, s, d):
"""Eliminate d from values[s]; propagate when values or places <= 2.
Return values, except return False if a contradiction is detected."""
if d not in values[s]:
return values # Already eliminated
values[s] = values[s].replace(d, '')
# (1) If a square s is reduced to one value d2, then eliminate d2 from the peers.
if len(values[s]) == 0:
return False # Contradiction: removed last value
elif len(values[s]) == 1:
d2 = values[s]
if not all(eliminate(values, s2, d2) for s2 in peers[s]):
return False
# (2) If a unit u is reduced to only one place for a value d, then put it there.
for u in units[s]:
dplaces = [s for s in u if d in values[s]]
if len(dplaces) == 0:
return False # Contradiction: no place for this value
elif len(dplaces) == 1:
# d can only be in one place in unit; assign it there
if not assign(values, dplaces[0], d):
return False
return values
#def solve(grid): return search(parse_grid(grid))
def solve(values): return search(values)
def search(values):
"""Using depth-first search and propagation, try all possible values."""
if values is False:
return False # Failed earlier
if all(len(values[s]) == 1 for s in squares):
return values # Solved!
# Chose the unfilled square s with the fewest possibilities
n, s = min((len(values[s]), s) for s in squares if len(values[s]) > 1)
return some(search(assign(values.copy(), s, d))
for d in values[s])
def some(seq):
"""Return some element of seq that is true."""
for e in seq:
if e:
return e
return False
def las_vegas(n):
# Generate a board by randomly picking n cells and
# fill them a random digit from 1-9
values = parse_grid('0' * 81)
valid_assignments = 0
while valid_assignments < n:
#display(values)
cell_to_assign = squares[random.randint(0, 80)]
valid_values = values[cell_to_assign]
if len(valid_values):
value_to_assign = valid_values[random.randint(0, len(valid_values)-1)]
assign(values, cell_to_assign, value_to_assign)
valid_assignments += 1
return values
def generate_completed_grid():
complete_values = solve(las_vegas(11))
grid = ''
for s in squares:
grid += complete_values[s]
return grid
def generate_dig_sequence(difficulty):
# TODO: Determine the number of givens and lower bound of given
if difficulty <= 1:
random_number = list(range(81))
while len(random_number) > 0:
print(len(random_number))
yield random_number.pop(random.randint(0, len(random_number)-1))
elif difficulty == 2:
current = 0
while current < 81:
row = int(current / 9)
if not row % 2:
yield current
else:
yield (row+1) * 9 - 1 - (current % 9)
current += 2
elif difficulty == 3:
current = 0
while current < 81:
row = int(current / 9)
if not row % 2:
yield current
else:
yield (row+1) * 9 - 1 - (current % 9)
current += 1
elif difficulty == 4:
current = 0
while current < 81:
yield current
current += 1
def generate_sudoku_puzzle(difficulty):
grid = generate_completed_grid()
if difficulty == 0:
n_givens = random.randint(50, 60)
lower_bound = 5
elif difficulty == 1:
n_givens = random.randint(36, 49)
lower_bound = 4
elif difficulty == 2:
n_givens = random.randint(32, 35)
lower_bound = 3
elif difficulty == 3:
n_givens = random.randint(28, 31)
lower_bound = 2
elif difficulty == 4:
n_givens = random.randint(22, 27)
lower_bound = 0
dig_sequence = generate_dig_sequence(difficulty)
holes = 0
while holes<81-n_givens:
try:
i = next(dig_sequence)
except StopIteration:
print("Reach end of Sequence")
break
# TODO: Check if givens at current row and column is at lower bound
# TODO: Dig the current hole and check for uniqueness
# TODO: Propagate and Output
return grid
if __name__ == "__main__":
#print(generate_completed_grid())
func = generate_dig_sequence(3)
#print(next(func))
[print(a) for a in next(func)]