Skip to content

Commit 00a477a

Browse files
author
paul.rutledge
committed
Initial commit
0 parents  commit 00a477a

File tree

9 files changed

+449
-0
lines changed

9 files changed

+449
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.idea
2+
*.pyc
3+
*.iml

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
## What
2+
This is a sudoku generating application. It generates a sudoku puzzle with a unique solution
3+
of varying difficulties according to the requested difficulty.
4+
5+
## Why
6+
This was one of my projects for a math modeling class in college.
7+
8+
## Status
9+
This is not being actively developed, though there are certainly
10+
improvements that could be made.
11+
12+
## Dependencies
13+
This project relies on numpy for performant array operations. `pip install numpy`.
14+
15+
## So what?
16+
Yep, lots of sudoku generators exist. Something cool that makes this one unique is
17+
the concept of board density in relation to difficulty. The basic idea is that the
18+
more disjoint the set of starting cells is, the more difficult the puzzle will be
19+
to solve. This goes beyond the normal measure of "less starting cells is more difficult"
20+
and begins to consider what particular configurations of those starting cells makes for
21+
the most difficult puzzle.
22+
23+
## How do I use it?
24+
```bash
25+
python sudoku_generator.py base.txt <difficulty>
26+
```
27+
28+
## Difficulties & Sample Results
29+
- easy
30+
- <pre>
31+
6|_|4|_|_|1|_|7|9
32+
_|8|7|6|_|_|1|_|_
33+
5|_|3|9|_|8|2|_|_
34+
2|4|_|_|_|_|5|6|_
35+
8|7|_|3|6|5|_|_|2
36+
3|_|_|_|1|4|_|9|8
37+
1|3|_|7|_|9|6|5|_
38+
7|9|_|_|5|6|_|2|1
39+
_|_|5|1|2|_|_|8|7
40+
</pre>
41+
- medium
42+
- <pre>
43+
_|_|_|_|_|_|_|7|8
44+
5|_|_|7|_|_|_|4|2
45+
9|_|8|4|_|6|_|_|1
46+
_|_|6|2|3|1|7|_|_
47+
_|_|3|_|_|7|4|5|6
48+
7|8|9|_|_|_|_|_|_
49+
2|1|_|9|7|_|3|_|_
50+
8|9|_|_|5|_|2|1|_
51+
_|6|_|_|4|_|8|_|7
52+
</pre>
53+
- hard
54+
- <pre>
55+
_|_|_|_|_|_|_|5|_
56+
_|8|9|5|_|_|1|_|_
57+
5|_|_|4|1|2|_|_|_
58+
_|_|_|_|7|9|4|2|_
59+
_|6|_|_|_|_|7|8|_
60+
8|9|_|_|4|6|_|_|_
61+
9|_|8|6|_|_|_|_|_
62+
_|_|_|3|2|1|_|_|7
63+
3|_|2|_|8|_|5|_|4
64+
</pre>
65+
- extreme
66+
- <pre>
67+
_|4|_|_|1|_|9|_|_
68+
_|_|5|_|_|_|6|_|4
69+
_|7|_|_|2|_|_|1|_
70+
3|_|_|_|9|_|4|_|5
71+
9|8|_|_|_|_|1|_|_
72+
_|_|_|1|3|_|_|_|_
73+
5|_|3|2|_|_|_|7|_
74+
_|_|_|3|_|6|_|4|1
75+
_|_|2|8|_|_|_|_|6
76+
</pre>

Sudoku/Board.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import random
2+
3+
from Cell import Cell
4+
5+
6+
class Board:
7+
8+
# intializing a board
9+
def __init__(self, numbers=None):
10+
11+
# we keep list of cells and dictionaries to point to each cell
12+
# by various locations
13+
self.rows = {}
14+
self.columns = {}
15+
self.boxes = {}
16+
self.cells = []
17+
18+
# looping rows
19+
for row in xrange(0, 9):
20+
# looping columns
21+
for col in xrange(0, 9):
22+
# calculating box
23+
box = 3 * (row / 3) + (col / 3)
24+
25+
# creating cell instance
26+
cell = Cell(row, col, box)
27+
28+
# if initial set is given, set cell value
29+
if not numbers is None:
30+
cell.value = numbers.pop(0)
31+
else:
32+
cell.value = 0
33+
34+
# initializing dictionary keys and corresponding lists
35+
# if they are not initialized
36+
if not row in self.rows:
37+
self.rows[row] = []
38+
if not col in self.columns:
39+
self.columns[col] = []
40+
if not box in self.boxes:
41+
self.boxes[box] = []
42+
43+
# adding cells to each list
44+
self.rows[row].append(cell)
45+
self.columns[col].append(cell)
46+
self.boxes[box].append(cell)
47+
self.cells.append(cell)
48+
49+
50+
# returning cells in puzzle that are not set to zero
51+
def get_used_cells(self):
52+
return [x for x in self.cells if x.value != 0]
53+
54+
# returning cells in puzzle that are set to zero
55+
def get_unused_cells(self):
56+
return [x for x in self.cells if x.value == 0]
57+
58+
# returning all possible values that could be assigned to the
59+
# cell provided as argument
60+
def get_possibles(self, cell):
61+
all = self.rows[cell.row] + self.columns[cell.col] + self.boxes[cell.box]
62+
excluded = set([x.value for x in all if x.value!=0 and x.value != cell.value])
63+
results = [x for x in range(1, 10) if x not in excluded]
64+
return results
65+
66+
# calculates the density of a specific cell's context
67+
def get_density(self,cell):
68+
all = self.rows[cell.row] + self.columns[cell.col] + self.boxes[cell.box]
69+
if cell.value != 0: all.remove(cell)
70+
return len([x for x in set(all) if x.value != 0])/20.0
71+
72+
# gets complement of possibles, values that cell cannot be
73+
def get_excluded(self,cell):
74+
all = self.rows[cell.row] + self.columns[cell.col] + self.boxes[cell.box]
75+
excluded = set([x.value for x in all if x.value!=0 and x.value != cell.value])
76+
77+
78+
# swaps two rows
79+
def swap_row(self, row_index1, row_index2, allow=False):
80+
if allow or row_index1 / 3 == row_index2 / 3:
81+
for x in range(0, len(self.rows[row_index2])):
82+
temp = self.rows[row_index1][x].value
83+
self.rows[row_index1][x].value = self.rows[row_index2][x].value
84+
self.rows[row_index2][x].value = temp
85+
else:
86+
raise Exception('Tried to swap non-familial rows.')
87+
88+
# swaps two columns
89+
def swap_column(self, col_index1, col_index2, allow=False):
90+
if allow or col_index1 / 3 == col_index2 / 3:
91+
for x in range(0, len(self.columns[col_index2])):
92+
temp = self.columns[col_index1][x].value
93+
self.columns[col_index1][x].value = self.columns[col_index2][x].value
94+
self.columns[col_index2][x].value = temp
95+
else:
96+
raise Exception('Tried to swap non-familial columns.')
97+
98+
# swaps two stacks
99+
def swap_stack(self, stack_index1, stack_index2):
100+
for x in range(0, 3):
101+
self.swap_column(stack_index1 * 3 + x, stack_index2 * 3 + x, True)
102+
103+
# swaps two bands
104+
def swap_band(self, band_index1, band_index2):
105+
for x in range(0, 3):
106+
self.swap_row(band_index1 * 3 + x, band_index2 * 3 + x, True)
107+
108+
# copies the board
109+
def copy(self):
110+
b = Board()
111+
for row in xrange(0,len(self.rows)):
112+
for col in xrange(0,len(self.columns)):
113+
b.rows[row][col].value=self.rows[row][col].value
114+
return b
115+
116+
# returns string representation
117+
def __str__(self):
118+
output = []
119+
for index, row in self.rows.iteritems():
120+
my_set = map(str, [x.value for x in row])
121+
new_set = []
122+
for x in my_set:
123+
if x == '0': new_set.append("_")
124+
else: new_set.append(x)
125+
output.append('|'.join(new_set))
126+
return '\r\n'.join(output)
127+
128+
129+
# exporting puzzle to a html table for prettier visualization
130+
def html(self):
131+
html = "<table>"
132+
for index, row in self.rows.iteritems():
133+
values = []
134+
row_string = "<tr>"
135+
for x in row:
136+
if x.value == 0:
137+
values.append(" ")
138+
row_string += "<td>%s</td>"
139+
else:
140+
values.append(x.value)
141+
row_string +="<td>%d</td>"
142+
row_string += "</tr>"
143+
html += row_string % tuple(values)
144+
html += "</table>"
145+
return html

