else/Sudoku/sudoku_generator.py
Voussoir 46b49f7113 else
More sudoku
2015-01-29 02:37:12 -08:00

251 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())