252 lines
7 KiB
Python
252 lines
7 KiB
Python
|
#!/usr/bin/env python
|
||
|
#
|
||
|
# Sudoku Generator and Solver in 250 lines of python
|
||
|
# Copyright (c) 2006 David Bau. All rights reserved.
|
||
|
#
|
||
|
# Can be used as either a command-line tool or as a cgi script.
|
||
|
#
|
||
|
# As a cgi-script, generates puzzles and estimates their level of
|
||
|
# difficulty. Uses files sudoku-template.pdf/.ps/.txt/.html
|
||
|
# in which it can fill in 81 underscores with digits for a puzzle.
|
||
|
# The suffix of the request URL determines which template is used.
|
||
|
#
|
||
|
# On a command line without any arguments, prints text for a
|
||
|
# random sudoku puzzle, with an estimate of its difficulty.
|
||
|
# On a command line with a filename, solves the given puzzles
|
||
|
# (files should look like the text generated by the generator).
|
||
|
|
||
|
import sys, os, random, getopt, re
|
||
|
|
||
|
def main():
|
||
|
args = sys.argv[1:]
|
||
|
if len(args) > 0:
|
||
|
puzzles = [loadboard(filename) for filename in args]
|
||
|
else:
|
||
|
puzzles = [makepuzzle(solution([None] * 81))]
|
||
|
for puzzle in puzzles:
|
||
|
print("PUZZLE:")
|
||
|
print(printboard(puzzle))
|
||
|
print("RATING:", ratepuzzle(puzzle, 4))
|
||
|
if len(args) > 0:
|
||
|
print()
|
||
|
print("SOLUTION:")
|
||
|
answer = solution(puzzle)
|
||
|
if answer is None: print("NO SOLUTION")
|
||
|
else: print(printboard(answer))
|
||
|
|
||
|
def cgimain(ext='txt'):
|
||
|
query = os.getenv('QUERY_STRING', '')
|
||
|
pathinfo = os.getenv('REQUEST_URI', '').split('?', 1)[0] or ''
|
||
|
template = ((("_"*9)+'\n')*9)
|
||
|
texttype = {'pdf':'application/pdf', 'ps':'application/postscript',
|
||
|
'txt':'text/plain', 'html':'text/html'}[ext]
|
||
|
#print("Content-Type: %s" % texttype)
|
||
|
#print("Content-Length: %d" % len(template))
|
||
|
#print("Cache-Control: no-cache")
|
||
|
#print("Expires: Sat, 01 Jan 2000 00:00:00 GMT")
|
||
|
#print()
|
||
|
segments = template.split('_', 81)
|
||
|
sys.stdout.write(segments[0])
|
||
|
sys.stdout.flush()
|
||
|
|
||
|
answers = solution([None] * 81)
|
||
|
puzzle = makepuzzle(answers)
|
||
|
|
||
|
return [answers, puzzle]
|
||
|
|
||
|
#for pos in range(81):
|
||
|
# if puzzle[pos] is None: sys.stdout.write(' ')
|
||
|
# else: sys.stdout.write(str(puzzle[pos] + 1))
|
||
|
# sys.stdout.write(segments[pos + 1].replace('####', ratingstr))
|
||
|
|
||
|
def makepuzzle(board):
|
||
|
puzzle = []; deduced = [None] * 81
|
||
|
order = random.sample(range(81), 81)
|
||
|
for pos in order:
|
||
|
if deduced[pos] is None:
|
||
|
puzzle.append((pos, board[pos]))
|
||
|
deduced[pos] = board[pos]
|
||
|
deduce(deduced)
|
||
|
random.shuffle(puzzle)
|
||
|
for i in range(len(puzzle) - 1, -1, -1):
|
||
|
e = puzzle[i]; del puzzle[i]
|
||
|
rating = checkpuzzle(boardforentries(puzzle), board)
|
||
|
if rating == -1: puzzle.append(e)
|
||
|
return boardforentries(puzzle)
|
||
|
|
||
|
def ratepuzzle(puzzle, samples):
|
||
|
total = 0
|
||
|
for i in range(samples):
|
||
|
state, answer = solveboard(puzzle)
|
||
|
if answer is None: return -1
|
||
|
total += len(state)
|
||
|
return float(total) / samples
|
||
|
|
||
|
def checkpuzzle(puzzle, board = None):
|
||
|
state, answer = solveboard(puzzle)
|
||
|
if answer is None: return -1
|
||
|
if board is not None and not boardmatches(board, answer): return -1
|
||
|
difficulty = len(state)
|
||
|
state, second = solvenext(state)
|
||
|
if second is not None: return -1
|
||
|
return difficulty
|
||
|
|
||
|
def solution(board):
|
||
|
return solveboard(board)[1]
|
||
|
|
||
|
def solveboard(original):
|
||
|
board = list(original)
|
||
|
guesses = deduce(board)
|
||
|
if guesses is None: return ([], board)
|
||
|
track = [(guesses, 0, board)]
|
||
|
return solvenext(track)
|
||
|
|
||
|
def solvenext(remembered):
|
||
|
while len(remembered) > 0:
|
||
|
guesses, c, board = remembered.pop()
|
||
|
if c >= len(guesses): continue
|
||
|
remembered.append((guesses, c + 1, board))
|
||
|
workspace = list(board)
|
||
|
pos, n = guesses[c]
|
||
|
workspace[pos] = n
|
||
|
guesses = deduce(workspace)
|
||
|
if guesses is None: return (remembered, workspace)
|
||
|
remembered.append((guesses, 0, workspace))
|
||
|
return ([], None)
|
||
|
|
||
|
def deduce(board):
|
||
|
while True:
|
||
|
stuck, guess, count = True, None, 0
|
||
|
# fill in any spots determined by direct conflicts
|
||
|
allowed, needed = figurebits(board)
|
||
|
for pos in range(81):
|
||
|
if None == board[pos]:
|
||
|
numbers = listbits(allowed[pos])
|
||
|
if len(numbers) == 0: return []
|
||
|
elif len(numbers) == 1: board[pos] = numbers[0]; stuck = False
|
||
|
elif stuck:
|
||
|
guess, count = pickbetter(guess, count, [(pos, n) for n in numbers])
|
||
|
if not stuck: allowed, needed = figurebits(board)
|
||
|
# fill in any spots determined by elimination of other locations
|
||
|
for axis in range(3):
|
||
|
for x in range(9):
|
||
|
numbers = listbits(needed[axis * 9 + x])
|
||
|
for n in numbers:
|
||
|
bit = 1 << n
|
||
|
spots = []
|
||
|
for y in range(9):
|
||
|
pos = posfor(x, y, axis)
|
||
|
if allowed[pos] & bit: spots.append(pos)
|
||
|
if len(spots) == 0: return []
|
||
|
elif len(spots) == 1: board[spots[0]] = n; stuck = False
|
||
|
elif stuck:
|
||
|
guess, count = pickbetter(guess, count, [(pos, n) for pos in spots])
|
||
|
if stuck:
|
||
|
if guess is not None: random.shuffle(guess)
|
||
|
return guess
|
||
|
|
||
|
def figurebits(board):
|
||
|
allowed, needed = [e is None and 511 or 0 for e in board], []
|
||
|
for axis in range(3):
|
||
|
for x in range(9):
|
||
|
bits = axismissing(board, x, axis)
|
||
|
needed.append(bits)
|
||
|
for y in range(9):
|
||
|
allowed[posfor(x, y, axis)] &= bits
|
||
|
return allowed, needed
|
||
|
|
||
|
def posfor(x, y, axis = 0):
|
||
|
if axis == 0: return x * 9 + y
|
||
|
elif axis == 1: return y * 9 + x
|
||
|
else: return ((0,3,6,27,30,33,54,57,60)[x] + (0,1,2,9,10,11,18,19,20)[y])
|
||
|
|
||
|
def axisfor(pos, axis):
|
||
|
if axis == 0: return pos / 9
|
||
|
elif axis == 1: return pos % 9
|
||
|
else: return (pos / 27) * 3 + (pos / 3) % 3
|
||
|
|
||
|
def axismissing(board, x, axis):
|
||
|
bits = 0
|
||
|
for y in range(9):
|
||
|
e = board[posfor(x, y, axis)]
|
||
|
if e is not None: bits |= 1 << e
|
||
|
return 511 ^ bits
|
||
|
|
||
|
def listbits(bits):
|
||
|
return [y for y in range(9) if 0 != bits & 1 << y]
|
||
|
|
||
|
def allowed(board, pos):
|
||
|
bits = 511
|
||
|
for axis in range(3):
|
||
|
x = axisfor(pos, axis)
|
||
|
bits &= axismissing(board, x, axis)
|
||
|
return bits
|
||
|
|
||
|
def pickbetter(b, c, t):
|
||
|
if b is None or len(t) < len(b): return (t, 1)
|
||
|
if len(t) > len(b): return (b, c)
|
||
|
if random.randint(0, c) == 0: return (t, c + 1)
|
||
|
else: return (b, c + 1)
|
||
|
|
||
|
def entriesforboard(board):
|
||
|
return [(pos, board[pos]) for pos in range(81) if board[pos] is not None]
|
||
|
|
||
|
def boardforentries(entries):
|
||
|
board = [None] * 81
|
||
|
for pos, n in entries: board[pos] = n
|
||
|
return board
|
||
|
|
||
|
def boardmatches(b1, b2):
|
||
|
for i in range(81):
|
||
|
if b1[i] != b2[i]: return False
|
||
|
return True
|
||
|
|
||
|
def printcode(n):
|
||
|
if n is None: return '_'
|
||
|
return str(n + 1)
|
||
|
|
||
|
def printboard(board):
|
||
|
out = ""
|
||
|
for row in range(9):
|
||
|
for col in range(9):
|
||
|
out += (""," "," "," "," "," "," "," "," ")[col]
|
||
|
out += printcode(board[posfor(row, col)])
|
||
|
out += ('\n','\n','\n\n','\n','\n','\n\n','\n','\n','\n')[row]
|
||
|
return out
|
||
|
|
||
|
def parseboard(str):
|
||
|
result = []
|
||
|
for w in str.split():
|
||
|
for x in w:
|
||
|
if x in '|-=+': continue
|
||
|
if x in '123456789': result.append(int(x) - 1)
|
||
|
else: result.append(None)
|
||
|
if len(result) == 81: return result
|
||
|
|
||
|
def loadboard(filename):
|
||
|
f = file(filename, 'r')
|
||
|
result = parseboard(f.read())
|
||
|
f.close()
|
||
|
return result
|
||
|
|
||
|
def basedir():
|
||
|
if hasattr(sys.modules[__name__], '__file__'):
|
||
|
return os.path.split(__file__)[0]
|
||
|
elif __name__ == '__main__':
|
||
|
if len(sys.argv) > 0 and sys.argv[0] != '':
|
||
|
return os.path.split(sys.argv[0])[0]
|
||
|
else:
|
||
|
return os.curdir
|
||
|
|
||
|
def loadsudokutemplate(ext):
|
||
|
f = open(os.path.join(basedir(), 'sudoku-template.%s' % ext), 'r')
|
||
|
result = f.read()
|
||
|
f.close()
|
||
|
return result
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
if os.getenv('GATEWAY_INTERFACE') is not None:
|
||
|
sys.exit(cgimain())
|
||
|
sys.exit(main())
|
||
|
|