Sudoku/Cell.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class Cell:
2+
3+
# defining the cell, each cell keeps track of its own value and location
4+
def __init__(self,row,col,box):
5+
self.row = row
6+
self.col = col
7+
self.box = box
8+
9+
self.value = 0
10+
11+
# returns a string representation of cell (for debugging)
12+
def __str__(self):
13+
temp = (self.value,self.row,self.col,self.box)
14+
return "Value: %d, Row: %d, Col: %d, Box: %d" % temp
15+

Sudoku/Generator.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from Board import *
2+
from Solver import *
3+
4+
class Generator:
5+
6+
# constructor for generator, reads in a space delimited
7+
def __init__(self, starting_file):
8+
9+
# opening file
10+
f = open(starting_file)
11+
12+
# reducing file to a list of numbers
13+
numbers = filter(lambda x: x in '123456789',list(reduce(lambda x,y:x+y,f.readlines())))
14+
numbers = map(int,numbers)
15+
16+
# closing file
17+
f.close()
18+
19+
# constructing board
20+
self.board = Board(numbers)
21+
22+
# function randomizes an existing complete puzzle
23+
def randomize(self, iterations):
24+
25+
# not allowing transformations on a partial puzzle
26+
if len(self.board.get_used_cells())==81:
27+
28+
# looping through iterations
29+
for x in range(0, iterations):
30+
31+
# to get a random column/row
32+
case = random.randint(0, 3)
33+
34+
# to get a random band/stack
35+
block = random.randint(0, 2) * 3
36+
37+
# in order to select which row and column we shuffle an array of
38+
# indices and take both elements
39+
options = range(0,3)
40+
random.shuffle(options)
41+
piece1, piece2 = options[0],options[1]
42+
43+
# pick case according to random to do transformation
44+
if case == 0:
45+
self.board.swap_row(block + piece1, block + piece2)
46+
elif case == 1:
47+
self.board.swap_column(block + piece1, block + piece2)
48+
elif case == 2:
49+
self.board.swap_stack(piece1, piece2)
50+
elif case == 3:
51+
self.board.swap_band(piece1, piece2)
52+
else:
53+
raise Exception('Rearranging partial board may compromise uniqueness.')
54+
55+
# method gets all possible values for a particular cell, if there is only one
56+
# then we can remove that cell
57+
def reduce_via_logical(self, cutoff = 81):
58+
cells = self.board.get_used_cells()
59+
random.shuffle(cells)
60+
for cell in cells:
61+
if len(self.board.get_possibles(cell)) == 1:
62+
cell.value = 0
63+
cutoff -= 1
64+
if cutoff == 0:
65+
break
66+
67+
# method attempts to remove a cell and checks that solution is still unique
68+
def reduce_via_random(self, cutoff=81):
69+
temp = self.board
70+
existing = temp.get_used_cells()
71+
72+
# sorting used cells by density heuristic, highest to lowest
73+
new_set = [(x,self.board.get_density(x)) for x in existing]
74+
elements= [x[0] for x in sorted(new_set, key=lambda x: x[1], reverse=True)]
75+
76+
# for each cell in sorted list
77+
for cell in elements:
78+
original = cell.value
79+
80+
# get list of other values to try in its place
81+
complement = [x for x in range(1,10) if x != original]
82+
ambiguous = False
83+
84+
# check each value in list of other possibilities to try
85+
for x in complement:
86+
87+
# set cell to value
88+
cell.value = x
89+
90+
# create instance of solver
91+
s = Solver(temp)
92+
93+
# if solver can fill every box and the solution is valid then
94+
# puzzle becomes ambiguous after removing particular cell, so we can break out
95+
if s.solve() and s.is_valid():
96+
cell.value = original
97+
ambiguous = True
98+
break
99+
100+
# if every value was checked and puzzle remains unique, we can remove it
101+
if not ambiguous:
102+
cell.value = 0
103+
cutoff -= 1
104+
105+
# if we ever meet the cutoff limit we can break out
106+
if cutoff == 0:
107+
break
108+
109+
110+
# returns current state of generator including number of empty cells and a representation
111+
# of the puzzle
112+
def get_current_state(self):
113+
template = "There are currently %d starting cells.\n\rCurrent puzzle state:\n\r\n\r%s\n\r"
114+
return template % (len(self.board.get_used_cells()),self.board.__str__())

0 commit comments

Comments
 (0)