"""
.. module:: sudoku
:platform: Unix, Windows
:synopsis: oop's method of solving sudoku
.. moduleauthor:: Robert J. Hwang <RobertOfTaiwan@gmail.com>
"""
import sys
import copy
import itertools
import traceback
import time
import os
# global variable
methodLoopIdx = 0 # the index of the methods loops
methodIdx = 0 # the method idx which run now
checkPos = None # set this varible, if you want to make an exception when this position has been set
writeDownAlready = False # if the every un-assigned positions have been writen down their possible number?
emulatePossibles = 2 # to set the possibles when using emulate method, for both positions and numbers
tryStack = [] # the try Stack
tryIdx = 0
tryUse = True
emuUse = True
Scope = 0 # the scope of difficult level
Level = 0 # the level Limit, 0 means no limit
NowPath = os.path.abspath(os.path.dirname(__file__))
ACTION_SET = 's' # action of set number
ACTION_REDUCE = 'r' # action of reduce number
WRITEN_POSSIBLE_LIMITS = 3 # When a position's possible numbers <= this numer,
# it will be assume that it's possible number has been writen down for a human,
# 0 is same as 9 means no any limit
ACTION_GET_INFO = False # if want to get the info of an action,
# those can be re-played by the info to describe the action
SCAN_ONE_NUMBER = True # in the methods of scanning numbers, use this to scan only one number
SCAN_ALL_NUMBER = False # in the methods of scanning numbers, use this to scan all numbers
SCAN_DEF_BEGIN = 1 # in the methods of scanning number, the default begin number
METHOD_DEF_BEGIN = 0 # the default idx of a serial methods to start
METHOD_FILL_LAST = 0 # the idx of the method of filling the last position in a group
METHOD_CHECK_OBVIOUS = 1 # the idx of the method of check obvious numbers
METHOD_WRITE_POSSIBLE = 4 # the method which write down all possible numbers in the positions
DEBUG_MODE = True # it is in the debug mode?
CHECK_MORE_OBVIOUS = True # When a postion will be setting in a method, does check it for more obvious way?
METHOD_BASIC_LEVEL = 7 # Basic methods
METHOD_EMULATE_START = 8 # the emulate method begin idx
METHOD_USE_TRY = True # the default of using try method or not
METHOD_USE_EMU = False # the default of using emulator
METHOD_LEVEL_LIMIT_WHENTRY = 2 # if start using try, set the level limit to use
[docs]class SudokuError(Exception):
"""An exception when x, y can't be set or reduce to or form the number v
t: is the type: 's' means set, 'r' means reduce"""
def __init__(self, x, y, v, t):
self.x = x
self.y = y
self.v = v
self.t = t
[docs]class SudokuDone(Exception):
"""An exception When the table has been filled 81 positions"""
def __init__(self, x, y, v):
self.x = x
self.y = y
self.v = v
[docs]class SudokuStop(Exception):
"""An exception When the the record number >= recLimit"""
def __init__(self):
pass
[docs]class SudokuWhenPosSet(Exception):
"""An exception When the position, checkPos, has been set, and program want to setit"""
def __init__(self, x, y, v):
self.x = x
self.y = y
self.v = v
[docs]class Point:
"""A Position in a Sudoku's table"""
def __init__(self, x, y):
self.x = x
self.y = y
self.v = 0
self.possible = list(i for i in range(1, 10))
self.writen = False # does the position's possible number has been writen by man
self.b = int(x / 3) * 3 + int(y / 3) # the box index which this position belong to
def __repr__(self):
return "p({0},{1})".format(self.x + 1, self.y + 1)
[docs] def can_see(self, p1):
"""this position can see p1? the value can't be 3 or 7 it means the same pos
rtn: 0: can't see p1
1: can see it in x line
2: can see it in y line
4: can see it in the box"""
if self == p1:
return -1
rtn = 0
if self.x == p1.x:
rtn += 1
if self.y == p1.y:
rtn += 2
if self.b == p1.b:
rtn += 4
return rtn
[docs] def can_see_those(self, posList):
"""check this position can see which positions in the posList, a [(x, y),...] list"""
rtn = []
for x, y in posList:
if self.x == x and self.y == y:
continue
if x == self.x or y == self.y:
rtn.append((x, y))
return rtn
class GroupBase:
def __init__(self, idx, p):
self.idx = idx
self.p = p
self.filled = 0
self.possible = list(i for i in range(1, 10))
self.chain = [] # store this group's chain positions
def allow(self, v):
"""check the group can be filled the number(v)?"""
for p1 in self.p:
if p1.v == v:
return False
return True
def get_num_pos(self, v):
"""get the position of a group which have been filled the number(v)"""
rtn = None
for p1 in self.p:
if p1.v == v:
rtn = p1
break
return rtn
def count_num_possible(self, count=1):
"""get the un-assigned position in this group, and possible numbers only in [count] postions
return: [(num,[p1, p2...]),...]"""
rtn = []
# count all possible number in to c and pos list, c is the times it show in the posiiton,
# p records which positions
c = list(0 for x in range(9))
pos = list([] for x in range(9))
for p1 in self.get_all_pos(method="u"):
for num in p1.possible:
c[num - 1] += 1
pos[num - 1].append(p1)
# get all the times = count
for i in range(9):
if c[i] == count:
rtn.append((i + 1, pos[i]))
return rtn
def get_all_pos(self, diff=[], method="a", num=0, notInLineX=None, notInLineY=None, chain=None, possibles=None):
"""
get position list in this group
diff: exclude the positions in it
method: a: all, s:has assigned, u:not assigned
num: if method is u and set the number to 1-9, will get all possible pos which are possible to be assigned the num
notInLineX: exclude the x line's positions
notInLineY: exclude the y line's positions
chain: if set, check it, if it is True, just get the chain positions, or get the un-chained postitions
possibles: if method="u" and possibles!=None, it will only get the possible set's length = possibles
"""
rtn = []
lGetOtherThan = len(diff) > 0
for p1 in self.p:
if (method == "u" and p1.v != 0) or (method == "s" and p1.v == 0):
continue
if lGetOtherThan and p1 in diff:
continue
if num != 0 and method == "u" and num not in p1.possible:
continue
if p1.x == notInLineX or p1.y == notInLineY:
continue
if chain is not None:
if (chain and p1 not in self.chain) or (not chain and p1 in self.chain):
continue
if possibles and method == "u":
if len(p1.possible) != possibles:
continue
rtn.append(p1)
return rtn
[docs]class LineX(GroupBase):
"""Line of X"""
def __repr__(self):
rtn = ""
for x in self.p:
rtn += "{0:3d}\n".format(x.v)
return rtn
[docs]class LineY(GroupBase):
"""Line of Y"""
def __repr__(self):
rtn = ""
for x in self.p:
rtn = rtn + "{0:3d}".format(x.v)
return rtn
[docs]class Box(GroupBase):
"""Box"""
def __init__(self, idx, p):
super(Box, self).__init__(idx, p)
x = int(idx / 3)
y = idx % 3
# record all the box's related boxes
self.effects = set()
self.effectsX = set() # x-direction's effect boxes
self.effectsY = set() # y-direction's effect boxes
for i in range(3):
for j in range(3):
if (i == x and j != y) or (i != x and j == y):
idx = i * 3 + j
self.effectsX.add(idx) if i == x else self.effectsY.add(idx)
self.effects.add(idx)
self.groupnumber = set() # record the box's group number
def __repr__(self):
rtn = ""
for i in range(3):
for j in range(3):
p1 = self.p[j * 3 + i]
rtn = rtn + "{0:3d}".format(p1.v)
rtn += "\n"
return rtn
[docs] def get_group_number(self, num, pos=[], notInLineX=None, notInLineY=None):
"""if the unassigned num in this box which it's all possible positions have the same direction,
we call it as a GroupNumber"""
if len(pos) <= 0:
pos = super(Box, self).get_all_pos(num=num, method="u", notInLineX=notInLineX, notInLineY=notInLineY)
amt = len(pos)
if amt < 2 or amt > 3:
return None
# check the first two position and decide the direction
if pos[0].x == pos[1].x:
idx = pos[0].x
direction = "x"
elif pos[0].y == pos[1].y:
idx = pos[0].y
direction = "y"
else:
return None
# if there is three possible position, check it
if amt > 2:
if not (pos[2].x == idx if direction == "x" else pos[2].y == idx):
return None
# record the number
self.groupnumber.add(num)
return GroupNumber(self.idx, num, pos, direction, idx)
[docs]class GroupNumber():
"""Group Number in Box"""
def __init__(self, b, num, p, direction, idx):
self.b = b # box idx
self.num = num # 1..9
self.p = p # the positions' list which form a group number
self.direction = direction # "x": as a x-line's number, "y": as a y-line's number
self.idx = idx # x-line or y-line's index
def __repr__(self):
rtn = "GroupNumber({0}) in box({1}) formed by {2} at line-{3}({4})".format(self.num, self.b, repr(self.p),
self.direction, self.idx)
return rtn
[docs]class Number:
"""Number Object"""
def __init__(self, v):
self.v = v
self.p = list()
self.filled = 0
self.group = [] # store the GroupNumber info
def __repr__(self):
return repr(self.p)
[docs] def setit(self, p1):
"""save assigned position in the p list"""
self.p.append(p1)
self.filled += 1
[docs] def can_see_by_group_number(self, p1):
"""Check if the position, p1, can be seen of all this number's group number"
return: gn if can be seen by it, or None"""
if len(self.group) <= 0:
return None
for gn in self.group:
if gn.b == p1.b: # at the same box
continue
if (gn.direction == "x" and p1.x == gn.idx) or (gn.direction == "y" and p1.y == gn.idx):
return gn
return None
[docs]class Chain:
"""A chain of two and above positions which are not in the same box but in the same line
and can form a chain, means the possible number in this chain positions only can be filled in these positions"""
def __init__(self, numList, posList):
self.numList = numList
self.posList = posList
#self.direction = "x" if posList[0].x == posList[1].x else "y"
#self.idx = posList[0].x if self.direction == "x" else posList[0].y
def __repr__(self):
return "chain({0} in Pos({1})".format(self.numList, self.posList)
[docs]class Matrix:
"""A Table of a Sudoku"""
def __init__(self, file=""):
self.rec = [] # record all the steps,
# element's format is (x, y, v, t, d), t="s"|"r", d="Description String"
self.filled = 0 # record how many numbers have been assigned in this table
self.done = False # if solved or not
self.error = False # if there is an error occurs
self.lineX = list([] for i in range(9))
self.lineY = list([] for i in range(9))
self.b = list([] for i in range(9))
self.p = list(list(Point(y, x) for x in range(9)) for y in range(9))
self.n = list([] for i in range(10))
for i in range(9):
self.lineX[i] = LineX(i, list(self.p[i][j] for j in range(9)))
for i in range(9):
self.lineY[i] = LineY(i, list(self.p[j][i] for j in range(9)))
for i in range(3):
for j in range(3):
idx = i * 3 + j
self.b[idx] = Box(idx, list(self.p[3 * i + x][3 * j + y] for x in range(3) for y in range(3)))
for i in range(1, 10): # the first, index=0 will not be used
self.n[i] = Number(i)
self.chain = [] # store chains in this list
# read define
if file != "":
self.read(file)
[docs] def get_all_pos(self, diff=[], method="a", num=0, chain=None, possibles=None):
"""get all postion"""
rtn = []
for line in self.lineX:
rtn = rtn + line.get_all_pos(method=method, num=num, chain=chain, diff=diff, possibles=possibles)
return rtn
[docs] def sort_unassigned_pos_by_possibles(self, possibles=0):
"""Get unassign position's possible number list, format is [p1, p2,...]
and Sorted By the possible numbers
possibles: 0 for all, >=2, mean get only the possible numbers for it"""
rtn = []
check = possibles >= 2
for i in range(9):
for j in range(9):
if self.p[i][j].v != 0:
continue
if check and len(self.p[i][j].possible) != possibles:
continue
rtn.append(self.p[i][j])
rtn = sorted(rtn, key=lambda pos: len(pos.possible))
return rtn
[docs] def can_see(self, p0, method="u", num=0):
"""get the possition list which can see the position, p
method: "u": un-assigned positions, "a": all, "s": assigned positions
num: if method="u", the position must have be possible to be filled the number
"""
rtn = []
for group in (self.lineX[p0.x].p, self.lineY[p0.y].p, self.b[p0.b].p):
for p1 in group:
if p1 == p0 or p1 in rtn:
continue
if method == "u":
if p1.v != 0 or (num != 0 and num not in p1.possible):
continue
elif method == "s":
if p1.v == 0:
continue
rtn.append(p1)
return rtn
[docs] def setit(self, x, y, v, d="define", info=""):
"""set the position x, y to be the number v
return: >=1 if set successfully, 0 if it can't be set the number v"""
global checkPos # the position want to check
sets = 0
if not self.allow(x, y, v):
raise SudokuError(x, y, v, ACTION_SET)
return 0
idx = self.p[x][y].b
# the position(x, y)'s possible set to be empty
self.p[x][y].possible.clear()
# the filled numbers add one
self.filled += 1
self.lineX[x].filled += 1
self.lineY[y].filled += 1
self.b[idx].filled += 1
# reduce every group's possible number set
self.b[idx].possible.remove(v)
self.lineX[x].possible.remove(v)
self.lineY[y].possible.remove(v)
# set number in this position
sets += 1
self.p[x][y].v = v
# reduce all effect's positions' possible numbers
for p1 in self.can_see(self.p[x][y], method="u", num=v):
rtn = self.reduce(p1.x, p1.y, v, d="set")
if rtn >= 2:
sets += 1
# record the number's position
self.n[v].setit(self.p[x][y])
# record this step
if d != "define":
self.rec.append((x, y, v, ACTION_SET, d, info))
# check if it is done
if self.filled == 81:
raise SudokuDone(x, y, v)
if DEBUG_MODE:
# check the checkPos is set and now the position is it or not
if checkPos and (x, y) in checkPos:
raise SudokuWhenPosSet(x, y, v)
#if d != "define":
# print("set: p({0},{1})={2} by {3}({4})".format(x+1, y+1, v, d, info))
return sets
[docs] def print_rec(self):
"""Print all the steps of solving process"""
i = 0
for x, y, v, t, d, info in self.rec:
i += 1
print("{5:3d}-{4}: p({0},{1})={2} by {3} - {6}".format(x + 1, y + 1, v, d, t, i, info))
[docs] def reduce(self, x, y, v, d="set", check=False, info=""):
"""reduce the position(x, y)'s possible numbers from v
Return::
int, as following
2 -- if set a number,
1 -- if just set number
0 -- if is not in the possible set, if check is True, it will raise an SudokuError exception
"""
global methodLoopIdx, methodIdx
if len(self.p[x][y].possible) <= 1:
raise SudokuError(x, y, v, ACTION_REDUCE)
if v in self.p[x][y].possible:
self.p[x][y].possible.remove(v)
# record this step
if d != "set":
self.rec.append((x, y, v, ACTION_REDUCE, d, info))
return 1
else:
if check:
raise SudokuError(x, y, v, ACTION_REDUCE)
else:
return 0
[docs] def allow(self, x, y, v):
"""Checking if the position x, y, can be set the value v"""
idx = self.p[x][y].b
if self.p[x][y].v != 0 or not self.lineX[x].allow(v) or not self.lineY[y].allow(v) or not self.b[idx].allow(v):
return False
else:
return True
[docs] def read(self, file):
"""Read Sudoku's Define from file"""
global NowPath
if not os.path.isfile(file):
file = "data/" + file
try:
f = open(file, "r")
except IOError:
return -1
i = 0
for line in f:
if len(line) > 0:
index = line.split(",")
x, y = (int(index[0]) - 1, int(index[1]) - 1)
v = int(index[2])
self.setit(x, y, v)
i += 1
return i
def __repr__(self):
rtn = ""
for i in range(9):
for j in range(9):
rtn = rtn + "{0:3d}".format(self.p[j][i].v)
rtn += "\n"
return rtn
[docs]def fill_only_one_possible(m, first=1, only=False):
"""Check every unassigned position, if it's possible numbers left one only WRITEN_POSSIBLE_LIMIT: True, Check position's writen is True or note False, don't check.
Args:
m: Matrix Object
first (int): the first number of checking
only (bool): just check the first number or not
Returns:
in the tuple format (sets, reduces, method Index to restart using, first, only)
"""
sets = 0
for line in m.p:
for p1 in line:
if (WRITEN_POSSIBLE_LIMITS == 0 or (WRITEN_POSSIBLE_LIMITS > 0 and p1.writen)) and p1.v == 0 and len(
p1.possible) == 1:
m.setit(p1.x, p1.y, p1.possible[0], d="Only One Possible")
sets += 1
return sets, 0, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def fill_last_position_of_group(m, first=1, only=False):
"""If the un-assigned positions in a group(line or box) are only one left"""
sets = 0
for grouptype in (m.lineX, m.lineY, m.b):
for grp in grouptype:
if grp.filled == 8:
for p1 in grp.p:
if p1.v == 0:
m.setit(p1.x, p1.y, grp.possible[0],
d="Last Position in a {0}: {1}".format(grp.__doc__, grp.idx))
sets += 1
break
return sets, 0, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def fill_last_position_by_setting(m, sets):
"""When setting a number, may cause 1-3 groups left only one possible position
check if a group have only position left, just set it"""
recIdx = len(m.rec) - 1
idx = 1
rtn = 0
v = 1
# process every sets from the end of records
while idx <= sets:
while True:
r = m.rec[recIdx]
if r[3] != "s":
recIdx -= 1
else:
break
x = r[0];
y = r[1]
for group in (m.lineX[x], m.lineY[y], m.b[m.p[x][y].b]):
if len(group.possible) != 1:
continue
for p1 in group.p:
if p1.v == 0:
v = group.possible[0]
m.setit(p1.x, p1.y, v, d="Last Position By Setting")
rtn += 1
idx += 1 # next settings
return rtn, 0, METHOD_CHECK_OBVIOUS, v, SCAN_ALL_NUMBER
[docs]def set_obvious_method_for_pos(m, method1, p1, v):
"""Check is there an more obvious method for the position, p1 than method1
Obvious methods include fillLastPostionOfGroup=0 and checkObviousNumber=1
return: True: set, False: not set
"""
if method1 > METHOD_FILL_LAST:
# check p1 is an last postion in a group or not
if m.lineX[p1.x].filled == 8 or m.lineY[p1.y].filled == 8 or m.b[p1.b].filled == 8:
m.setit(p1.x, p1.y, v, d="Change To Be Last Position!")
return True
if method1 > METHOD_CHECK_OBVIOUS:
info = ""
# check p1 can get in an obvious way, like METHOD_CHECK_OBVIOUS
for p2 in m.b[p1.b].get_all_pos(method="u", num=v, diff=[(p1.x, p1.y)]):
if m.lineX[p2.x].allow(v):
return False
else:
if ACTION_GET_INFO:
info = info + repr(p2) + ", lineX(" + repr(m.lineX[p2.x].get_num_pos(v)) + ")\n"
if m.lineY[p2.y].allow(v):
return False
else:
if ACTION_GET_INFO:
info = info + repr(p2) + ", lineY(" + repr(m.lineY[p2.y].get_num_pos(v)) + ")\n"
# after checking every possible poses in a box other than the position, p1,
# that not allowing for the number, v, so p1 must be v
m.setit(p1.x, p1.y, v, d="checkObviousNumber", info=info)
return True
return False
[docs]def check_obvious_number(m, first=1, only=False):
"""Check every number which has been assigned and its effect's boxes' does not have assigned that number
Only: False, check all numbers
True, check the first number only
first: the first number to be checked"""
sets = 0
end = 9 if not only else 1 # if check the first number
for i in range(end):
num = first + i
num = num if num < 10 else num - 9
checked = set() # save the checked box
for p1 in m.n[num].p:
for b in m.b[p1.b].effects:
#print("check number {0} of pos({1},{2})!".format(num, p1.x, p1.y))
possible = []
info = "" # record how to descide to set
if b in checked:
continue
else:
checked.add(b)
if num not in m.b[b].possible:
continue
for p2 in m.b[b].p:
if p2.v != 0 or p2.can_see(p1) > 0:
continue;
if not m.lineX[p2.x].allow(num):
if ACTION_GET_INFO:
info = info + repr(p2) + ", lineX(" + repr(m.lineX[p2.x].get_num_pos(num)) + ")\n"
continue
if not m.lineY[p2.y].allow(num):
if ACTION_GET_INFO:
info = info + repr(p2) + ", lineY(" + repr(m.lineX[p2.x].get_num_pos(num)) + ")\n"
continue
#print(num, p2, p2.possible)
possible.append(p2)
if len(possible) == 1:
sets += 1
flag_set_more_obvious = False
if CHECK_MORE_OBVIOUS:
flag_set_more_obvious = set_obvious_method_for_pos(m, METHOD_CHECK_OBVIOUS, possible[0], num)
if not flag_set_more_obvious:
m.setit(possible[0].x, possible[0].y, num, d="checkObviousNumber", info=info)
# call self to solve the same number completely
r = check_obvious_number(m, first=num, only=SCAN_ALL_NUMBER)
sets += r[0]
return sets, r[1], r[2], r[3], r[4]
return sets, 0, METHOD_CHECK_OBVIOUS, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def update_group_number(m, num):
"""Update the group number, num, in a box, and store those group number in m.n.group list
return: >=0 means the group number's amount in the matrix, m"""
sets = 0
actions = 0
info = ""
#m.n[num].group.clear() # empty first
for i in range(9): # check every box
if num not in m.b[i].possible or num in m.b[i].groupnumber:
continue
pos = m.b[i].get_all_pos(num=num, method='u')
# if the possible position, just set it and return
if len(pos) == 1:
#print(m.b[i], pos, num)
if ACTION_GET_INFO:
pass
SetInMoreObvious = False
if CHECK_MORE_OBVIOUS:
SetInMoreObvious = set_obvious_method_for_pos(m, 3, pos[0], num)
if not SetInMoreObvious:
m.setit(pos[0].x, pos[0].y, num, d="UpdateGroupNumber", info=info)
return 1, 0, METHOD_CHECK_OBVIOUS, num, SCAN_ONE_NUMBER
gn = m.b[i].get_group_number(num, pos=pos)
if gn is not None:
m.n[num].group.append(gn)
actions += 1
if actions > 0:
sets, k, start, first, only = update_indirect_group_number(m, num)
actions = actions + k
else:
start = METHOD_DEF_BEGIN
first = SCAN_DEF_BEGIN
only = SCAN_ALL_NUMBER
return sets, actions, start, first, only
[docs]def update_indirect_group_number(m, num, amt=0, start=METHOD_DEF_BEGIN, first=SCAN_DEF_BEGIN, only=SCAN_ALL_NUMBER):
"""Update in-direct Group Number, formed by the assigned number and groupnumber already known,
a recursive function"""
info = ""
for gn in m.n[num].group:
effects = m.b[gn.b].effectsX if gn.direction == "x" else m.b[gn.b].effectsY
#print(amt, effectBoxes, gn)
for idx in effects: # check every effect box, does the possible position for the num can form a group
# number or not
# check if the num is existed in the box, or already as a groupnumber
if num not in m.b[idx].possible or num in m.b[idx].groupnumber:
continue
if gn.direction == "x":
pos = m.b[idx].get_all_pos(num=num, method='u', notInLineX=gn.idx)
else:
pos = m.b[idx].get_all_pos(num=num, method='u', notInLineY=gn.idx)
if len(pos) == 1:
if ACTION_GET_INFO:
pass
flag = False
if CHECK_MORE_OBVIOUS:
flag = set_obvious_method_for_pos(m, 4, pos[0], num)
if not flag:
m.setit(pos[0].x, pos[0].y, num, d="UpdateInDirectGroupNumber", info=info)
return 1, amt, METHOD_CHECK_OBVIOUS, num, SCAN_ONE_NUMBER
gn0 = m.b[idx].get_group_number(num, pos=pos)
if gn0 is not None:
m.n[num].group.append(gn0)
amt += 1
# call self again to get all possible InDirect GroupNumber
return update_indirect_group_number(m, num, amt=amt)
return 0, amt, start, first, only
[docs]def check_inobvious_number(m, first=1, only=False):
"""Check every number which has been assigned and known as group-number and its effect's boxes' does not have assigned that number"
Only: False, check all numbers
True, check the first number only
first: the first number to be checked"""
sets = 0
actions = 0
end = 9 if not only else 1 # if check the first number
for i in range(end):
num = first + i
num = num if num < 10 else num - 9
checked = set() # save the checked box
sets, actions, start, first, only = update_group_number(m, num)
if sets > 0:
return sets, actions, start, first, only
# check every box
for bi in range(9):
if num not in m.b[bi].possible:
continue
pos = []
info = ""
for p1 in m.b[bi].get_all_pos(num=num, method="u"):
gn = m.n[num].can_see_by_group_number(p1)
if gn is not None:
if ACTION_GET_INFO:
info = info + repr(gn) + "\n"
continue
else:
pos.append(p1)
if len(pos) == 1:
flag = False
if CHECK_MORE_OBVIOUS:
flag = set_obvious_method_for_pos(m, 5, pos[0], num)
if not flag:
m.setit(pos[0].x, pos[0].y, num, d="checkInObviousNumber", info=info)
return 1, actions, METHOD_CHECK_OBVIOUS, num, SCAN_ALL_NUMBER
return sets, actions, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def check_line_last_possible_for_number(m, first=1, only=False):
"""Check every line that only have one position for un-assigned number"""
sets = 0
for groupType in (m.lineX, m.lineY):
for line in groupType:
if line.filled >= 8:
continue
for c in line.count_num_possible(count=1):
p1 = c[1][0]
info = ""
if ACTION_GET_INFO:
pass
m.setit(p1.x, p1.y, c[0], d="checkLineLastPossibleForNumber", info=info)
sets += 1
# if get one, just return, let other methods to process others
return sets, 0, METHOD_DEF_BEGIN, c[0], SCAN_ONE_NUMBER
return sets, 0, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def write_down_possible(m, first=1, only=False):
"""Write down the possible numbers in every un-assigned position
if WRITEN_POSSIBLE_LIMITS has set to 1..9, it will only write down the
possibles which <= that limits"""
global writeDownAlready
writeDownAlready = True
for line in m.p:
for p1 in line:
if p1.v != 0 or p1.writen:
continue
# if set writen limits and the possible numbers great it, just pretend it can not be seen for the user
if 0 < WRITEN_POSSIBLE_LIMITS < len(p1.possible):
continue
p1.writen = True
if len(p1.possible) == 1:
v = p1.possible[0]
m.setit(p1.x, p1.y, v, d="writeDownPossible")
return 1, 0, METHOD_CHECK_OBVIOUS, v, SCAN_ALL_NUMBER
return 0, 0, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def reduce_by_group_number(m, first=1, only=False):
"""Reduce the possible number in a posiition by GroupNumber"""
sets = 0
actions = 0
end = 9 if not only else 1 # if check the first number
for i in range(9):
num = first + i
num = num if num < 10 else num - 9
for gn in m.n[num].group:
for bi in m.b[gn.b].effectsX if gn.direction == "x" else m.b[gn.b].effectsY:
for p1 in m.b[bi].get_all_pos(method="u", num=num):
if p1.x == gn.idx if gn.direction == "x" else p1.y == gn.idx:
rtn = m.reduce(p1.x, p1.y, num, d="reduceByGroupNumber")
actions += 1
return sets, actions, METHOD_DEF_BEGIN, num, SCAN_ONE_NUMBER
def get_chains(m, group, pos, numbers):
# get the possible pos's numbers chain
amt = 0
q = []
for p1 in pos:
if len(p1.possible) <= numbers:
q.append(p1)
# if qualified positions is less than numbers, just return
if len(q) < numbers:
return []
rtn = []
for c in itertools.combinations(q, numbers):
s = set()
for p1 in c:
s = s | set(p1.possible)
if len(s) == numbers:
chain = Chain(list(s), c)
rtn.append(chain)
m.chain.append(chain)
for p1 in c:
group.chain.append(p1)
amt = amt + 1
return rtn
[docs]def update_chain(m, first=1, only=False):
"""Update the chain of line
return: >=0 means the chain number's amount in the matrix, m"""
sets = 0
reduces = 0
info = ""
for groupType in (m.lineX, m.lineY, m.b):
for g in groupType:
pos = g.get_all_pos(method="u", chain=False)
k = len(pos)
if k == 0:
continue
elif k == 1:
#print(g, g.idx, pos, k, pos[0].possible)
m.setit(pos[0].x, pos[0].y, pos[0].possible[0], d="updateChain")
return 1, 0, METHOD_CHECK_OBVIOUS, pos[0].possible[0], SCAN_ALL_NUMBER
else:
maxChainNumbers = min(k, WRITEN_POSSIBLE_LIMITS) if WRITEN_POSSIBLE_LIMITS != 0 else k
for i in range(1, maxChainNumbers):
chains = get_chains(m, g, pos, i + 1)
if len(chains) == 0:
continue
# reduce by chain and return
for chain in chains:
for p1 in g.get_all_pos(method="u", diff=chain.posList):
for v in chain.numList:
if v in p1.possible:
m.reduce(p1.x, p1.y, v, d="updateChain", info=repr(chain))
reduces += 1
if reduces > 0:
return 0, reduces, METHOD_CHECK_OBVIOUS, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
return sets, reduces, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def reduce_by_two_possible_in_one_position(m, first=1, only=False):
"""when a position(p1) has two possible numbers only, we can assume if the position is one number(first)
then try to emulate to set the position with the other number(second),
then see the first number will be filled in a position(p2) which the position can see it
if so, we can reduce all these positions which can see p1 and p2 at the same time from the first number"""
reduces = 0
sets = 0
for p1 in m.get_all_pos(method="u", possibles=2):
for i in range(2):
if i == 0:
first, second = p1.possible
else:
second, first = p1.possible
pos = m.can_see(p1, method="u", num=first)
if len(pos) <= 1:
continue
else:
targets = [(p.x, p.y) for p in pos]
rtn, m1, idx = emulator(m, p1.x, p1.y, second, targets=targets, checkval=first)
if rtn == 2:
m.setit(p1.x, p1.y, second, d="Emulate it and solve the sudoku!")
return 1, 0, METHOD_CHECK_OBVIOUS, second, SCAN_ALL_NUMBER
elif rtn == 1:
p0 = m.p[targets[idx][0]][targets[idx][1]]
for x, y in p0.can_see_those(targets):
if first in m.p[x][y].possible:
#print("{4} and {5} reduce ({0},{1})'s possilbe={2} from {3}".format(x+1, y+1, m.p[x][y].possible, first, p1, p0))
r = m.reduce(x, y, first,
d="reduceByTwoPossibleInOnePostion{0}, first={1}, second={2} with postion:{3}".format(
(p1.x, p1.y), first, second, p0))
if r == 1:
reduces += 1
elif r == 2:
sets += 1
if sets > 0 or reduces > 0:
return sets, reduces, METHOD_CHECK_OBVIOUS, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
elif rtn == -1:
m.setit(p1.x, p1.y, first, d="Emulate it and it causes error, so the number must be another!")
return 1, 0, METHOD_CHECK_OBVIOUS, first, SCAN_ALL_NUMBER
else:
continue
return sets, reduces, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def reduce_by_emulate_possible_in_one_position(m, first=1, only=False):
"""when a position(p1) has 2 or more possible numbers,
we can emulate every possible number and get its result,
1. if it causes an error, we can reduce that number,
2. if it can solve the sudoku, we can set this number,
3. if all possible number can's get condition 1 or 2, we can compare their rec, if they have the same records, we can do it.
"""
global emulatePossibles
print("reduceByEmulatePossibleInOnePositions: {0}".format(emulatePossibles))
reduces = 0
sets = 0
emu = [] # record the emulate method, [(p1,v),....]
result = [] # save the emulate result of matrix for every possible number
for p1 in m.get_all_pos(method="u", possibles=emulatePossibles):
for num in p1.possible:
emu.append((p1, num))
rtn, m1, idx = emulator(m, p1.x, p1.y, num)
if rtn == 2:
m.setit(p1.x, p1.y, num, d="Emulate it and solve the sudoku!")
return 1, 0, METHOD_CHECK_OBVIOUS, num, SCAN_ALL_NUMBER
elif rtn == -1:
m.reduce(p1.x, p1.y, num, d="Emulate it and it causes error, so the number can be reduce!")
return 0, 1, METHOD_CHECK_OBVIOUS, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
else:
result.append(m1)
continue
if len(result) == emulatePossibles:
sets, reduces = compare_result(m, emu, result)
return sets, reduces, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def reduce_by_emulate_possible_number_in_group(m, first=1, only=False):
"""when a group(lineX, lineY, Box) has 2 or more position have the same possible number,
we can emulate every position to set the number and get its result,
1. if it causes an error, we can reduce the position's possible number from that number,
2. if it can solve the sudoku, we can set this number in the position,
3. if all possible position can's get condition 1 or 2, we can compare their rec, if they have the same records, we can do it.
"""
global emulatePossibles
print("reduceByEmulatePossibleNumberInGroup: {0}".format(emulatePossibles))
reduces = 0
sets = 0
emu = [] # record the emulate method, [(p1,v),....]
result = [] # save the emulate result of matrix for every possible number
for groupType in (m.lineX, m.lineY, m.b):
for idx in range(9):
for num, pos in groupType[idx].count_num_possible(count=emulatePossibles):
for p1 in pos:
emu.append((p1, num))
rtn, m1, idx = emulator(m, p1.x, p1.y, num)
if rtn == 2:
m.setit(p1.x, p1.y, num, d="Emulate it and solve the sudoku!")
return 1, 0, METHOD_CHECK_OBVIOUS, num, SCAN_ALL_NUMBER
elif rtn == -1:
m.reduce(p1.x, p1.y, num, d="Emulate it and it causes error, so the number can be reduce!")
return 0, 1, METHOD_CHECK_OBVIOUS, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
else:
result.append(m1)
continue
if len(result) == emulatePossibles:
s, r = compare_result(m, emu, result)
sets += s
reduces += r
return sets, reduces, METHOD_DEF_BEGIN, SCAN_DEF_BEGIN, SCAN_ALL_NUMBER
[docs]def compare_result(m, emu, result):
"""compare the result list, to check if there are same result in every step after the last record of original m.rec
if same for all emulate result, it means that it must be true, so can do by it
"""
global emulatePossibles
sets = 0
reduces = 0
recIdx = len(m.rec)
results = len(result)
m1 = result[0] # use the first result to compare with others
i = 0
for r1 in m1.rec[recIdx:]:
i += 1 # step idx of m1
info = "The #{0} step of the {1} emulate as {2} is same with follows:".format(i, emu[0][0], emu[0][1])
same = 0
resultIdx = 0
for m2 in result[1:]:
resultIdx += 1
idx = 0 # step idx of others
for r2 in m2.rec[recIdx:]:
idx += 1
if (r1[0], r1[1], r1[2], r1[3]) == (r2[0], r2[1], r2[2], r2[3]):
if ACTION_GET_INFO:
info = info + "\nThe #{0} step of the {1} emulate as {2}".format(idx, emu[resultIdx][0],
emu[resultIdx][1])
same += 1
break
if same == results - 1:
if r1.t == "s":
sets += 1
m.setit(r1[0], r1[1], r1[2], d="compareResult for {0} emulate!".format(results), info=info)
else:
reduces += 1
m.reduce(r1[0], r1[1], r1[2], d="compareResult for {0} emulate!".format(results), info=info)
return sets, reduces
[docs]def emulator(m, x, y, v, targets=[], checkval=0):
"""emulate the x, y to be set v, then start to use some basic methods to try to solve
it will stop when and return
1: one of the targets have been set the checkval
2: isDone
-1: error is True
0: all basic methods have been tested, and can't solve
and the result matrix"""
global writeDownAlready, checkPos
check_pos_save = checkPos
m1 = copy.deepcopy(m)
if len(targets) > 0:
check_target = True
checkPos = targets
else:
check_target = False
methodLoopIdx = 0
begin = time.time()
allmethods = reg_method()
start = METHOD_CHECK_OBVIOUS # start from for the first time
num = 1
only = False
rtn = 0
idx = 0 # the index of the targets, which = checkval
emulate_first = True
while True:
actions = 0
methodLoopIdx += 1
try:
if emulate_first:
#print(m1.p[2][2].possible)
#print("Emulator Start in P({0},{1})={2}!".format(x, y, v))
m1.setit(x, y, v, d="Emulator Start!")
emulate_first = False
for method in allmethods[start:METHOD_BASIC_LEVEL]:
methodIdx = method.idx
# every method have 5 return values:
# sets: how many positions have been set
# reduces: how many numbers have been reduced
# start: which method will start to be used, default is 1
# num: which number will be the first number to be procossed, for methods that look over all numbers
# only: for methods which look over all numbers to process one only, which is the num
sets, reduces, start, num, only = method.run(m1, first=num, only=only)
#print("Emulate#{0}-{2}: {1}, sets={3}, reduces={4}".format(methodLoopIdx, method.name, methodIdx, sets, reduces))
if sets > 0:
rtn = fill_last_position_by_setting(m1, sets)
if rtn[0] > 0:
start = rtn[2];
first = rtn[3];
only = rtn[4]
#print("Emulate#{0}-last: {1}, sets={3}, reduces={4}".format(methodLoopIdx, "LastPosition", methodIdx, rtn, reduces))
sets = sets + rtn[0]
if (sets > 0 or reduces > 0) and writeDownAlready:
rtn = fill_only_one_possible(m1)
if rtn[0] > 0:
sets = rtn[0];
start = rtn[2];
first = rtn[3];
only = rtn[4]
#print("Emulate#{0}-only: {1}, sets={3}, reduces={4}".format(methodLoopIdx, "LastPossible", methodIdx, rtn, reduces))
if sets > 0 or reduces > 0:
actions = actions + sets + reduces
break
except SudokuDone as err:
print("It is done by the last position({0},{1}) to set to be {2}!".format(err.x, err.y, err.v))
rtn = 2
break
except SudokuWhenPosSet as err:
#print("It is stopped by the position({0}) be set! method={1}".format(checkPos, methodIdx))
if err.v == checkval:
idx = targets.index((err.x, err.y))
rtn = 1
break
else:
continue
except SudokuError as err:
rtn = -1
print("It is impossible for {0}, {1} to set/reduce {2}! ({3})".format(err.x, err.y, err.v, err.t))
#traceback.print_exc()
break
except: # unexpected error
print("Unexpected error:", sys.exc_info()[0])
traceback.print_exc()
break
if actions <= 0:
break
#restore
checkPos = check_pos_save
#print(checkPos)
return rtn, m1, idx
[docs]def try_error(m=None, file="", depth=0):
"""Try Error Method, only fill the first possible postion"""
if file != "":
m = Matrix(file=file)
possibles = m.sort_unassigned_pos_by_possibles()
done = False
depth += 1
m1 = copy.deepcopy(m)
p1 = possibles[0]
k = len(p1.possible)
if k <= 0:
#print("try#{0}: {1} has empty possible!".format(depth, p1))
return False
elif k == 1:
#print("try#{0}: {1} has only one possible!, set it to {2}".format(depth, p1, p1.possible[0]))
try:
m1.setit(p1.x, p1.y, p1.possible[0], d="try")
flag = try_error(m1, depth=depth)
if flag:
return True
else:
return False
except SudokuDone:
print(m1)
return True
except SudokuError:
return False
else:
flag = False
m2 = copy.deepcopy(m1)
for v in p1.possible:
try:
if m1.allow(p1.x, p1.y, v):
#print("try#{0}: {1} try set it to be {2} of {3}".format(depth, p1, v, p1.possible))
m1.setit(p1.x, p1.y, v, d="try")
flag = try_error(m1, depth=depth)
if flag:
return True
else:
m1 = copy.deepcopy(m2)
continue
else:
#print("try#{0}: {1} is impossible to be set to be {2}".format(depth, p1, v))
continue
except SudokuDone as err:
print(m1)
done = True
return True
except SudokuError as err:
m1 = copy.deepcopy(m2)
continue
if not flag:
#print("try#{2}: {0} is not impossible to set any for {1}".format(p1, p1.possible, depth))
# restore it and continue
# m1 = copy.deepcopy(m)
return False
else:
#print(p1, p1.v, p1.possible)
return True
[docs]def guess(m, idx=0, first=0, only=False):
"""Guess Method"""
global tryStack, tryIdx, Scope, Level
# if start using tryMethod, set the level to the METHOD_LEVEL_LIMIT_WHENTRY
Level = METHOD_LEVEL_LIMIT_WHENTRY
Scope += 5 # it is easy as the method of write down possible
if idx == 0: # Add New Try
tryIdx += 1
possibles = m.sort_unassigned_pos_by_possibles() # get all unassigned postion and sorted by the possibles number
m1 = copy.deepcopy(m)
p1 = possibles[0] # the first un-assigned postion
x = p1.x;
y = p1.y
tryStack.append([m1, x, y, 0])
print("Try Add: {0},{1} to set {2} of {3}".format(x, y, p1.possible[0], p1.possible))
else:
i = tryIdx - 1
tryStack[i][3] = idx
x = tryStack[i][1];
y = tryStack[i][2]
print("Try Idx={3}: {0},{1} to set {2} of {4}".format(x, y, m.p[x][y].possible[idx], idx, m.p[x][y].possible))
v = m.p[x][y].possible[idx]
m.setit(x, y, v, d="try")
return 1, 0, METHOD_CHECK_OBVIOUS, v, SCAN_ALL_NUMBER
[docs]class SolveMethod():
"""Method Object"""
lastNumber = 1
def __init__(self, fun, idx, name="", level=0, obvious=True):
self.fun = fun
self.idx = idx
self.name = name if name != "" else fun.__name__
self.des = fun.__doc__
self.level = level # the difficult level for human, can be set 0-9
self.obvious = obvious
def run(self, m, *args, **ks):
return self.fun(m, *args, **ks)
[docs]def reg_method():
"""register all method as an object and save them into a list to return"""
global tryUse, emuUse
methods = []
#methods.append(SolveMethod(fillOnlyOnePossible, 0, level=0))
methods.append(SolveMethod(fill_last_position_of_group, 1, level=0))
methods.append(SolveMethod(check_obvious_number, 2, level=1))
methods.append(SolveMethod(check_line_last_possible_for_number, 3, level=2))
methods.append(SolveMethod(check_inobvious_number, 4, level=3))
methods.append(SolveMethod(reduce_by_group_number, 5, level=5))
methods.append(SolveMethod(write_down_possible, 6, level=5))
methods.append(SolveMethod(update_chain, 7, level=10))
methods.append(SolveMethod(reduce_by_two_possible_in_one_position, 8, level=15))
if emuUse:
methods.append(SolveMethod(reduce_by_emulate_possible_in_one_position, 9, level=20))
methods.append(SolveMethod(reduce_by_emulate_possible_number_in_group, 10, level=20))
if tryUse:
methods.append(SolveMethod(guess, 11, level=0))
return methods
[docs]def solve(file, loop_limit=0, rec_limit=0, check=None, level_limit=0, emu_limits=2, use_try=METHOD_USE_TRY,
use_emu=METHOD_USE_EMU):
"""Solve a sudoku which define in a file!
loopLimit: the limit for the method loops, 0: no limits
recLimit: when the records >= recLimit, it will stop, 0: no limits"""
global methodLoopIdx # how many method loops it has taken
global methodIdx # the method idx of now running
global checkPos # setting the position want to check
global writeDownAlready # if the every un-assigned positions have been writen down their possible number?
global emulatePossibles # set the emulate possibles
global tryStack, tryIdx, tryUse, Scope, emuUse
global Level
writeDownAlready = False
checkPos = check
methodLoopIdx = 0
begin = time.time()
m = Matrix(file=file)
start = METHOD_CHECK_OBVIOUS # start from for the first time
num = 1
only = False
Scope = 0
max_method = 0
tryIdx = 0
tryUse = use_try
emuUse = use_emu
tryStack = []
allmethods = reg_method()
Level = level_limit
while True:
actions = 0
methodLoopIdx += 1
try:
for method in allmethods[start:]:
if 0 < Level < method.level:
continue
methodIdx = method.idx
# every method have 5 return values:
# sets: how many positions have been set
# reduces: how many numbers have been reduced
# start: which method will start to be used, default is 1
# num: which number will be the first number to be procossed, for methods that look over all numbers
# only: for methods which look over all numbers to process one only, which is the num
sets, reduces, start, num, only = method.run(m, first=num, only=only)
Scope = Scope + method.level
max_method = max(max_method, method.idx)
print("Try#{0}-{2}: {1}, sets={3}, reduces={4}".format(methodLoopIdx, method.name, methodIdx, sets,
reduces))
if sets > 0:
rtn = fill_last_position_by_setting(m, sets)
if rtn[0] > 0:
start, first, only = rtn[2:]
print(
"Try#{0}-last: {1}, sets={3}, reduces={4}".format(methodLoopIdx, "LastPosition", methodIdx,
rtn, reduces))
sets = sets + rtn[0]
if (sets > 0 or reduces > 0) and writeDownAlready:
rtn = fill_only_one_possible(m)
if rtn[0] > 0:
sets = rtn[0]
start, first, only = rtn[2:]
print(
"Try#{0}-only: {1}, sets={3}, reduces={4}".format(methodLoopIdx, "LastPossible", methodIdx,
rtn, reduces))
if DEBUG_MODE:
if 0 < rec_limit <= len(m.rec):
raise SudokuStop()
if sets > 0 or reduces > 0:
emulatePossibles = 2 # if any thing work, the emulatePossible set to the begin number
actions = actions + sets + reduces
break
except SudokuDone as err:
print("It is done by the last position({0},{1}) to set to be {2}!".format(err.x, err.y, err.v))
break
except SudokuStop:
print("It is stop by the limit of recLimit set to {0}:".format(rec_limit))
m.print_rec()
break
except SudokuWhenPosSet:
print("It is stopped by the position({0}) be set! method={1}".format(checkPos, methodIdx))
traceback.print_exc()
break
except SudokuError as err:
if tryUse and tryIdx > 0:
flag = False
while tryIdx > 0:
m1, x, y, idx = tryStack[tryIdx - 1]
idx += 1
if len(m1.p[x][y].possible) > idx:
m = copy.deepcopy(m1)
sets, reduces, start, num, only = guess(m, idx=idx)
flag = True
break
else:
tryIdx -= 1
tryStack.pop(-1)
if flag:
continue
print("It is impossible for {0}, {1} to set/reduce {2}! ({3})".format(err.x, err.y, err.v, err.t))
traceback.print_exc()
break
except KeyboardInterrupt:
traceback.print_exc()
break
if DEBUG_MODE:
# for testing
if methodLoopIdx == loop_limit:
break
if actions <= 0:
if emulatePossibles < emu_limits:
Scope += 1000
emulatePossibles += 1
print("Try emulate numbers = {0}".format(emulatePossibles))
start = METHOD_EMULATE_START
continue
else:
break
print(m)
if m.filled < 81:
print("Can't solve it, still need {0} more effort!".format(81 - m.filled))
else:
print(
"Done! good job, it takes {0}! Level={1}, Methods Used={2}".format(time.time() - begin, Scope, max_method))
return m