Skip to content

Instantly share code, notes, and snippets.

@dsasse07
Last active February 18, 2021 04:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dsasse07/f95b7c3380b14660b85c6ac2780e1b9d to your computer and use it in GitHub Desktop.
Save dsasse07/f95b7c3380b14660b85c6ac2780e1b9d to your computer and use it in GitHub Desktop.
OO Sudoku Board generator & solver - Basis for my sudoku_wizard gem
class Sudoku
attr_accessor :starting_board, :solution, :removed_values, :difficulty
BLANK_BOARD = [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]
]
def initialize(holes = 30)
holes > 64 ? 64 : holes
generate_game(holes)
end
def generate_game(holes)
begin
@iteration_counter = 0
self.solution = new_solved_board
self.removed_values, self.starting_board = poke_holes(self.solution.map(&:clone), holes)
self.difficulty = holes
rescue
generate_game(holes)
end
end
def new_solved_board
new_board = BLANK_BOARD.map(&:clone)
solve(new_board)
new_board
end
def solve (puzzle_matrix)
empty_cell = find_next_empty_cell(puzzle_matrix)
return puzzle_matrix if !empty_cell #If no empty cells, we are done. Return the completed puzzle
# Fill in the empty cell
for num in (1..9).to_a.shuffle do
raise if (@iteration_counter > 1_000_000) #Abandon current attempt and start over if recursion attempt takes too long
if safe(puzzle_matrix, empty_cell, num) # For a number, check if it safe to place that number in the empty cell
puzzle_matrix[empty_cell[:row_i]][empty_cell[:col_i]] = num # if safe, place number
return puzzle_matrix if solve(puzzle_matrix) # Recursively call solve method again.
puzzle_matrix[empty_cell[:row_i]][empty_cell[:col_i]] = 0
end
end
return false #If unable to place a number, return false, trigerring previous iteration to move to next number
end
def find_next_empty_cell(puzzle_matrix)
# Find the coordinates of the next unsolved cell
empty_cell = {row_i:"",col_i:""}
for row in puzzle_matrix do
next_zero_index = row.find_index(0)
empty_cell[:row_i] = puzzle_matrix.find_index(row)
empty_cell[:col_i] = next_zero_index
return empty_cell if empty_cell[:col_i]
end
return false
end
def safe(puzzle_matrix, empty_cell, num)
row_safe(puzzle_matrix, empty_cell, num) &&
col_safe(puzzle_matrix, empty_cell, num) &&
box_safe(puzzle_matrix, empty_cell, num)
end
def row_safe (puzzle_matrix, empty_cell, num)
!puzzle_matrix[ empty_cell[:row_i] ].find_index(num)
end
def col_safe (puzzle_matrix, empty_cell, num)
!puzzle_matrix.any?{|row| row[ empty_cell[:col_i] ] == num}
end
def box_safe (puzzle_matrix, empty_cell, num)
box_start_row = (empty_cell[:row_i] - (empty_cell[:row_i] % 3))
box_start_col = (empty_cell[:col_i] - (empty_cell[:col_i] % 3))
(0..2).to_a.each do |box_row|
(0..2).to_a.each do |box_col|
return false if puzzle_matrix[box_start_row + box_row][box_start_col + box_col] == num
end
end
return true
end
def poke_holes(puzzle_matrix, holes)
removed_values = []
while removed_values.length < holes
row_i = (0..8).to_a.sample
col_i = (0..8).to_a.sample
next if (puzzle_matrix[row_i][col_i] == 0)
removed_values.push({row_i: row_i, col_i: col_i, val: puzzle_matrix[row_i][col_i] })
puzzle_matrix[row_i][col_i] = 0
proposed_board = puzzle_matrix.map(&:clone)
puzzle_matrix[row_i][col_i] = removed_values.pop[:val] if !solve( proposed_board )
end
[removed_values, puzzle_matrix]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment