From 1e2c2dcc259b6caa7425405e6a8b8ae97c825eb2 Mon Sep 17 00:00:00 2001 From: En Yi Date: Thu, 12 Jul 2018 10:49:09 +0800 Subject: [PATCH] Finish hole digging --- gameplay/Sudoku_Generator.py | 169 +++++++++-------------------------- gameplay/Sudoku_Solver.py | 120 +++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 129 deletions(-) create mode 100644 gameplay/Sudoku_Solver.py diff --git a/gameplay/Sudoku_Generator.py b/gameplay/Sudoku_Generator.py index a2133a0..9452cbf 100644 --- a/gameplay/Sudoku_Generator.py +++ b/gameplay/Sudoku_Generator.py @@ -7,135 +7,32 @@ Credits for Generator: http://zhangroup.aporc.org/images/files/Paper_3485.pdf import random import re +#from . import Sudoku_Solver as solver +import Sudoku_Solver as solver + 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] +def check_for_nonzeros(seq): + return len([m.start() for m in filledcell.finditer(seq)]) -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): +def generate_completed_grid(n): # Generate a board by randomly picking n cells and # fill them a random digit from 1-9 - values = parse_grid('0' * 81) + values = solver.parse_grid('0' * 81) valid_assignments = 0 while valid_assignments < n: - #display(values) - cell_to_assign = squares[random.randint(0, 80)] + # display(values) + cell_to_assign = solver.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) + value_to_assign = valid_values[random.randint(0, len(valid_values) - 1)] + solver.assign(values, cell_to_assign, value_to_assign) valid_assignments += 1 - return values - -def generate_completed_grid(): - complete_values = solve(las_vegas(11)) + complete_values = solver.solve(values) grid = '' - for s in squares: + for s in solver.squares: grid += complete_values[s] return grid @@ -146,7 +43,7 @@ def generate_dig_sequence(difficulty): if difficulty <= 1: random_number = list(range(81)) while len(random_number) > 0: - print(len(random_number)) + #print(len(random_number)) yield random_number.pop(random.randint(0, len(random_number)-1)) elif difficulty == 2: current = 0 @@ -172,9 +69,7 @@ def generate_dig_sequence(difficulty): yield current current += 1 -def generate_sudoku_puzzle(difficulty): - grid = generate_completed_grid() - +def specify_grid_properties(difficulty): if difficulty == 0: n_givens = random.randint(50, 60) lower_bound = 5 @@ -191,25 +86,41 @@ def generate_sudoku_puzzle(difficulty): n_givens = random.randint(22, 27) lower_bound = 0 + return n_givens, lower_bound + + +def generate_sudoku_puzzle(difficulty): + grid = generate_completed_grid(11) + n_givens, lower_bound = specify_grid_properties() dig_sequence = generate_dig_sequence(difficulty) holes = 0 - while holes<81-n_givens: + 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 - + row = i % 9 + if check_for_nonzeros(grid[row:row+9]) > lower_bound: + current_number = grid[i] + other_numbers = solver.digits.replace(current_number, '') + unique = True + for digit in other_numbers: + grid_check = grid[:i] + digit + grid[i+1:] + if solver.solve(solver.parse_grid(grid_check)): + unique = False + break + if unique: + grid = grid[:i] + '0' + grid[i+1:] + holes += 1 # 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)] \ No newline at end of file + puzzle = generate_sudoku_puzzle(4) + print(check_for_nonzeros(puzzle)) + + solver.display_grid(puzzle) + solver.display(solver.solve(solver.parse_grid(puzzle))) diff --git a/gameplay/Sudoku_Solver.py b/gameplay/Sudoku_Solver.py new file mode 100644 index 0000000..88e6362 --- /dev/null +++ b/gameplay/Sudoku_Solver.py @@ -0,0 +1,120 @@ +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 display_grid(grid): + """Display these values as a 2-D grid.""" + line = '+'.join(['- '*3]*3) + for i in range(9): + row = '' + for j in range(9): + row = row + grid[i*9+j] + ' ' + if j == 2 or j == 5: + row = row + '|' + print(row) + if i == 2 or i == 5: + 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