-------------------------------------------------------------------------=---
-- Name: wxluasudoku.wx.lua
-- Purpose: wxLuaSudoku - a wxLua program to generate/solve/play Sudoku puzzles
-- Author: John Labenski
-- Created: 2006
-- Copyright: (c) 2006 John Labenski. All rights reserved.
-- Licence: wxWindows licence
-------------------------------------------------------------------------=---
-- Load the wxLua module, does nothing if running from wxLua, wxLuaFreeze, or wxLuaEdit
require("wx")
-- Coding notes:
-- All non gui sudoku functions are in the "sudoku" table and all gui related
-- functions are in the "sudokuGui" table.
-- sudoku.GetXXX() retrieve precalculated or set values from the sudoku table
-- sudoku.SetXXX() set values to the sudoku table
-- sudoku.FindXXX() search the table for "stuff", but do not modify it, returns result
-- sudoku.CalcXXX() search the table for "stuff" and store the values to it for GetXXX functions
sudoku = sudoku or {} -- the table to hold the sudoku solver functions
-- ============================================================================
-- A simple function to implement "cond ? A : B", eg "result = iff(cond, A, B)"
-- note all terms must be able to be evaluated
function iff(cond, A, B) if cond then return A else return B end end
-- make the number or bool into a bool or number
function inttobool(n)
if (n == nil) or (n == 0) then return false end
return true
end
function booltoint(n)
if (n == nil) or (n == false) then return 0 end
return 1
end
-- ============================================================================
-- Simple functions to count the total number of elements in a table
-- Notes about speed : using for loop and pairs() in TableCount is 2X faster
-- than using while loop and next(table). However using next() is slightly
-- faster than using for k,v in pairs(t) do return true end in TableIsEmpty.
function TableCount(atable)
local count = 0
for k, v in pairs(atable) do count = count + 1 end
return count
end
function TableIsEmpty(atable)
return next(atable) == nil
end
-- Set a value in the table or subtable, first making sure that the subtables
-- are created first. Modifies the input table.
-- TableSetValue(3, atable, "How", "Are", 4, "You") ==> atable.How.Are[4].You = 3
function TableSetValue(value, atable, ...)
if type(atable) ~= "table" then atable = {} end
local t = atable -- t moves up levels through atable
local args = {...}
for n = 1, #args-1 do
local a = args[n]
if not t[a] then t[a] = {} end
t = t[a]
end
t[args[#args]] = value
end
-- Remove a value in the table or subtable, first making sure that the subtables
-- exist. Modifies the input table.
-- TableRemoveValue(atable, false, "How", "Are", 4, "You") ==> atable.How.Are[4].You = nil
function TableRemoveValue(atable, only_if_empty, ...)
if type(atable) ~= "table" then return end
local t = atable -- t moves up levels through atable
local args = {...}
for n = 1, #args-1 do
t = t[args[n]]
if not t then return end -- already gone
end
if (not only_if_empty) or ((type(t[args[#args]]) == "table") and TableIsEmpty(t[args[#args]])) then
t[args[#args]] = nil
end
end
-- ============================================================================
-- completely dump the contents of a table
-- atable is the input table to dump the contents of
-- prefix is a string prefix for debugging purposes
-- tablelevel is tracker for recursive calls to TableDump (do not use initially)
function TableDump(atable, prefix, tablelevel)
local function print_val(v)
local t = type(v)
if t == "number" then
return tostring(v)
elseif t == "string" then
return "\""..v.."\""
end
return "'"..tostring(v).."'"
end
prefix = prefix or ""
if tablelevel == nil then
tablelevel = ""
print(prefix.."-Dumping Table "..tostring(atable))
end
prefix = prefix.." "
local n = 0
for k, v in pairs(atable) do
n = n + 1
print(string.format("%s %d: %s[%s] = %s", prefix, n, tablelevel, print_val(k), print_val(v)))
if type(v) == "table" then
TableDump(v, prefix.." ", tablelevel.."["..print_val(k).."]")
end
end
end
-- ============================================================================
-- Make a deep copy of a table, including all sub tables, fails on recursive tables.
-- returns a new table
function TableCopy(atable)
if not atable then return nil end
local newtable = {}
for k, v in pairs(atable) do
if type(v) == "table" then
newtable[k] = TableCopy(v)
else
newtable[k] = v
end
end
return newtable
end
-- Merge the two tables together, adding or replacing values in original_table
-- with those in new_table, returns a new table and doesn't modify inputs
-- fails on recursive tables, returns the new table
function TableMerge(new_table, original_table)
new_table = new_table or {}
local out_table = TableCopy(original_table or {})
for k, v in pairs(new_table) do
if type(v) == "table" then
if out_table[k] and (type(out_table[k]) == "table") then
out_table[k] = TableMerge(v, out_table[k])
elseif out_table[k] then
local ov = out_table[k]
out_table[k] = TableCopy(v)
table.insert(out_table[k], ov)
else
out_table[k] = TableCopy(v)
end
elseif out_table[k] and (type(out_table[k]) == "table") then
table.insert(out_table[k], v)
else
out_table[k] = v
end
end
return out_table
end
-- ============================================================================
-- Flags for the sudokuTable.flags[flag] = true/false/nil
sudoku.ELIMINATE_NAKED_PAIRS = 1 -- for sudoku.CalcAllPossible(sudokuTable)
sudoku.ELIMINATE_HIDDEN_PAIRS = 2 -- set to true to have it run
sudoku.ELIMINATE_NAKED_TRIPLETS = 3 -- for sudoku.CalcAllPossible(sudokuTable)
sudoku.ELIMINATE_HIDDEN_TRIPLETS = 4 -- set to true to have it run
sudoku.ELIMINATE_NAKED_QUADS = 5 -- for sudoku.CalcAllPossible(sudokuTable)
sudoku.ELIMINATE_HIDDEN_QUADS = 6 -- set to true to have it run
sudoku.FILENAME = 7 -- store the fileName from Open/Save functions
sudoku.ELIMINATE_FLAG_MIN = 1 -- for iterating the ELIMINATE flags
sudoku.ELIMINATE_FLAG_MAX = 6
-- given the upper left cell of block you can iterate through the block using
-- for n = 1, 9 do cell = n + block_cell + sudoku.LinearBlockCellTable[n] ... end
sudoku.LinearBlockCellTable = { -1, -1, -1, 5, 5, 5, 11, 11, 11 }
sudoku.CellToRowTable = {}
sudoku.CellToColTable = {}
sudoku.BlockToRowTable = {}
sudoku.BlockToColTable = {}
sudoku.CellToBlockTable = {}
sudoku.BlockToCellTable = {}
for cell = 1, 81 do
local row = math.floor(cell/9.1)+1
local col = cell-(row-1)*9
sudoku.CellToRowTable[cell] = row
sudoku.CellToColTable[cell] = col
local block_row = math.floor(row/3.5)+1
local block_col = math.floor(col/3.5)+1
sudoku.CellToBlockTable[cell] = (block_row-1)*3 + block_col
end
for block = 1, 9 do
local row = math.floor(block/3.5)*3+1
local col = math.fmod(block-1,3)*3+1
sudoku.BlockToRowTable[block] = row
sudoku.BlockToColTable[block] = col
sudoku.BlockToCellTable[block] = (row-1)*9 + col
end
-- ============================================================================
-- Create a sudoku table to be used with the rest of the sudoku functions
function sudoku.CreateTable()
local sudokuTable =
{
values = {}, -- array (1-81) of values[cell#] = value (0 means unset)
row_values = {}, -- array (1-9) of values[row#][value] = { cell1, cell2... }
col_values = {}, -- array (1-9) of values[col#][value] = { cell1, cell2... }
block_values = {}, -- array (1-9) of values[block#][value] = { cell1, cell2... }
possible = {}, -- possible values per cell, possible[cell# 1-81] = { val1, val2... }
invalid = {}, -- array (1-81) of known invalid[cell#] = true/nil
flags = {} -- extra flags for puzzle, eg. ELIMINATE_NAKED_PAIRS
}
for i = 1, 81 do
sudokuTable.values[i] = 0 -- initialize to unknown
sudokuTable.possible[i] = {} -- initialize to empty
end
for i = 1, 9 do
sudokuTable.row_values[i] = {}
sudokuTable.col_values[i] = {}
sudokuTable.block_values[i] = {}
end
sudoku.UpdateTable(sudokuTable)
return sudokuTable
end
-- Update all the values in the table using only the cell values, modifies input sudokuTable.
function sudoku.UpdateTable(sudokuTable)
sudoku.CalcRowColBlockValues(sudokuTable)
sudoku.CalcInvalidCells(sudokuTable)
sudoku.CalcAllPossible(sudokuTable)
end
-- Set the values table in the sudokuTable and update everything, modifies input sudokuTable.
function sudoku.SetValues(sudokuTable, values)
sudokuTable.values = values
sudoku.UpdateTable(sudokuTable)
end
-- Open a sudoku table from a file, the file should be formatted as 9x9 numbers
-- with 9 numbers per row and 9 columns.
-- returns a sudoku.CreateTable() with the values set and "" on success
-- or nil, error_message on failure
function sudoku.Open(fileName)
local values = {}
local value_count = 0 -- number of cols in line
local row_count = 0 -- number of rows read
local line_n = 0 -- actual line number in file
for line in io.lines(fileName) do
line_n = line_n + 1
local col_count = 0
for k, v in string.gmatch(line, "%d") do
k = tonumber(k)
if (k >= 0) and (k <= 9) then
table.insert(values, k)
col_count = col_count + 1
value_count = value_count + 1
else
return nil, string.format("Error loading sudoku file : '%s' invalid number '%d' on line %d.", fileName, k, line_n)
end
end
if col_count == 9 then
row_count = row_count + 1
elseif (col_count ~= 0) and (col_count ~= 9) then
return nil, string.format("Error loading sudoku file : '%s' on line %d.\nExpecting 9 columns, found %d.", fileName, line_n, col_count)
end
end
if line_n == 0 then
return nil, string.format("Error opening sudoku file : '%s'.", fileName)
elseif row_count ~= 9 then
return nil, string.format("Error loading sudoku file : '%s', expected 9 rows, found %d.", fileName, row_count)
elseif value_count ~= 81 then
return nil, string.format("Error loading sudoku file : '%s', expected 81 numbers, found %d.", fileName, value_count)
end
local s = sudoku.CreateTable()
s.flags[sudoku.FILENAME] = fileName
sudoku.SetValues(s, values)
return s, ""
end
-- Save a sudoku grid as a 9x9 comma separated table to a file, returns success
function sudoku.Save(sudokuTable, fileName)
local f = io.open(fileName, "w+")
if not f then return false end
local str = sudoku.ToString(sudokuTable)
f:write(str)
io.close(f)
sudokuTable.flags[sudoku.FILENAME] = fileName
return true
end
-- ============================================================================
-- Neatly print the sudoku grid
function sudoku.PrintGrid(sudokuTable)
local str = string.rep("-", 13).."\n"
for r = 1, 9 do
str = str.."|"
for c = 1, 9 do
local v = " "
if sudoku.HasValue(sudokuTable, r, c) then
v = tostring(sudoku.GetValue(sudokuTable, r, c))
end
str = str..v
if math.fmod(c, 3) == 0 then str = str.."|" end
end
str = str.."\n"
if math.fmod(r, 3) == 0 then str = str..string.rep("-", 13).."\n" end
end
print(str)
end
-- Write the grid itself to a string as 9x9 with a space/line separating blocks
function sudoku.ToString(sudokuTable)
local str = ""
for r = 1, 9 do
for c = 1, 9 do
local v = "0"
if sudoku.HasValue(sudokuTable, r, c) then
v = tostring(sudoku.GetValue(sudokuTable, r, c))
end
str = str..v..","
if math.fmod(c, 3) == 0 then str = str.." " end
end
str = str.."\n"
if (r < 9) and (math.fmod(r, 3) == 0) then str = str.."\n" end
end
return str
end
-- Neatly print the possible values for each cell (you must calculate it first)
function sudoku.PrintPossible(sudokuTable)
local str = string.rep("-", 103).."\n"
for r = 1, 9 do
str = str.."|"
for c = 1, 9 do
local has_value = sudoku.HasValue(sudokuTable, r, c)
if has_value then str = str.."<" else str = str.."[" end
local p = sudoku.GetPossible(sudokuTable, r, c)
for i = 1, 9 do
str = str..(p[i] or " ")
end
if has_value then str = str..">" else str = str.."]" end
if math.fmod(c, 3) == 0 then str = str.."|" end
end
str = str.."\n"
if math.fmod(r, 3) == 0 then str = str..string.rep("-", 103).."\n" end
end
print(str)
end
-- ============================================================================
-- Convert a row, col cell index (1-9) into a linear position in the grid (1-81)
function sudoku.RowColToCell(row, col)
return (row-1)*9 + col
end
-- Convert a linear cell index (1-81) into a row, col cell index (1-9)
function sudoku.CellToRowCol(cell)
return sudoku.CellToRowTable[cell], sudoku.CellToColTable[cell]
end
function sudoku.CellToRow(cell)
return sudoku.CellToRowTable[cell]
end
function sudoku.CellToCol(cell)
return sudoku.CellToColTable[cell]
end
-- ============================================================================
-- Check the validity of rows, cols, cells, blocks, values
function sudoku.IsValidValueN(value)
return (value >= 1) and (value <= 9)
end
-- ============================================================================
-- Convert a row, col cell index (1-9) into the linear block number (1-9)
function sudoku.RowColToBlock(row, col)
return sudoku.CellToBlockTable[sudoku.RowColToCell(row, col)]
end
-- Get the block (1-9) that this cell (1-81) is in
function sudoku.CellToBlock(cell)
return sudoku.CellToBlockTable[cell]
end
-- Get the upper left cell of this block
function sudoku.BlockToCell(block)
return sudoku.BlockToCellTable[block]
end
-- Convert a linear block index (1-9) into upper left row, col cell index (1-9)
function sudoku.BlockToRowCol(block)
return sudoku.BlockToRowTable[block], sudoku.BlockToColTable[block]
end
function sudoku.BlockToRow(block)
return sudoku.BlockToRowTable[block]
end
function sudoku.BlockToCol(block)
return sudoku.BlockToColTable[block]
end
-- Get the upper left row, col cell of the block given by row, col
function sudoku.RowColToBlockRowCol(row, col)
local block = sudoku.RowColToBlock(row, col)
return sudoku.BlockToRowTable[block], sudoku.BlockToColTable[block]
end
-- Generate a table of {[cell] = {hash table of cells that are in the row, col, block of this cell}}
sudoku.cellToRowColBlockCellsTable = {}
for cell = 1, 81 do
local row, col = sudoku.CellToRowCol(cell)
local block_cell = sudoku.BlockToCell(sudoku.CellToBlock(cell))
sudoku.cellToRowColBlockCellsTable[cell] = {}
for rcb = 1, 9 do
local c = sudoku.RowColToCell(rcb, col)
sudoku.cellToRowColBlockCellsTable[cell][c] = true
c = sudoku.RowColToCell(row, rcb)
sudoku.cellToRowColBlockCellsTable[cell][c] = true
c = rcb + block_cell + sudoku.LinearBlockCellTable[rcb]
sudoku.cellToRowColBlockCellsTable[cell][c] = true
end
end
-- Generate a table of {[cell] = {array of cells that are in the row, col, block of this cell}}
sudoku.cellToRowColBlockCellsArray = {}
for cell = 1, 81 do
sudoku.cellToRowColBlockCellsArray[cell] = {}
for k, v in pairs(sudoku.cellToRowColBlockCellsTable[cell]) do
table.insert(sudoku.cellToRowColBlockCellsArray[cell], k)
end
end
sudoku.RowCellTable = {}
sudoku.ColCellTable = {}
sudoku.BlockCellTable = {}
sudoku.BlockCellShiftTable = {0, 3, 6, 27, 30, 33, 54, 57, 60}
for n = 1, 9 do
local nn = (n-1)*9
sudoku.RowCellTable[n] = {1+nn, 2+nn, 3+nn, 4+nn, 5+nn, 6+nn, 7+nn, 8+nn, 9+nn}
nn = n - 1
sudoku.ColCellTable[n] = {1+nn, 10+nn, 19+nn, 28+nn, 37+nn, 46+nn, 55+nn, 64+nn, 73+nn}
nn = sudoku.BlockCellShiftTable[n]
sudoku.BlockCellTable[n] = {1+nn, 2+nn, 3+nn, 10+nn, 11+nn, 12+nn, 19+nn, 20+nn, 21+nn}
end
-- ============================================================================
-- Get the cell value at a specific row, col
function sudoku.GetValue(sudokuTable, row, col)
return sudoku.GetCellValue(sudokuTable, sudoku.RowColToCell(row, col))
end
-- Set the cell value at a specific row, col, modifies input sudokuTable.
function sudoku.SetValue(sudokuTable, row, col, value)
local cell = sudoku.RowColToCell(row, col)
local block = sudoku.CellToBlock(cell)
local old_value = sudokuTable.values[cell]
if not sudoku.IsValidValueN(value) then value = 0 end
sudokuTable.values[cell] = value
--remove the old_value from the row, col, block values
if sudoku.IsValidValueN(old_value) then
if sudokuTable.row_values[row] and sudokuTable.row_values[row][old_value] then
sudokuTable.row_values[row][old_value][cell] = nil
if TableIsEmpty(sudokuTable.row_values[row][old_value]) then sudokuTable.row_values[row][old_value] = nil end
end
if sudokuTable.col_values[col] and sudokuTable.col_values[col][old_value] then
sudokuTable.col_values[col][old_value][cell] = nil
if TableIsEmpty(sudokuTable.col_values[col][old_value]) then sudokuTable.col_values[col][old_value] = nil end
end
if sudokuTable.block_values[block] and sudokuTable.block_values[block][old_value] then
sudokuTable.block_values[block][old_value][cell] = nil
if TableIsEmpty(sudokuTable.block_values[block][old_value]) then sudokuTable.block_values[block][old_value] = nil end
end
end
--add new value to the row, col, block values
if value ~= 0 then
if not sudokuTable.row_values[row] then
sudokuTable.row_values[row] = {[value] = {[cell] = cell}}
elseif not sudokuTable.row_values[row][value] then
sudokuTable.row_values[row][value] = {[cell] = cell}
else
sudokuTable.row_values[row][value][cell] = cell
end
if not sudokuTable.col_values[col] then
sudokuTable.col_values[col] = {[value] = {[cell] = cell}}
elseif not sudokuTable.col_values[col][value] then
sudokuTable.col_values[col][value] = {[cell] = cell}
else
sudokuTable.col_values[col][value][cell] = cell
end
if not sudokuTable.block_values[block] then
sudokuTable.block_values[block] = {[value] = {[cell] = cell}}
elseif not sudokuTable.block_values[block][value] then
sudokuTable.block_values[block][value] = {[cell] = cell}
else
sudokuTable.block_values[block][value][cell] = cell
end
end
end
-- Does the cell have a value at a specific row, col
function sudoku.HasValue(sudokuTable, row, col)
return sudoku.HasCellValue(sudokuTable, sudoku.RowColToCell(row, col))
end
-- Set the cell value at a specific cell
function sudoku.GetCellValue(sudokuTable, cell)
return sudokuTable.values[cell]
end
-- Set the cell value at a specific cell, modifies input sudokuTable.
function sudoku.SetCellValue(sudokuTable, cell, value)
local row, col = sudoku.CellToRowCol(cell)
sudoku.SetValue(sudokuTable, row, col, value)
end
-- Does the cell have a value at a specific cell
function sudoku.HasCellValue(sudokuTable, cell)
return sudoku.IsValidValueN(sudokuTable.values[cell])
end
-- ============================================================================
-- Set the row_values, col_values, block_values tables of the input sudokuTable
-- eg. row values table is row_values[row#][value][cell#] = cell#
-- if no value then row_values[row#][value] = nil
function sudoku.CalcRowColBlockValues(sudokuTable)
sudokuTable.row_values = {}
sudokuTable.col_values = {}
sudokuTable.block_values = {}
for cell = 1, 81 do
local row, col = sudoku.CellToRowCol(cell)
local block = sudoku.CellToBlock(cell)
if not sudokuTable.row_values[row] then sudokuTable.row_values[row] = {} end
if not sudokuTable.col_values[col] then sudokuTable.col_values[col] = {} end
if not sudokuTable.block_values[block] then sudokuTable.block_values[block] = {} end
local value = sudoku.GetCellValue(sudokuTable, cell)
if sudoku.IsValidValueN(value) then
if not sudokuTable.row_values[row][value] then
sudokuTable.row_values[row][value] = {[cell] = cell}
else
sudokuTable.row_values[row][value][cell] = cell
end
if not sudokuTable.col_values[col][value] then
sudokuTable.col_values[col][value] = {[cell] = cell}
else
sudokuTable.col_values[col][value][cell] = cell
end
if not sudokuTable.block_values[block][value] then
sudokuTable.block_values[block][value] = {[cell] = cell}
else
sudokuTable.block_values[block][value][cell] = cell
end
end
end
end
-- ============================================================================
-- Can this value be put into this cell given the other existing values?
function sudoku.IsValidValue(sudokuTable, row, col, value)
if sudokuTable.row_values[row][value] or
sudokuTable.col_values[col][value] or
sudokuTable.block_values[sudoku.RowColToBlock(row, col)][value] then
return false
end
return true
end
-- Find all the invalid cells by looking for duplicates, modifies input sudokuTable.
-- fills sudokuTable.invalid table with the values
function sudoku.CalcInvalidCells(sudokuTable)
sudokuTable.invalid = {} -- reset to all good
for n = 1, 9 do
for i, cell_table in pairs(sudokuTable.row_values[n]) do
if TableCount(cell_table) > 1 then
for j, cell in pairs(cell_table) do
sudokuTable.invalid[cell] = true
end
end
end
for i, cell_table in pairs(sudokuTable.col_values[n]) do
if TableCount(cell_table) > 1 then
for j, cell in pairs(cell_table) do
sudokuTable.invalid[cell] = true
end
end
end
for i, cell_table in pairs(sudokuTable.block_values[n]) do
if TableCount(cell_table) > 1 then
for j, cell in pairs(cell_table) do
sudokuTable.invalid[cell] = true
end
end
end
end
end
-- ============================================================================
-- Get the possible values at a specific row, col cell
-- Must be previously set from sudoku.CalcAllPossible
function sudoku.GetPossible(sudokuTable, row, col)
return sudokuTable.possible[sudoku.RowColToCell(row, col)]
end
function sudoku.GetCellPossible(sudokuTable, cell)
return sudokuTable.possible[cell]
end
-- Set the possible values at a specific row, col cell. Modifies input sudokuTable.
function sudoku.SetPossible(sudokuTable, row, col, possibleTable)
sudokuTable.possible[sudoku.RowColToCell(row, col)] = possibleTable
end
function sudoku.SetCellPossible(sudokuTable, cell, possibleTable)
sudokuTable.possible[cell] = possibleTable
end
-- Remove a possible value at a specific row, col cell only. Modifies input sudokuTable.
function sudoku.RemovePossible(sudokuTable, row, col, value)
return sudoku.RemoveCellPossible(sudokuTable, sudoku.RowColToCell(row, col), value)
end
function sudoku.RemoveCellPossible(sudokuTable, cell, value)
sudokuTable.possible[cell][value] = nil
end
-- Remove a possible values from the row, col, block. Modifies input sudokuTable.
-- if exceptTable then don't remove it from exceptTable[cell#] = true
function sudoku.RemovePossibleAll(sudokuTable, cell, value, exceptTable, break_if_empty)
exceptTable = exceptTable or {}
break_if_empty = break_if_empty or false
for i, c in ipairs(sudoku.cellToRowColBlockCellsArray[cell]) do
if (not exceptTable[c]) and sudokuTable.possible[c][value] then
sudokuTable.possible[c][value] = nil
if break_if_empty and (not sudoku.HasCellValue(sudokuTable, c)) and TableIsEmpty(sudokuTable.possible[c]) then
return
end
end
end
end
-- Remove a possible values from the row. Modifies input sudokuTable.
-- if exceptTable then don't remove it from exceptTable[cell#] = true
function sudoku.RemovePossibleRow(sudokuTable, row, value, exceptTable)
exceptTable = exceptTable or {}
for col = 1, 9 do
local cell = sudoku.RowColToCell(row, col)
if (not exceptTable[cell]) and sudokuTable.possible[cell][value] then
sudokuTable.possible[cell][value] = nil
end
end
end
-- Remove a possible values from the col. Modifies input sudokuTable.
-- if exceptTable then don't remove it from exceptTable[cell#] = true
function sudoku.RemovePossibleCol(sudokuTable, col, value, exceptTable)
exceptTable = exceptTable or {}
for row = 1, 9 do
local cell = sudoku.RowColToCell(row, col)
if (not exceptTable[cell]) and sudokuTable.possible[cell][value] then
sudokuTable.possible[cell][value] = nil
end
end
end
-- Remove a possible values from the block. Modifies input sudokuTable.
-- if exceptTable then don't remove it from exceptTable[cell#] = true
function sudoku.RemovePossibleBlock(sudokuTable, block, value, exceptTable)
exceptTable = exceptTable or {}
local block_cell = sudoku.BlockToCell(block)
for n = 1, 9 do
local cell = n + block_cell + sudoku.LinearBlockCellTable[n]
if (not exceptTable[cell]) and sudokuTable.possible[cell][value] then
sudokuTable.possible[cell][value] = nil
end
end
end
-- Get the count of all possible values for rows, cols, and blocks
-- returns 3 tables row_possible[row#][value] = #times possible value occurs in row
-- and the same for col_possible, block_possible
-- if no possible values (all values set) then row_possible[row#] = nil
function sudoku.FindPossibleCountRowColBlock(sudokuTable)
local row_possible = {}
local col_possible = {}
local block_possible = {}
for cell = 1, 81 do
local row, col = sudoku.CellToRowCol(cell)
local block = sudoku.CellToBlock(cell)
local cell_possible = sudoku.GetCellPossible(sudokuTable, cell)
for pvalue, is_possible in pairs(cell_possible) do
if not row_possible[row] then row_possible[row] = {} end
if not col_possible[col] then col_possible[col] = {} end
if not block_possible[block] then block_possible[block] = {} end
row_possible[row][pvalue] = (row_possible[row][pvalue] or 0) + 1
col_possible[col][pvalue] = (col_possible[col][pvalue] or 0) + 1
block_possible[block][pvalue] = (block_possible[block][pvalue] or 0) + 1
end
end
return row_possible, col_possible, block_possible
end
-- Find all the possible values for row, col cell
-- returns a table of possible[value] = true
function sudoku.FindPossibleCell(sudokuTable, row, col)
local possible = {}
-- gather up all the set values in row, col, and block
local rowValues = sudokuTable.row_values[row]
local colValues = sudokuTable.col_values[col]
local blockValues = sudokuTable.block_values[sudoku.RowColToBlock(row, col)]
-- remove the set values from the possible values
for v = 1, 9 do
if (rowValues[v] == nil) and (colValues[v] == nil) and (blockValues[v] == nil) then
possible[v] = v
end
end
return possible
end
-- Find all the possible values for the whole table by filling out the
-- possible table in the input sudokuTable. Modifies input sudokuTable.
function sudoku.CalcAllPossible(sudokuTable)
for cell = 1, 81 do
local row, col = sudoku.CellToRowCol(cell)
local possible = {}
if not sudoku.HasCellValue(sudokuTable, cell) then
local block = sudoku.CellToBlock(cell)
for v = 1, 9 do
if (sudokuTable.row_values[row][v] == nil) and
(sudokuTable.col_values[col][v] == nil) and
(sudokuTable.block_values[block][v] == nil) then
possible[v] = v
end
end
end
sudoku.SetCellPossible(sudokuTable, cell, possible)
end
-- this function checks flags to see if it should run
sudoku.RemovePossibleGroups(sudokuTable)
end
-- Find all the possible pairs, triplets, quads in the table
-- must run CalcAllPossible first, does not eliminate any.
-- returns 3 tables, possible_pairs.rows[row#][key] = { cell1, cell2... },
-- possible_pairs.cols[col#][key] = { cell1, cell2... },
-- possible_pairs.blocks[block#][key] = { cell1, cell2... },
-- and the same for possible_triplets, possible_quads
-- key is constructed from the number group as string.char(val1, val2...)
sudoku.FindAllPossibleGroups_Cache = {}
function sudoku.FindAllPossibleGroups(sudokuTable)
local possible_pairs = {rows = {}, cols = {}, blocks = {}}
local possible_triplets = {rows = {}, cols = {}, blocks = {}}
local possible_quads = {rows = {}, cols = {}, blocks = {}}
local char0 = string.byte("0")
local cache_key_flags = 1*booltoint(sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_PAIRS]) +
2*booltoint(sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS]) +
4*booltoint(sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_QUADS])
local cache_keys = { 10^1, 10^2, 10^3, 10^4, 10^5, 10^6, 10^7, 10^8, 10^9 }
local function add_possible(atable, rcb_key, key, cell)
local a = atable[rcb_key]
if not a then
atable[rcb_key] = { [key] = {cell} }
elseif not a[key] then
a[key] = {cell}
else
a[key][#a[key]+1] = cell
end
end
for cell = 1, 81 do
local row, col = sudoku.CellToRowCol(cell)
local block = sudoku.CellToBlock(cell)
local cell_possible = sudoku.GetCellPossible(sudokuTable, cell)
local cell_possible_table = {}
local cache_key = cache_key_flags
-- convert key, value table to indexed table and a key for the cache
local count = 0
for n = 1, 9 do
if cell_possible[n] then
cell_possible_table[#cell_possible_table+1] = char0+n
cache_key = cache_key + cache_keys[n]
count = count + 1
end
end
local possible_pairs_keys = {}
local possible_triplets_keys = {}
local possible_quads_keys = {}
-- either use the cached key table or create a new key table for the possible
-- Note: cache cuts time for 100 calls to this fn w/ empty puzzle from 8 to 1 sec
if (count > 1) and sudoku.FindAllPossibleGroups_Cache[cache_key] then
possible_pairs_keys = sudoku.FindAllPossibleGroups_Cache[cache_key].possible_pairs
possible_triplets_keys = sudoku.FindAllPossibleGroups_Cache[cache_key].possible_triplets
possible_quads_keys = sudoku.FindAllPossibleGroups_Cache[cache_key].possible_quads
elseif (count > 1) then
local elim_pairs = (count == 2) or sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_PAIRS]
local elim_triplets = (count == 3) or sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS]
local elim_quads = (count == 4) or sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_QUADS]
for i = 1, count do
for j = i+1, count do
local pkey = string.char(cell_possible_table[i], cell_possible_table[j])
if elim_pairs then
possible_pairs_keys[#possible_pairs_keys+1] = pkey
end
for k = j+1, count do
local tkey = pkey..string.char(cell_possible_table[k])
if elim_triplets then
possible_triplets_keys[#possible_triplets_keys+1] = tkey
end
if elim_quads then
for l = k+1, count do
local qkey = tkey..string.char(cell_possible_table[l])
possible_quads_keys[#possible_quads_keys+1] = qkey
end
end
end
end
end
sudoku.FindAllPossibleGroups_Cache[cache_key] = {}
sudoku.FindAllPossibleGroups_Cache[cache_key].possible_pairs = possible_pairs_keys
sudoku.FindAllPossibleGroups_Cache[cache_key].possible_triplets = possible_triplets_keys
sudoku.FindAllPossibleGroups_Cache[cache_key].possible_quads = possible_quads_keys
end
for k, key in pairs(possible_pairs_keys) do
add_possible(possible_pairs.rows, row, key, cell)
add_possible(possible_pairs.cols, col, key, cell)
add_possible(possible_pairs.blocks, block, key, cell)
end
for k, key in pairs(possible_triplets_keys) do
add_possible(possible_triplets.rows, row, key, cell)
add_possible(possible_triplets.cols, col, key, cell)
add_possible(possible_triplets.blocks, block, key, cell)
end
for k, key in pairs(possible_quads_keys) do
add_possible(possible_quads.rows, row, key, cell)
add_possible(possible_quads.cols, col, key, cell)
add_possible(possible_quads.blocks, block, key, cell)
end
end
return possible_pairs, possible_triplets, possible_quads
end
-- Find all the naked and hidden pairs, triplets, quads in the table
-- must run CalcAllPossible first, does not eliminate any.
-- returns 2 tables, naked.rows[row#][key] = { cell1, cell2... },
-- naked.cols[col#][key] = { cell1, cell2... },
-- naked.blocks[block#][key] = { cell1, cell2... },
-- and the same for hidden
-- key is constructed from the number group as string.char(val1, val2...)
function sudoku.FindAllNakedHiddenGroups(sudokuTable, find_all)
local flags = sudokuTable.flags
if find_all == true then
sudokuTable.flags = TableCopy(flags) -- unref the table
-- turn all ELIMINATE_XXX on
for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do
sudokuTable.flags[n] = true
end
end
local row_possible, col_possible, block_possible = sudoku.FindPossibleCountRowColBlock(sudokuTable)
local possible_pairs, possible_triplets, possible_quads = sudoku.FindAllPossibleGroups(sudokuTable)
local char0 = string.byte("0")
local all_groups = { [2] = 36, [3] = 84, [4] = 126 } -- eg. 9!/(2! * (9-2)!)
if find_all == true then
sudokuTable.flags = flags -- put the flags back to how they were
end
local naked =
{
pairs = {rows = {}, cols = {}, blocks = {}, cells = {}},
triplets = {rows = {}, cols = {}, blocks = {}, cells = {}},
quads = {rows = {}, cols = {}, blocks = {}, cells = {}}
}
local hidden =
{
pairs = {rows = {}, cols = {}, blocks = {}, cells = {}},
triplets = {rows = {}, cols = {}, blocks = {}, cells = {}},
quads = {rows = {}, cols = {}, blocks = {}, cells = {}}
}
-- cache all the cell possible value counts
local cell_possible_count = {}
for n = 1, 81 do
cell_possible_count[n] = TableCount(sudoku.GetCellPossible(sudokuTable, n) or {})
end
local function dofind(rcb_table, num, key, cell_table_, rcb, rcb_possible)
local naked_cell_table = {}
local naked_cell_count = 0
local hidden_cell_table = {}
local hidden_cell_count = 0
local is_hidden = true
-- can only be exactly as many nums in key as in rcb for hidden
for n = 1, num do
if rcb_possible[string.byte(key, n)-char0] ~= num then
is_hidden = false
break
end
end
for n, cell in ipairs(cell_table_) do
if cell_possible_count[cell] == num then
naked_cell_table[#naked_cell_table+1] = cell
naked_cell_count = naked_cell_count + 1
end
if is_hidden then
hidden_cell_table[#hidden_cell_table+1] = cell
hidden_cell_count = hidden_cell_count + 1
end
end
-- has to be at least the same cell_count as num, if more then error, but...
if (naked_cell_count >= num) then
if not rcb_table.naked_table[rcb] then rcb_table.naked_table[rcb] = {} end
rcb_table.naked_table[rcb][key] = naked_cell_table
local cell_table = rcb_table.naked_table_base.cells
for n, cell in pairs(naked_cell_table) do
if not cell_table[cell] then cell_table[cell] = {} end
table.insert(cell_table[cell], key)
end
end
-- has to be at least the same cell_count as num, if more then error, but...
if is_hidden and (hidden_cell_count >= num) then
if not rcb_table.hidden_table[rcb] then rcb_table.hidden_table[rcb] = {} end
rcb_table.hidden_table[rcb][key] = hidden_cell_table
local cell_table = rcb_table.hidden_table_base.cells
for n, cell in pairs(hidden_cell_table) do
if not cell_table[cell] then cell_table[cell] = {} end
table.insert(cell_table[cell], key)
end
end
end
local function find(naked_table, hidden_table, possible_table, num)
local rcb_table = {}
rcb_table.naked_table_base = naked_table
rcb_table.hidden_table_base = hidden_table
rcb_table.naked_table = naked_table.rows
rcb_table.hidden_table = hidden_table.rows
for row, key_table in pairs(possible_table.rows) do
for key, cell_table in pairs(key_table) do
dofind(rcb_table, num, key, cell_table, row, row_possible[row])
end
end
rcb_table.naked_table = naked_table.cols
rcb_table.hidden_table = hidden_table.cols
for col, key_table in pairs(possible_table.cols) do
for key, cell_table in pairs(key_table) do
dofind(rcb_table, num, key, cell_table, col, col_possible[col])
end
end
rcb_table.naked_table = naked_table.blocks
rcb_table.hidden_table = hidden_table.blocks
for block, key_table in pairs(possible_table.blocks) do
for key, cell_table in pairs(key_table) do
dofind(rcb_table, num, key, cell_table, block, block_possible[block])
end
end
return naked_table, hidden_table
end
naked.pairs, hidden.pairs = find(naked.pairs, hidden.pairs, possible_pairs, 2)
naked.triplets, hidden.triplets = find(naked.triplets, hidden.triplets, possible_triplets, 3)
naked.quads, hidden.quads = find(naked.quads, hidden.quads, possible_quads, 4)
return naked, hidden
end
-- ============================================================================
-- Find all pairs, triplets, quads of values in rows and reset the possible
-- values for the row to exclude these values. Modifies input sudokuTable.
function sudoku.RemovePossibleGroups(sudokuTable)
-- must have at least one flag set
local has_elim_flags = false
for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do
if sudokuTable.flags[n] == true then
has_elim_flags = true
break
end
end
if has_elim_flags == false then
return
end
local naked, hidden = sudoku.FindAllNakedHiddenGroups(sudokuTable, false)
local char0 = string.byte("0")
local function clear_possible(group_table, num, remove_fn)
for n = 1, 9 do
if group_table[n] then
for key, cell_table in pairs(group_table[n]) do
local exceptTable = {}
for k, v in pairs(cell_table) do
exceptTable[v] = v
end
for k = 1, num do
local val = string.byte(key, k)-char0
remove_fn(sudokuTable, n, val, exceptTable)
end
end
end
end
end
if (sudokuTable.flags[sudoku.ELIMINATE_NAKED_PAIRS] == true) then
clear_possible(naked.pairs.rows, 2, sudoku.RemovePossibleRow)
clear_possible(naked.pairs.cols, 2, sudoku.RemovePossibleCol)
clear_possible(naked.pairs.blocks, 2, sudoku.RemovePossibleBlock)
end
if (sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] == true) then
clear_possible(hidden.pairs.rows, 2, sudoku.RemovePossibleRow)
clear_possible(hidden.pairs.cols, 2, sudoku.RemovePossibleCol)
clear_possible(hidden.pairs.blocks, 2, sudoku.RemovePossibleBlock)
end
if (sudokuTable.flags[sudoku.ELIMINATE_NAKED_TRIPLETS] == true) then
clear_possible(naked.triplets.rows, 3, sudoku.RemovePossibleRow)
clear_possible(naked.triplets.cols, 3, sudoku.RemovePossibleCol)
clear_possible(naked.triplets.blocks, 3, sudoku.RemovePossibleBlock)
end
if (sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] == true) then
clear_possible(hidden.triplets.rows, 3, sudoku.RemovePossibleRow)
clear_possible(hidden.triplets.cols, 3, sudoku.RemovePossibleCol)
clear_possible(hidden.triplets.blocks, 3, sudoku.RemovePossibleBlock)
end
if (sudokuTable.flags[sudoku.ELIMINATE_NAKED_QUADS] == true) then
clear_possible(naked.quads.rows, 4, sudoku.RemovePossibleRow)
clear_possible(naked.quads.cols, 4, sudoku.RemovePossibleCol)
clear_possible(naked.quads.blocks, 4, sudoku.RemovePossibleBlock)
end
if (sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_QUADS] == true) then
clear_possible(hidden.quads.rows, 4, sudoku.RemovePossibleRow)
clear_possible(hidden.quads.cols, 4, sudoku.RemovePossibleCol)
clear_possible(hidden.quads.blocks, 4, sudoku.RemovePossibleBlock)
end
end
-- ============================================================================
-- Find all the cells that only have a single possible value and set it.
-- Modifies input sudokuTable.
function sudoku.SolveScanSingles(sudokuTable)
sudoku.CalcAllPossible(sudokuTable)
local changed_cells = {}
for row = 1, 9 do
for col = 1, 9 do
if not sudoku.HasValue(sudokuTable, row, col) then
local possible = sudoku.GetPossible(sudokuTable, row, col)
local count = 0
local value = nil
for pvalue, is_possible in pairs(possible) do -- count possible values
count = count + 1
value = pvalue
end
if count == 1 then
local cell = sudoku.RowColToCell(row, col)
sudoku.SetValue(sudokuTable, row, col, value)
sudoku.RemovePossibleAll(sudokuTable, cell, value)
changed_cells[cell] = value
end
end
end
end
if TableIsEmpty(changed_cells) then changed_cells = nil end -- reset if not used
return changed_cells
end
-- Find all the cells that only have a single possible value per row and set it.
-- Modifies input sudokuTable.
function sudoku.SolveScanRows(sudokuTable)
sudoku.SolveScanRowsCols(sudokuTable, true)
end
-- Find all the cells that only have a single possible value per col and set it
-- Modifies input sudokuTable.
function sudoku.SolveScanCols(sudokuTable)
sudoku.SolveScanRowsCols(sudokuTable, false)
end
function sudoku.SolveScanRowsCols(sudokuTable, scan_rows)
sudoku.CalcAllPossible(sudokuTable)
local changed_cells = {}
for row_col1 = 1, 9 do
local row = nil -- set row or col depending on scan_rows
local col = nil
if scan_rows then row = row_col1 else col = row_col1 end
local possible = {} -- all the possible values along the row or col
for i = 1, 9 do possible[i] = {} end
-- fill possible[pvalue] = { cell1, cell2... } along row or col
for row_col2 = 1, 9 do
if scan_rows then col = row_col2 else row = row_col2 end
if not sudoku.HasValue(sudokuTable, row, col) then
local cell_possible = sudoku.GetPossible(sudokuTable, row, col)
for pvalue, is_possible in pairs(cell_possible) do
table.insert(possible[pvalue], sudoku.RowColToCell(row, col))
end
end
end
-- iterate through the values and if only one possibility set it
for value = 1, 9 do
if TableCount(possible[value]) == 1 then
local cell = possible[value][1]
sudoku.SetCellValue(sudokuTable, cell, value)
sudoku.RemovePossibleAll(sudokuTable, cell, value)
changed_cells[cell] = value
end
end
end
if TableIsEmpty(changed_cells) then changed_cells = nil end -- reset if not used
return changed_cells
end
-- Find all the cells that only have a single possible value per block and set it
-- Modifies input sudokuTable.
function sudoku.SolveScanBlocks(sudokuTable)
sudoku.CalcAllPossible(sudokuTable)
local changed_cells = {}
for block = 1, 9 do
local block_row, block_col = sudoku.BlockToRowCol(block)
local possible = {}
for i = 1, 9 do possible[i] = {} end
-- fill possible[pvalue] = { cell1, cell2... } for whole block
for row = block_row, block_row+2 do
for col = block_col, block_col+2 do
if not sudoku.HasValue(sudokuTable, row, col) then
local cell_possible = sudoku.GetPossible(sudokuTable, row, col)
for pvalue, is_possible in pairs(cell_possible) do
table.insert(possible[pvalue], sudoku.RowColToCell(row, col))
end
end
end
end
-- iterate through the values and if only one possibility set it
for value = 1, 9 do
if TableCount(possible[value]) == 1 then
local cell = possible[value][1]
sudoku.SetCellValue(sudokuTable, cell, value)
sudoku.RemovePossibleAll(sudokuTable, cell, value)
changed_cells[cell] = value
end
end
end
if TableIsEmpty(changed_cells) then changed_cells = nil end -- reset if not used
return changed_cells
end
-- Find all the cells that only have a single possible value per row, col, block and set it
-- Modifies input sudokuTable.
function sudoku.SolveScan(sudokuTable)
local changed_single = {}
local changed_rows = {}
local changed_cols = {}
local changed_blocks = {}
local changed_cells = {} -- total cells changed
local count = 0
local function add_changed(changed_table, changed_cells)
if changed_table then
changed_cells = TableMerge(changed_table, changed_cells)
end
return changed_cells
end
while (count < 10000) and (changed_single or changed_rows or changed_cols or changed_blocks) do
changed_single = sudoku.SolveScanSingles(sudokuTable)
changed_rows = sudoku.SolveScanRows(sudokuTable)
changed_cols = sudoku.SolveScanCols(sudokuTable)
changed_blocks = sudoku.SolveScanBlocks(sudokuTable)
changed_cells = add_changed(changed_single, changed_cells)
changed_cells = add_changed(changed_rows, changed_cells)
changed_cells = add_changed(changed_cols, changed_cells)
changed_cells = add_changed(changed_blocks, changed_cells)
count = count + 1
end
if TableIsEmpty(changed_cells) then changed_cells = nil end -- nothing done
return count, changed_cells
end
-- Brute force recursive solver, returns a new table and does not the input sudokuTable.
-- (call with only the SudokuTable, don't enter other parameters)
function sudoku.SolveBruteForce(sudokuTable, backwards)
-- first time through find possible to limit choices, subsequent calls ok
local s = sudoku.CreateTable()
-- finding all the possibilities is slow, but at least do solve scan
--s.flags[sudoku.ELIMINATE_NAKED_PAIRS] = true
--s.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] = true
--s.flags[sudoku.ELIMINATE_NAKED_TRIPLETS] = true
--s.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] = true
--s.flags[sudoku.ELIMINATE_NAKED_QUADS] = true
--s.flags[sudoku.ELIMINATE_HIDDEN_QUADS] = true
s.values = TableCopy(sudokuTable.values)
sudoku.CalcRowColBlockValues(s)
sudoku.CalcAllPossible(s)
sudoku.SolveScan(s)
-- table consists of guesses[cell] = #num
-- guesses.current is current guess #
local guesses = { current = 0 }
for n = 1, 81 do guesses[n] = 0 end
-- we don't need these for this and they just slow TableCopy down
-- they're recreated at the end using UpdateTable
s.row_values = nil
s.col_values = nil
s.block_values = nil
s.invalid = nil
s.flags = nil
return sudoku.DoSolveBruteForce(sudokuTable, backwards, s, guesses, 1)
end
function sudoku.DoSolveBruteForce(sudokuTable, backwards, simpleTable, guesses, cell)
local s = simpleTable
local g, empty_possible
if sudoku.SolveBruteForceHook then
if not sudoku.SolveBruteForceHook(guesses, cell) then
return nil, guesses, cell
end
end
while cell <= 81 do
if not sudoku.HasCellValue(s, cell) then
local possible = sudoku.GetCellPossible(s, cell)
--for k, v in pairs(possible) do -- use for loop to ensure direction
for n = 1, 9 do
local k = n
if backwards == true then k = 10 - n end
if possible[k] then
-- try a number and remove it as a possibility
sudoku.RemoveCellPossible(s, cell, k)
-- start a new table and test out this guess
local s1 = TableCopy(s)
-- don't use SetValue since we only care about possible
s1.values[cell] = k --sudoku.SetValue(s1, row, col, k)
sudoku.RemovePossibleAll(s1, cell, k, nil, true)
guesses[cell] = guesses[cell] + 1
guesses.current = guesses.current + 1
-- check for nil return from RemovePossibleAll for break_if_empty
if s1 then
s1, g = sudoku.DoSolveBruteForce(sudokuTable, backwards, s1, guesses, cell+1)
-- if s1 then success! we're all done
if s1 then
-- copy all original data back and just set the values
local s2 = TableCopy(sudokuTable)
sudoku.SetValues(s2, s1.values)
return s2, g
end
end
end
end
return nil, guesses -- tried all values for cell with no solution
end
cell = cell + 1
end
local s2 = TableCopy(sudokuTable)
sudoku.SetValues(s2, s.values)
return s2, guesses
end
-- Does this puzzle have a unique solution. It works by trying the brute force
-- solution method iterating from low to high numbers and then from high to low.
-- This should always find if there are at least two solutions.
-- returns nil if no solution or [s1, s2] if at least two solutions, else
-- just the single unique solution.
-- Returns a new solved table or nil on failure, doesn't modify input sudokuTable.
function sudoku.IsUniquePuzzle(sudokuTable)
local s1, g1 = sudoku.SolveBruteForce(sudokuTable, false)
if not s1 then return nil end
local s2, g2 = sudoku.SolveBruteForce(sudokuTable, true)
if not s2 then return nil end
if not sudoku.IsSamePuzzle(s1, s2) then return s1, s2 end
return s1
end
-- Do these two puzzles have the same cell values? Returns true/false.
function sudoku.IsSamePuzzle(s1, s2)
for cell = 1, 81 do
if sudoku.GetCellValue(s1, cell) ~= sudoku.GetCellValue(s2, cell) then
return false
end
end
return true
end
-- ============================================================================
-- Create a full puzzle with all the values
-- returns sudokuTable, count where count is the number of iterations
function sudoku.GeneratePuzzle()
local cell = 0
local count = 0
local stuck = {} -- count how many times we've backtracked stuck[cell/9] = count
math.randomseed(os.time())
local sudokuTable = sudoku.CreateTable()
while cell < 81 do
cell = cell + 1
count = count + 1
if sudoku.GeneratePuzzleHook then
if not sudoku.GeneratePuzzleHook(count, cell) then
return nil, count
end
end
local value = math.random(9)
local row, col = sudoku.CellToRowCol(cell)
if sudoku.IsValidValue(sudokuTable, row, col, value) then
sudoku.SetCellValue(sudokuTable, cell, value)
sudoku.RemovePossibleAll(sudokuTable, cell, value)
else
-- try other values starting at value+1 and wrapping around
local set_value = false
local i = value + 1
if i > 9 then i = 1 end
while i ~= value do
if sudoku.IsValidValue(sudokuTable, row, col, i) then
sudoku.SetCellValue(sudokuTable, cell, i)
sudoku.RemovePossibleAll(sudokuTable, cell, i)
set_value = true
break
end
i = i + 1
if i > 9 then i = 1 end
end
-- whoops, go back a row or more and start over just to be sure
if not set_value then
local block = math.floor(cell/9) + 1
stuck[block] = (stuck[block] or 0) + 1
local goback = 9
if stuck[block] and (stuck[block] > 5) then
goback = 2 * stuck[block] + 1
end
local cell_start = cell - goback
if cell_start < 1 then
cell_start = 1
elseif cell_start < 10 then
stuck = {} -- really start all over
end
for i = cell_start, cell do
sudoku.SetCellValue(sudokuTable, i, 0)
end
cell = cell_start - 1
end
end
end
return sudokuTable, count
end
function sudoku.GeneratePuzzleDifficulty(sudokuTable, num_cells_to_keep, ensure_unique)
if num_cells_to_keep < 1 then
return sudoku.CreateTable()
end
if ensure_unique == nil then ensure_unique = true end
math.randomseed(os.time()+1)
local trial = 1
local i = 0
local count = 0
local soln = TableCopy(sudokuTable)
local cellTable = {}
for n = 1, 81 do cellTable[n] = n end
local cell_count, cell_n, cell
while i < 81 - num_cells_to_keep do
cell_count = #cellTable
-- restart this function if we run out of cells to try
if cell_count + i < num_cells_to_keep then
trial = trial + 1
i = 0
count = 0
cellTable = {}
for n = 1, 81 do cellTable[n] = n end
cell_count = #cellTable
cell_n = math.random(cell_count)
elseif cell_count == 1 then
cell_n = 1
else
cell_n = math.random(cell_count)
end
cell = cellTable[cell_n]
count = count + 1
if sudoku.GeneratePuzzleDifficultyHook then
if not sudoku.GeneratePuzzleDifficultyHook(count, i, cell, cell_count, trial) then
return nil, count
end
end
if ensure_unique == true then
-- test if soln going forward is same as soln backwards and original
local s = TableCopy(sudokuTable)
sudoku.SetCellValue(s, cell, 0)
local soln1 = sudoku.SolveBruteForce(s, false)
if not soln1 then return nil, count end
if not sudoku.IsSamePuzzle(soln, soln1) then
table.remove(cellTable, cell_n)
else
local soln2 = sudoku.SolveBruteForce(s, true)
if not soln2 then return nil, count end
if not sudoku.IsSamePuzzle(soln, soln2) then
table.remove(cellTable, cell_n)
else
table.remove(cellTable, cell_n)
sudoku.SetCellValue(sudokuTable, cell, 0)
i = i + 1
end
end
else
table.remove(cellTable, cell_n)
sudoku.SetCellValue(sudokuTable, cell, 0)
i = i + 1
end
end
return sudokuTable, count
end
-- ============================================================================
-- ============================================================================
sudokuGUIxpmdata =
{
"16 15 7 1",
" c None",
"a c Black",
"b c #808080",
"c c #FFFF00",
"d c #FF0000",
"e c #0000FF",
"g c #00FF00",
" aaaaaaaaaaaaa ",
" addaggaeeaccab ",
" addaggaeeaccab ",
" aaaaaaaaaaaaab ",
" accaddaggaeeab ",
" accaddaggaeeab ",
" aaaaaaaaaaaaab ",
" aeeaccaddaggab ",
" aeeaccaddaggab ",
" aaaaaaaaaaaaab ",
" aggaeeaccaddab ",
" aggaeeaccaddab ",
" aaaaaaaaaaaaab ",
" bbbbbbbbbbbbb ",
" "
}
-- NOTE: HTML generated using NVU and "tidy -wrap 79 -i -omit -o l.html input.html"
sudokuGUIhelp =
[[
wxLuaSudoku
wxLuaSudoku
Copyright : John Labenski, 2006
License : wxWindows license.
This program is free software; you can redistribute it and/or modify it
under the terms of the wxWindows License; either version 3 of the License,
or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the wxWindows License for more
details.
If you use this program and find any bugs or have added any features that
you feel may be generally useful, please feel free to contact me by e-mail
at jrl1[at]sourceforge[dot]net or on the wxlua-users mailing list.
Purpose
The purpose of this program is to demonstrate programming in
wxLua. It may not be the best, fastest, or most complete sudoku solver
program available, but the code should be easily understandable with a
straightforward implementation. However, you may find that it is capable
enough and since it's free, you got what you paid for.
wxLua is a binding of the wxWidgets cross-platform GUI library to the Lua
programming language. It is available as an application, library, or source
code for MSW, Unix like systems, and OSX. More information about wxLua can
be found on wxlua.sourceforge.net. Information about
the wxWidgets library can be found on www.wxwidgets.org and the Lua programming
language on www.lua.org.
Program features
- Enter your own initial values for a puzzle to solve
- Generate random unique new puzzles
- Load/Save puzzles to files
- Print the puzzle
- Undo/Redo of the steps you've taken
- Mark errors and/or mistakes
- Show calculated possible values for each cell
- Pencil marks, possible values for cells that you have determined
- Automatic marking of hidden/naked pairs, triplets, and quads of
possible values
- Eliminate possible values by finding hidden/naked pairs, triplets, and
quads of possible values
- Solve the puzzle by scanning for values, can use eliminate
naked/hidden groups to do better
- Solve the puzzle by brute force, by guessing values from the possible
values
- No installer, registry entries, or other nonsense. Merely delete the
program when you're done with it.
How to play
Sudoku is a puzzle game of choosing where to place the
numbers 1 to 9 in a 9x9 grid of cells. There are many varieties of this
game, but wxLuaSudoku only works with the 9x9 grid. The grid of 81 cells has
9 rows, 9 columns, and 9 blocks that consist of smaller 3x3 grids of cells.
Each cell in the grid can take a value of 1 to 9, but there are limitations.
Each row, column, and block must have the full array of numbers from 1 to 9.
Therefore, there can be no duplicate values in any row, column, or block.
For a well formed puzzle there's just one possible solution for the initial
seed of values. There are numerous tutorials on the internet, for example
http://en.wikipedia.org/wiki/Sudoku,
so lets move on...
Some Sudoku
terms
- Initial values : These are values for the cells that the puzzle
starts with. The minimum number of initial values that a puzzle can start
with is thought to be 17 in order for the puzzle to have a single unique
solution.
- Possible values : These are values that an empty cell might
take given the existing values in the row, col, and block that the cell is
in.
-
Naked pairs, triplets, quads : When looking though the possible
values for the cells you may find that two or more cells have the
identical possible values and only those values. Those would be called
naked because they're immediately visible.
- Pair example: If two cells in a row have the possible values of 2
and 4 then either 2 or 4 has to be in those two cells. You can
therefore eliminate 2 and 4 from the possible values in the rest of
the row and if they're both in the same block, the block too.
-
Hidden pairs, triplets, quads : A hidden group is defined as a
set of numbers that appear in as many cells as the size of the group.
They are similar to naked groups, but there may also be some other
possible values that make them a little harder to find.
- Pair example: If two cells in a row have the possible values 1, 2,
4 and 1, 2, 4, 5, 6 respectively and the numbers 2 and 4 do not appear
in any other cell in the row you know that either 2 or 4 must go into
either cell and eliminate the other possible values from those two
cells.
Entering values
Enter values into the cells by clicking on a cell to
highlight it and then type a number. Use 0, space, or any non number to
clear the cell's value. The cells that the puzzle is initialized with are
read-only, meaning that you cannot change their values unless you are
actually creating a puzzle using the menu item File->Create.
Navigate through the cells with the cursor keys or mouse.
Pencil marks
You can
annotate the empty cells with a list of possible values that may work by
checking the menu item Possible->Show/Edit pencil marks. If you
want initialize the pencil marks to be all selected, none selected, or set
to the calculated values use the menu items under Possible->Pencil
marks. In order to toggle the values, hold down the shift key and all of
the possible values will be shown. Click on a value to either select or
deselect it. Alternatively, you can use shift-Number to toggle the selection
state of the pencil marks.
Note that this is different than the menu item Possible->Show
calculated possible in that, obviously, this will show a list of
calculated possible values that could go into the cells based on the current
state of the puzzle. You can switch between the two views or not show the
possible values altogether by unchecking both of them.
Creating a
puzzle
To create a new puzzle by hand check the menu item
File->Create. You've now put the program in a state where any
values that you enter will be the initial values of the puzzle. Error
checking of invalid values, values that appear more than once in the same
row, column, or block, is automatically turned on since it's fairly
pointless to create puzzles that cannot be solved, though this program
generously allows you to do so. Enter the values as described above and when
done be sure to uncheck the File->Create item. The program will
then try to find a solution to the puzzle and verify that there is only one
unique solution.
If you want to check the state of the puzzle during creation to confirm that
it has a unique solution, use the Solve->Verify unique solution
menu item. If you're determined to create an invalid sudoku puzzle this
program will not stop you and you can merely cancel out of the warning
dialogs to continue and you're on your own. However, showing mistakes will
be disabled and "solving" using the scanning technique may yield strange
results that are mathematically consistent with the rules of the game, but
bring you no closer to the nonexistent solution. If at some later point you
wish to have mistakes shown using View->Mark mistakes, the program
will try to solve the initial puzzle again, with the same result, no
solution.
If you want to start with a completely empty grid use File->New.
If on the other hand, you want to start with a completely filled grid of
random values, use File->Generate with the number of cell values
to show set to 81, all shown.
Cell markings
The colors and fonts for the cells can be changed with
the preferences dialog from the menu item View->Preferences. The
values in the cells have one color if it's an initial value and a different
color if you've entered the value into the cell during play. The possible
values that could go into a particular cell use a smaller font and can be
drawn as a 3x3 grid or in a single line which may be useful if you want to
print it and work it out by hand.
Possible groups of values for the cells may be set to be marked if they are
naked and or hidden. Note: It may happen that, for example, there's a
triplet of possible values in a row and some of the values are shared by a
pair in a column. In that case the pair values get the appropriate markings
overriding the triplet markings since it's usually best to try to work with
them first. Also, if you get really off track the program will happily mark
groups that would obviously make no sense if there weren't any mistakes.
This is by design since some people apparently find it interesting to do
whatever it is that they want to do.
Menu item descriptions
All items followed by ... denote that a
dialog will be shown that can be canceled before any action is taken.
-
File
- New... : Clear the whole puzzle to an uninitialized state so
that you can then "Create" it from scratch.
-
Create... : Check this item to enter the initial values,
uncheck when done to play.
-
Generate... : Have the program create a new random unique
puzzle for you.
- Set the number of cell values to show, the more that are
visible, the easier it'll probably be.
- This program does not try to categorize how difficult the
generated puzzle is.
- Note: If you have less than 17 cells shown there will not be a
unique solution.
- The less cells you have shown the longer it will take to find
a unique puzzle, but the program will try for as long as it takes.
If you get impatient, just cancel the generation and you will be
asked if you want the program to just randomly remove cell values
which may or may not yield a unique solution.
-
Open... : Open a puzzle from a file, the puzzle should be a
9x9 grid of numbers separated by spaces or commas.
- After opening, the program will try to solve the puzzle and let
you know if there is a problem, such as a non unique solution. You
can just cancel out if you'd like to skip this check.
- Save as... : Save the current puzzle to disk.
- Page Setup... : Adjust the paper and margins for printing.
- Print Preview... : Preview what the printout will look
like.
- Print... : Print the current puzzle, WYSIWYG, what you see
is what you get.
- Exit : Exit the program.
-
Edit
- Copy puzzle : Copy the puzzle to the clipboard to paste
into other programs.
- Reset... : Clear all the values you've entered to return
back to the initial state. This is the same as just undoing back to
the beginning.
- Undo/Redo : You can undo and redo values that you've
entered into the puzzle. If you undo a couple steps and enter a new
value you cannot then redo back to where you were.
- Preferences : Set the colors to use and whatnot in a
dialog. See the preferences dialog
section.
-
Save preferences : Save the current preferences, including
what you've set to show and exclude.
- This program does not automatically save the preferences on
exit, but rather only when you explicitly request it to do so.
- MSW : preferences are stored in "Documents and
Settings\username\wxLuaSudoku.ini"
- Unix : preferences are stored in "~/.wxLuaSudoku.ini"
- You may delete these files if you want to reset the program to
the defaults or are done using it.
-
View
- Mark errors : Mark the cells that have obviously invalid
values, not invalid from the standpoint of it being the "correct"
value, but whether or not it's in conflict with another value that has
already exists in same row, column, or block in the puzzle.
-
Mark mistakes : Mark the cells that have "wrong" values that
will not lead to a solution by comparing the values to a
precalculated solution.
- If the solution has not been already found, perhaps you've
canceled the initial check, the program will have to work it out.
If you cancel this process and then want any mistakes shown again,
it'll have to try to solve it again. In the case that the program
cannot find a solution or there isn't a unique solution, a warning
dialog will be shown and marking the mistakes will be
automatically unchecked.
- Show toolbar : Show or hide the toolbar at the top of the
program.
- Show statusbar : Show or hide the statusbar at the bottom
of the program.
-
Possible
-
Show calculated possible : Show the possible values that the
program has calculated for the empty cells.
- Note that this does not take into account any invalid or
erroneous values you may have entered, it just shows you possible
values given the current state of the puzzle.
-
Show/Edit pencil marks : Allows you to enter the possible
values for the cells by hand.
- See the section above about entering
pencil marks.
- You can show the calculated possible values or the pencil
marks or neither, but not both at the same time.
- Show possible line : Show the possible values in a single
line, useful if you want to print it out to play later.
-
Pencil marks
- Clear all... : Clear all the pencil marks for the whole
puzzle.
- Set all... : Set all values for the pencil marks for
the whole puzzle.
- Calculate... : Set all the pencil marks to the same
values as the calculated possible values.
-
Mark groups
- Mark naked groups : Mark all naked groups of cells;
pairs, triplets, and quads.
- Mark hidden groups : Mark all hidden groups of cells;
pairs, triplets, and quads.
- ... mark each group individually
- Note: See Solve->Eliminate to remove possible values
that can be excluded once the groups have been found. You do not
have to show the groups to eliminate values or vice versa.
-
Solve
-
Verify unique solution... : Use the brute force solver to
find if there is more than one solution for the initial values of
the puzzle or if there is a solution at all.
- Typically this is not necessary since the program
automatically tries to verify the puzzle after opening or creating
a puzzle, but if you had canceled the check you can verify it by
hand using this.
-
Show solution : Show the solution to the puzzle that should
have been automatically found after opening, creating, or generating
a puzzle.
- If you had canceled out of the puzzle verification procedure
the program will have to solve it using the initial values in the
puzzle.
- Use undo to return back to playing the game if you wish to
just take a peek at some answers.
-
Eliminate groups
- Eliminate naked groups : Remove all calculated possible
values that can be excluded by evaluating the naked groups of
pairs, triplets, and quads. This does not work on the pencil
marks. See the Sudoku terms
section about naked groups.
- Eliminate hidden groups : Remove all calculated
possible values that can be excluded by evaluating the hidden
groups of pairs, triplets, and quads. This does not work on the
pencil marks. See the Sudoku
terms section about hidden groups.
- ... eliminate each group individually
- Note: This updates the View->Show calculated possible
and also for solving using Solve (scan ...) since the more
possible values you remove the easier it is to narrow down the
correct values.
- Note on solving: Solving works on the current puzzle
state which means that if you have inadvertently entered a wrong value
the solver stops at the point where no new values can be placed. The
values it finds will be logically correct based on the rules of the
game, but probably wrong. Use mark mistakes if you want to check how
you're doing.
- Solve (scan singles) : Fill in the values for all cells
that can only take one value.
- Solve (scan rows) : Fill in the values of cells that are
the only ones in the row to have a particular possible value.
- Solve (scan cols) : Same for columns
- Solve (scan blocks) : Same for blocks
- Note on solve scanning : You can try iterating through
scanning singles, rows, columns, blocks to see which new values can be
found after other values have been set.
-
Solve (scanning) : Try solving the puzzle by scanning over
and over until no new cell values can be found.
- Check different eliminate groups to allow the solver to take
advantage of the reduced number of possible values.
-
Solve (brute force) : Solve the puzzle by guessing values
from the possible values using the current puzzle values.
- This will fail if the puzzle cannot be solved (maybe you've
made a mistake?)
-
Help
- About... : A simple about this program dialog.
- Help... : Show this document.
Preferences
dialog
In the preferences dialog you can adjust the colors to use for
the different elements in a cell. A sample cell is shown to give you an idea
of what it'll look like when done. Note that some fonts you may choose will
not render properly or may be badly placed in the cells. This is a problem
with the font itself, just pick a different font. Also, bitmapped fonts only
come in a limited number of sizes and cannot be scaled to arbitrary sizes,
again, pick another font that works better on your system.
Additionally, a simpler interface to the menu items Possible->Mark
groups and Solve->Eliminate groups is provided to make it
easier to check and uncheck multiple items.
]]
-- Simple way to generate unique window or menu ids and ensure they're in order
function NewID()
if not sudokuGUI_ID_New then sudokuGUI_ID_New = wx.wxID_HIGHEST end
sudokuGUI_ID_New = sudokuGUI_ID_New + 1
return sudokuGUI_ID_New
end
sudokuGUI =
{
frame = nil, -- The wxFrame of the program
panel = nil, -- The main wxPanel child of the wxFrame
cellWindows = {}, -- The 81 grid cell wxWindows, children of panel
cellTextCtrl = nil, -- Single wxTextCtrl editor for entering cell values
focused_cell_id = 0, -- window id of the currently focused cell, 0 for none
block_refresh = false, -- temporarily block refreshing when true
filePath = "", -- last opened filePath
fileName = "", -- last opened fileName
printData = wx.wxPrintData(),
pageSetupData = wx.wxPageSetupDialogData(),
config = nil, -- the wxFileConfig for saving preferences
Colours = {}, -- table of wxColours to use, indexes below
VALUE_COLOUR = 1,
INIT_VALUE_COLOUR = 2,
POSS_VALUE_COLOUR = 3,
INVALID_VALUE_COLOUR = 4,
BACKGROUND_COLOUR = 5,
ODD_BACKGROUND_COLOUR = 6,
FOCUS_CELL_COLOUR = 7,
NAKED_PAIRS_COLOUR = 8,
NAKED_TRIPLETS_COLOUR = 9,
NAKED_QUADS_COLOUR = 10,
HIDDEN_PAIRS_COLOUR = 11,
HIDDEN_TRIPLETS_COLOUR = 12,
HIDDEN_QUADS_COLOUR = 13,
COLOUR_MAX = 13,
-- A large wxFont for the cell values
valueFont = { wxfont = nil, size = 8, width = 0, height = 0 },
-- A smallish wxFont for the possible values
possibleFont = { wxfont = nil, size = 6, width = 0, height = 0 },
valueFont_cache = {}, -- valueFont_cache[size] = { width, height }
possibleFont_cache = {}, -- possibleFont_cache[size] = { width, height }
-- calc positions once in PaintCell
-- recalc if any of these parameters change
-- pos[value].x and .y are upper left corners
possiblePosCache = { pos = {}, width = 1, height = 1, line = false, cell_width = 1, cell_height = 1 },
query_save_prefs = true, -- tell user that prefs are stored as fileconfig
ID_NEW = NewID(),
ID_CREATE = NewID(),
ID_GENERATE = NewID(),
ID_OPEN = NewID(),
ID_SAVEAS = NewID(),
ID_PAGESETUP = NewID(),
ID_PRINTSETUP = NewID(),
ID_PRINTPREVIEW = NewID(),
ID_PRINT = NewID(),
ID_EXIT = wx.wxID_EXIT,
ID_COPY_PUZZLE = NewID(),
ID_RESET = NewID(),
ID_UNDO = NewID(),
ID_REDO = NewID(),
ID_PREFERENCES = NewID(), -- show preferences dialog
ID_SAVE_PREFERENCES = NewID(), -- save preferences
ID_SHOW_TOOLBAR = NewID(),
ID_SHOW_TOOLBAR_LABELS = NewID(),
ID_SHOW_STATUSBAR = NewID(),
ID_SHOW_ERRORS = NewID(), -- Show errors in the grid
ID_SHOW_MISTAKES = NewID(), -- Show mistakes in the grid
ID_SHOW_POSSIBLE = NewID(), -- Show the possible values
ID_SHOW_USER_POSSIBLE = NewID(), -- Show the user's possible values
ID_SHOW_POSSIBLE_LINE = NewID(), -- Show the possible values in a line
ID_USER_POSSIBLE_MENU = NewID(),
ID_USER_POSSIBLE_CLEAR = NewID(),
ID_USER_POSSIBLE_SETALL = NewID(),
ID_USER_POSSIBLE_INIT = NewID(),
ID_SHOW_MENU = NewID(),
ID_SHOW_NAKED = NewID(), -- mark naked groups
ID_SHOW_HIDDEN = NewID(), -- mark hidden groups
ID_SHOW_NAKEDPAIRS = NewID(), -- mark naked pairs
ID_SHOW_HIDDENPAIRS = NewID(), -- mark hidden pairs
ID_SHOW_NAKEDTRIPLETS = NewID(), -- mark naked triplets
ID_SHOW_HIDDENTRIPLETS = NewID(), -- mark hidden triplets
ID_SHOW_NAKEDQUADS = NewID(), -- mark naked quads
ID_SHOW_HIDDENQUADS = NewID(), -- mark hidden quads
ID_VERIFY_PUZZLE = NewID(),
ID_SHOW_SOLUTION = NewID(), -- Show the solution to the puzzle
ID_ELIMINATE_MENU = NewID(),
ID_ELIMINATE_NAKED = NewID(), -- eliminate naked groups
ID_ELIMINATE_HIDDEN = NewID(), -- eliminate hidden groups
ID_ELIMINATE_NAKEDPAIRS = NewID(), -- eliminate naked pairs
ID_ELIMINATE_HIDDENPAIRS = NewID(), -- eliminate hidden pairs
ID_ELIMINATE_NAKEDTRIPLETS = NewID(), -- eliminate naked triplets
ID_ELIMINATE_HIDDENTRIPLETS = NewID(), -- eliminate hidden triplets
ID_ELIMINATE_NAKEDQUADS = NewID(), -- eliminate naked quads
ID_ELIMINATE_HIDDENQUADS = NewID(), -- eliminate hidden quads
ID_SOLVE_SCANSINGLES = NewID(), -- Solve for single lone values
ID_SOLVE_SCANROWS = NewID(), -- Solve for single values in rows
ID_SOLVE_SCANCOLS = NewID(), -- Solve for single values in cols
ID_SOLVE_SCANBLOCKS = NewID(), -- Solve for single values in blocks
ID_SOLVE_SCANNING = NewID(), -- Solve the puzzle by scanning
ID_SOLVE_BRUTEFORCE = NewID(), -- Solve the puzzle by brute force
ID_ABOUT = NewID(),
ID_HELP = NewID(),
menuCheckIDs = {}, -- the current state of the check menu items
-- GTK doesn't like to access them in EVT_PAINT
-- we don't have to set them since nil == false
sudokuTables = {}, -- A table of the sudoku tables for undoing, should always be 1
sudokuTables_pos = 0, -- Current position in the tables
sudokuSolnTable = nil, -- solution to the current puzzle
nonunique_init_puzzle = nil, -- nil for don't know, is true/false once verified
possNakedTable = nil,
possHiddenTable = nil,
pencilMarks = {}, -- pencilMarks[cell][value] = true/nil
pencilMarksNakedTable = nil,
pencilMarksHiddenTable = nil,
difficulty = 30, -- number of cells to keep for new puzzle
sayings_n = 0, -- number of sayings, calculated later
sayings = {
"Pondering... ",
"Musing... ",
"Meditating... ",
"Contemplating... ",
"Reflecting... ",
"Mulling... ",
"Considering... ",
"Deliberating... ",
"Thinking... ",
"Working... ",
"Grinding... ",
"Brooding... ",
"Languishing? ",
"Despairing... ",
"To be, or not to be that is the question ",
"Whether 'tis nobler in the mind to suffer ",
"The slings and arrows of outrageous fortune, ",
"Or to take arms against a sea of troubles, ",
"And by opposing end them? ",
"To die: to sleep; ",
"No more; and by a sleep to say we end ",
"The heart-ache and the thousand natural shocks",
"That flesh is heir to, 'tis a consummation",
"Devoutly to be wish'd. To die, to sleep;",
"To sleep: perchance to dream:",
"ay, there's the rub;",
"For in that sleep of death what dreams may come",
"When we have shuffled off this mortal coil,",
"Must give us pause: there's the respect",
"That makes calamity of so long life;",
"For who would bear the whips and scorns of time,",
"The oppressor's wrong, the proud man's contumely,",
"The pangs of despised love, the law's delay,",
"The insolence of office and the spurns",
"That patient merit of the unworthy takes,",
"When he himself might his quietus make",
"With a bare bodkin? who would fardels bear,",
"To grunt and sweat under a weary life,",
"But that the dread of something after death,",
"The undiscover'd country from whose bourn",
"No traveller returns, puzzles the will",
"And makes us rather bear those ills we have",
"Than fly to others that we know not of?",
"Thus conscience does make cowards of us all;",
"And thus the native hue of resolution",
"Is sicklied o'er with the pale cast of thought,",
"And enterprises of great pitch and moment",
"With this regard their currents turn awry,",
"And lose the name of action.-- Soft you now!",
"The fair Ophelia! Nymph, in thy orisons",
"Be all my sins remember'd..."
}
}
sudokuGUI.sayings_n = #sudokuGUI.sayings -- calc number of sayings
for cell = 1, 81 do
sudokuGUI.pencilMarks[cell] = {}
end
-- ----------------------------------------------------------------------------
-- Run this function once to find the best font sizes to use and store them
-- if height/width = nil then use a default size,
-- else try to fit a font size to given height/width
function sudokuGUI.GetCellBestSize(cell_width, cell_height)
local dc = wx.wxClientDC(sudokuGUI.frame)
local size = sudokuGUI.DoGetCellBestSize(dc, cell_width, cell_height,
sudokuGUI.valueFont, sudokuGUI.possibleFont,
sudokuGUI.valueFont_cache, sudokuGUI.possibleFont_cache)
dc:delete() -- ALWAYS delete() any wxDCs created when done
return size
end
function sudokuGUI.DoGetCellBestSize(dc, cell_width, cell_height,
valueFont, possibleFont,
valueFont_cache, possibleFont_cache) -- cache are optional
local function GetBestFontSize(dc, width, height, fontTable, font_cache)
local function DoGetBestFontSize(step, largest)
for s = fontTable.size, largest, step do
fontTable.size = s
if font_cache[fontTable.size] then
fontTable.width = font_cache[fontTable.size].width
fontTable.height = font_cache[fontTable.size].height
else
fontTable.wxfont:SetPointSize(fontTable.size)
dc:SetFont(fontTable.wxfont)
fontTable.width, fontTable.height = dc:GetTextExtent("5")
font_cache[fontTable.size] = {}
font_cache[fontTable.size].width = fontTable.width
font_cache[fontTable.size].height = fontTable.height
end
if (fontTable.height > height) or (fontTable.width > width) then
break
end
end
end
fontTable.size = 2
if not font_cache then font_cache = {} end -- use local font cache
-- skip font sizes by 4 to get a rough estimate of the size
DoGetBestFontSize(4, 1000)
local largest = fontTable.size
fontTable.size = iff(fontTable.size-3 > 1, fontTable.size-3, 2)
-- get the best size to use
DoGetBestFontSize(1, largest)
-- use next smaller value that actually fits
fontTable.size = iff(fontTable.size-1 > 1, fontTable.size-1, 2)
fontTable.wxfont:SetPointSize(fontTable.size)
fontTable.width = font_cache[fontTable.size].width
fontTable.height = font_cache[fontTable.size].height
return fontTable
end
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE) then
possibleFont = GetBestFontSize(dc, (cell_width-2)/10, (cell_height-2), possibleFont, possibleFont_cache)
else
possibleFont = GetBestFontSize(dc, (cell_width-2)/3, (cell_height-2)/3, possibleFont, possibleFont_cache)
end
valueFont = GetBestFontSize(dc, cell_width-4, cell_height-4, valueFont, valueFont_cache)
return wx.wxSize(valueFont.height+4, valueFont.height+4)
end
-- ----------------------------------------------------------------------------
-- Is this cell in an "odd" block for colouring the blocks
function sudokuGUI.IsOddBlockCell(cell)
return math.fmod(sudoku.CellToBlock(cell), 2) ~= 0
end
-- ----------------------------------------------------------------------------
-- Create one of the 81 cell windows and connect the events to it
function sudokuGUI.CreateCellWindow(parent, winID, size)
local win = wx.wxWindow(parent, winID,
wx.wxDefaultPosition, wx.wxDefaultSize,
wx.wxWANTS_CHARS+wx.wxSIMPLE_BORDER) --wxSUNKEN_BORDER)
-- set the background colour to reduce flicker
if sudokuGUI.IsOddBlockCell(winID) then
win:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR])
else
win:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR])
end
win:Connect(wx.wxEVT_ERASE_BACKGROUND, function(event) end)
win:Connect(wx.wxEVT_PAINT, sudokuGUI.OnPaintCellWindow)
win:Connect(wx.wxEVT_KEY_DOWN, sudokuGUI.OnKeyDownCellWindow )
win:Connect(wx.wxEVT_KEY_UP, sudokuGUI.OnKeyUpCellWindow )
win:Connect(wx.wxEVT_LEFT_DOWN, sudokuGUI.OnLeftClickCellWindow )
win:Connect(wx.wxEVT_LEFT_DCLICK, sudokuGUI.OnLeftDClickCellWindow )
return win
end
-- ----------------------------------------------------------------------------
-- wxPaintEvent handler for all of the cell windows
function sudokuGUI.OnPaintCellWindow(event)
local win = event:GetEventObject():DynamicCast("wxWindow")
-- ALWAYS create a wxPaintDC in a wxEVT_PAINT handler, even if unused
local dc = wx.wxPaintDC(win)
if not sudokuGUI.block_refresh then
local cell = win:GetId()
local width, height = win:GetClientSizeWH()
sudokuGUI.PaintCell(dc, cell, width, height, sudokuGUI.valueFont, sudokuGUI.possibleFont)
end
dc:delete() -- ALWAYS delete() any wxDCs created when done
end
function sudokuGUI.PaintCell(dc, cell, width, height, valueFont, possibleFont)
-- clear the window before drawing
dc:SetPen(wx.wxTRANSPARENT_PEN)
local bgColour
if sudokuGUI.focused_cell_id ~= cell then
if sudokuGUI.IsOddBlockCell(cell) then
bgColour = sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR]
else
bgColour = sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR]
end
else
bgColour = sudokuGUI.Colours[sudokuGUI.FOCUS_CELL_COLOUR]
end
local brush = wx.wxBrush(bgColour, wx.wxSOLID)
dc:SetBrush(brush)
dc:DrawRectangle(0, 0, width, height)
brush:delete()
local sudokuTable = sudokuGUI.GetCurrentTable()
local value_str, is_init = sudokuGUI.GetCellValueString(cell)
local has_cell_value = string.len(value_str) ~= 0
-- Draw the possible values
local show_possible = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE)
local show_possible_user = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE)
local show_possible_line = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE)
if (show_possible or show_possible_user) and (not has_cell_value) then
local possible = sudoku.GetCellPossible(sudokuTable, cell)
if show_possible_user then
possible = sudokuGUI.pencilMarks[cell]
end
dc:SetTextForeground(sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR])
dc:SetFont(possibleFont.wxfont)
-- find the positions of each possible value
local pos = sudokuGUI.CalcPossiblePositions(width, height, possibleFont)
local show_possible_user_all = false
if show_possible_user and (cell == sudokuGUI.focused_cell_id) then
show_possible_user_all = wx.wxGetKeyState(wx.WXK_SHIFT)
end
-- draw each one separately, even for line to ensure monospace
for i = 1, 9 do
if possible and possible[i] then
dc:DrawText(tostring(i), pos[i].x, pos[i].y)
elseif show_possible_user_all then
dc:SetBackgroundMode(wx.wxSOLID)
dc:SetTextForeground(bgColour)
dc:SetTextBackground(sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR])
dc:DrawText(tostring(i), pos[i].x, pos[i].y)
dc:SetTextForeground(sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR])
dc:SetTextBackground(bgColour)
dc:SetBackgroundMode(wx.wxTRANSPARENT)
end
end
local nakedTable = sudokuGUI.possNakedTable
local hiddenTable = sudokuGUI.possHiddenTable
if show_possible_user then
nakedTable = sudokuGUI.pencilMarksNakedTable
hiddenTable = sudokuGUI.pencilMarksHiddenTable
end
if nakedTable and hiddenTable then
dc:SetBrush(wx.wxTRANSPARENT_BRUSH)
local char0 = string.byte("0")
local function draw_nakedhidden(colourID, num, key_table, hidden)
if (not key_table) or (#key_table < 1) then return end
local pen = wx.wxPen(sudokuGUI.Colours[colourID], 1, wx.wxSOLID)
dc:SetPen(pen)
for k = 1, #key_table do
for n = 1, num do
local val = string.byte(key_table[k], n)-char0
if hidden ~= true then
dc:DrawRectangle(pos[val].x, pos[val].y,
possibleFont.width, possibleFont.height)
else
dc:DrawRoundedRectangle(pos[val].x, pos[val].y,
possibleFont.width, possibleFont.height,
20)
end
end
end
pen:delete()
end
-- draw pair marker last so it's on top of the others
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_HIDDENQUADS) then
draw_nakedhidden(sudokuGUI.HIDDEN_QUADS_COLOUR, 4, hiddenTable.quads.cells[cell], true)
end
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_NAKEDQUADS) then
draw_nakedhidden(sudokuGUI.NAKED_QUADS_COLOUR, 4, nakedTable.quads.cells[cell])
end
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_HIDDENTRIPLETS) then
draw_nakedhidden(sudokuGUI.HIDDEN_TRIPLETS_COLOUR, 3, hiddenTable.triplets.cells[cell], true)
end
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_NAKEDTRIPLETS) then
draw_nakedhidden(sudokuGUI.NAKED_TRIPLETS_COLOUR, 3, nakedTable.triplets.cells[cell])
end
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_HIDDENPAIRS) then
draw_nakedhidden(sudokuGUI.HIDDEN_PAIRS_COLOUR, 2, hiddenTable.pairs.cells[cell], true)
end
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_NAKEDPAIRS) then
draw_nakedhidden(sudokuGUI.NAKED_PAIRS_COLOUR, 2, nakedTable.pairs.cells[cell])
end
end
end
-- mark invalid cells, always mark invalid if creating
local show_errors = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_ERRORS)
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE) then show_errors = true end
if show_errors then show_errors = sudokuGUI.GetCurrentTable().invalid[cell] end
local show_mistakes = sudokuGUI.sudokuSolnTable and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES)
if show_errors then
dc:SetPen(wx.wxPen(sudokuGUI.Colours[sudokuGUI.INVALID_VALUE_COLOUR], 1, wx.wxSOLID))
dc:DrawLine(0, 0, width, height)
dc:DrawLine(width, 0, 0, height)
elseif show_mistakes then
if sudoku.HasCellValue(sudokuTable, cell) and
(sudoku.GetCellValue(sudokuGUI.sudokuSolnTable, cell) ~=
sudoku.GetCellValue(sudokuTable, cell)) then
local pen = wx.wxPen(sudokuGUI.Colours[sudokuGUI.INVALID_VALUE_COLOUR], 1, wx.wxSOLID)
dc:SetPen(pen)
pen:delete()
dc:DrawLine(0, 0, width, height)
end
end
-- Draw the set value, if any
if has_cell_value then
local fgColour = sudokuGUI.Colours[sudokuGUI.VALUE_COLOUR]
if is_init then
fgColour = sudokuGUI.Colours[sudokuGUI.INIT_VALUE_COLOUR]
end
dc:SetTextForeground(fgColour)
dc:SetFont(valueFont.wxfont)
dc:DrawText(value_str, width/2 - valueFont.width/2,
height/2 - valueFont.height/2)
end
end
-- ----------------------------------------------------------------------------
function sudokuGUI.CalcPossiblePositions(width, height, possibleFont)
local pos = {}
local show_possible_line = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE)
if (sudokuGUI.possiblePosCache.pos and
(sudokuGUI.possiblePosCache.cell_width == width) and
(sudokuGUI.possiblePosCache.cell_height == height) and
(sudokuGUI.possiblePosCache.width == possibleFont.width) and
(sudokuGUI.possiblePosCache.height == possibleFont.height) and
(sudokuGUI.possiblePosCache.line == show_possible_line)) then
pos = sudokuGUI.possiblePosCache.pos
else
if show_possible_line then
for i = 1, 9 do
pos[i] = { x = (1+ (i-1)*possibleFont.width), y = 1 }
end
else
local c = 0
local horiz_space = (width - 3*possibleFont.width)/4
local vert_space = (height - 3*possibleFont.height)/4
local h_space = horiz_space
local v_space = vert_space
for j = 1, 3 do
-- try to center it a little better
if (j == 1) and (vert_space - math.floor(vert_space) > .3) then
v_space = vert_space+1
else
v_space = vert_space
end
for i = 1, 3 do
c = c + 1
if (i == 1) and (horiz_space - math.floor(horiz_space) > .3) then
h_space = horiz_space+1
else
h_space = horiz_space
end
pos[c] = { x = i*h_space + (i-1)*possibleFont.width,
y = j*v_space + (j-1)*possibleFont.height }
end
end
end
-- cache these values for next cell
sudokuGUI.possiblePosCache.pos = pos
sudokuGUI.possiblePosCache.cell_width = width
sudokuGUI.possiblePosCache.cell_height = height
sudokuGUI.possiblePosCache.width = possibleFont.width
sudokuGUI.possiblePosCache.height = possibleFont.height
sudokuGUI.possiblePosCache.line = show_possible_line
end
return pos
end
-- ----------------------------------------------------------------------------
function sudokuGUI.ConnectPrintEvents(printOut)
printOut:SetPageInfo(1, 1, 1, 1)
printOut.HasPage = function(self, pageNum) return pageNum == 1 end
--printOut.GetPageInfo = function(self) return 1, 1, 1, 1 end
printOut.OnPrintPage = function(self, pageNum)
local dc = self:GetDC()
local ppiScr_width, ppiScr_height = self:GetPPIScreen()
local ppiPrn_width, ppiPrn_height = self:GetPPIPrinter()
local ppi_scale_x = ppiPrn_width/ppiScr_width
local ppi_scale_y = ppiPrn_height/ppiScr_height
-- Get the size of DC in pixels and the number of pixels in the page
local dc_width, dc_height = dc:GetSize()
local pagepix_width, pagepix_height = self:GetPageSizePixels()
local dc_pagepix_scale_x = dc_width/pagepix_width
local dc_pagepix_scale_y = dc_height/pagepix_height
-- If printer pageWidth == current DC width, then this doesn't
-- change. But w might be the preview bitmap width, so scale down.
local dc_scale_x = ppi_scale_x * dc_pagepix_scale_x
local dc_scale_y = ppi_scale_y * dc_pagepix_scale_y
-- calculate the pixels / mm (25.4 mm = 1 inch)
local ppmm_x = ppiScr_width / 25.4
local ppmm_y = ppiScr_height / 25.4
-- Adjust the page size for the pixels / mm scaling factor
local pageMM_width, pageMM_height = self:GetPageSizeMM()
local pagerect_x, pagerect_y = 0, 0
local pagerect_w, pagerect_h = pageMM_width * ppmm_x, pageMM_height * ppmm_y
-- get margins informations and convert to printer pixels
local topLeft = sudokuGUI.pageSetupData:GetMarginTopLeft()
local bottomRight = sudokuGUI.pageSetupData:GetMarginBottomRight()
local top = topLeft:GetY() * ppmm_y
local bottom = bottomRight:GetY() * ppmm_y
local left = topLeft:GetX() * ppmm_x
local right = bottomRight:GetX() * ppmm_x
local printrect_x, printrect_y = left, top
local printrect_w, printrect_h = pagerect_w-(left+right), pagerect_h-(top+bottom)
dc:SetUserScale(dc_scale_x, dc_scale_y);
local cell_width = (printrect_w)/11
local cell_height = (printrect_h)/11
if cell_width < cell_height then cell_height = cell_width end
if cell_width > cell_height then cell_width = cell_height end
-- calculate font sizes for the printout, copy font since we'll recalc the size
local valueFont = { wxfont = wx.wxFont(sudokuGUI.valueFont.wxfont), size = 8, width = 0, height = 0 }
local possibleFont = { wxfont = wx.wxFont(sudokuGUI.possibleFont.wxfont), size = 6, width = 0, height = 0 }
sudokuGUI.DoGetCellBestSize(dc, cell_width, cell_height,
valueFont, possibleFont)
local function RowOrigin(row) return printrect_x + row*cell_height + row end
local function ColOrigin(col) return printrect_y + col*cell_width + col end
local old_focused_cell_id = sudokuGUI.focused_cell_id -- clear focus
sudokuGUI.focused_cell_id = 0
for row = 1, 9 do
for col = 1, 9 do
local x = ColOrigin(col)
local y = RowOrigin(row)
dc:SetDeviceOrigin(x*dc_scale_x, y*dc_scale_x)
local cell = sudoku.RowColToCell(row, col)
sudokuGUI.PaintCell(dc, cell, cell_width, cell_height, valueFont, possibleFont)
end
end
valueFont.wxfont:delete()
possibleFont.wxfont:delete()
sudokuGUI.focused_cell_id = old_focused_cell_id -- restore focus
dc:SetDeviceOrigin(0, 0)
local borders = { [1]=true, [4]=true, [7]=true, [10]=true }
for i = 1, 10 do
local pen = wx.wxPen(wx.wxBLACK, iff(borders[i], 4, 2), wx.wxSOLID)
dc:SetPen(pen)
pen:delete()
dc:DrawLine(ColOrigin(1), RowOrigin(i), ColOrigin(10), RowOrigin(i))
dc:DrawLine(ColOrigin(i), RowOrigin(1), ColOrigin(i), RowOrigin(10))
end
return true
end
end
function sudokuGUI.Print()
local printDialogData = wx.wxPrintDialogData(sudokuGUI.printData)
local printer = wx.wxPrinter(printDialogData)
local luaPrintout = wx.wxLuaPrintout("wxLuaSudoku Printout")
sudokuGUI.ConnectPrintEvents(luaPrintout)
if printer:Print(sudokuGUI.frame, luaPrintout, true) == false then
if printer:GetLastError() == wx.wxPRINTER_ERROR then
wx.wxMessageBox("There was a problem printing.\n"..
"Perhaps your current printer is not setup correctly?",
"wxLuaSudoku Printout",
wx.wxOK, sudokuGUI.frame)
end
else
sudokuGUI.printData = printer:GetPrintDialogData():GetPrintData():Copy()
end
end
function sudokuGUI.PrintPreview()
luaPrintout = wx.wxLuaPrintout("wxLuaSudoku Print Preview")
luaPrintPrintout = wx.wxLuaPrintout("wxLuaSudoku Printout")
sudokuGUI.ConnectPrintEvents(luaPrintout)
sudokuGUI.ConnectPrintEvents(luaPrintPrintout)
local printDialogData = wx.wxPrintDialogData(sudokuGUI.printData):GetPrintData()
local preview = wx.wxPrintPreview(luaPrintout, luaPrintPrintout, printDialogData)
local result = preview:Ok()
if result == false then
wx.wxMessageBox("There was a problem previewing.\n"..
"Perhaps your current printer is not setup correctly?",
"wxLuaSudoku print preview error",
wx.wxOK, sudokuGUI.frame)
else
local previewFrame = wx.wxPreviewFrame(preview, sudokuGUI.frame,
"wxLuaSudoku print preview",
wx.wxDefaultPosition, wx.wxSize(600, 600))
previewFrame:Connect(wx.wxEVT_CLOSE_WINDOW,
function (event)
previewFrame:Destroy()
event:Skip()
end )
previewFrame:Centre(wx.wxBOTH)
previewFrame:Initialize()
previewFrame:Show(true)
end
end
function sudokuGUI.PrintSetup() -- FIXME DEPRICATED IN WXWIDGETS?
local printDialogData = wx.wxPrintDialogDataFromPrintData(sudokuGUI.printData)
local printerDialog = wx.wxPrintDialog(sudokuGUI.frame, printDialogData)
printerDialog:GetPrintDialogData():SetSetupDialog(true)
printerDialog:ShowModal()
sudokuGUI.printData = printerDialog:GetPrintDialogData():GetPrintData():Copy()
--printerDialog:Destroy()
end
function sudokuGUI.PageSetup()
sudokuGUI.printData = sudokuGUI.pageSetupData:GetPrintData():Copy()
local pageSetupDialog = wx.wxPageSetupDialog(sudokuGUI.frame, sudokuGUI.pageSetupData)
pageSetupDialog:ShowModal()
sudokuGUI.printData = pageSetupDialog:GetPageSetupDialogData():GetPrintData():Copy()
sudokuGUI.pageSetupData = pageSetupDialog:GetPageSetupDialogData():Copy()
--pageSetupDialog:Destroy()
end
-- ----------------------------------------------------------------------------
-- Set the currently focused window, refresh new and old
function sudokuGUI.SetFocusWindow(cell)
local last_id = sudokuGUI.focused_cell_id
sudokuGUI.focused_cell_id = iff((cell>=1) and (cell<=81), cell, 0)
if sudokuGUI.cellWindows[last_id] then
sudokuGUI.cellWindows[last_id]:Refresh()
end
if sudokuGUI.cellWindows[cell] then
sudokuGUI.cellWindows[cell]:Refresh()
end
end
-- ----------------------------------------------------------------------------
function sudokuGUI.OnKeyUpCellWindow(event)
event:Skip()
-- we don't care who actually got this event, just use the "focused cell"
if (sudokuGUI.focused_cell_id < 1) then return end
local key = event:GetKeyCode()
if (sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) == true) and
(key == wx.WXK_SHIFT) then
sudokuGUI.cellWindows[sudokuGUI.focused_cell_id]:Refresh(false)
return
end
end
-- Left down click handler for the cell windows, hide the cell editor
function sudokuGUI.OnKeyDownCellWindow(event)
event:Skip()
-- we don't care who actually got this event, just use the "focused cell"
if (sudokuGUI.focused_cell_id < 1) then return end
local key = event:GetKeyCode()
if (sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) == true) and
(key == wx.WXK_SHIFT) then
sudokuGUI.cellWindows[sudokuGUI.focused_cell_id]:Refresh(false)
end
if event:HasModifiers() or event:AltDown() or event:ControlDown() then
sudokuGUI.cellWindows[sudokuGUI.focused_cell_id]:Refresh(false)
return
end
-- clear the current focused window
if (key == wx.WXK_ESCAPE) then
sudokuGUI.SetFocusWindow(0)
return
end
-- the cursor keys move the focus cell
local movefocus =
{
[wx.WXK_LEFT] = -1, [wx.WXK_NUMPAD_LEFT] = -1,
[wx.WXK_UP] = -9, [wx.WXK_NUMPAD_UP] = -9,
[wx.WXK_RIGHT] = 1, [wx.WXK_NUMPAD_RIGHT] = 1,
[wx.WXK_DOWN] = 9, [wx.WXK_NUMPAD_DOWN] = 9,
[wx.WXK_PAGEUP] = -9, [wx.WXK_PRIOR] = -9,
[wx.WXK_PAGEDOWN] = 9, [wx.WXK_NEXT] = 9,
[wx.WXK_NUMPAD_HOME] = -10,
[wx.WXK_NUMPAD_PAGEUP] = -8, [wx.WXK_NUMPAD_PRIOR] = -8,
[wx.WXK_NUMPAD_END] = 8,
[wx.WXK_NUMPAD_PAGEDOWN] = 10, [wx.WXK_NUMPAD_NEXT] = 10,
[wx.WXK_TAB] = 1,
[wx.WXK_RETURN] = 1,
[wx.WXK_NUMPAD_ENTER] = 1
}
if (key == wx.WXK_HOME) then
sudokuGUI.SetFocusWindow(1)
return
elseif (key == wx.WXK_END) then
sudokuGUI.SetFocusWindow(81)
return
elseif movefocus[key] then
local cell = sudokuGUI.focused_cell_id + movefocus[key]
if (cell >= 1) and (cell <= 81) then
sudokuGUI.SetFocusWindow(cell)
end
return
end
-- translate number pad keys to numbers
local numpad =
{
[wx.WXK_NUMPAD0] = 0,
[wx.WXK_NUMPAD1] = 1,
[wx.WXK_NUMPAD2] = 2,
[wx.WXK_NUMPAD3] = 3,
[wx.WXK_NUMPAD4] = 4,
[wx.WXK_NUMPAD5] = 5,
[wx.WXK_NUMPAD6] = 6,
[wx.WXK_NUMPAD7] = 7,
[wx.WXK_NUMPAD8] = 8,
[wx.WXK_NUMPAD9] = 9,
[wx.WXK_DELETE] = 0,
[wx.WXK_BACK] = 0,
[wx.WXK_SPACE] = 0,
[wx.WXK_NUMPAD_INSERT] = 0,
[wx.WXK_NUMPAD_DECIMAL] = 0,
[wx.WXK_NUMPAD_DELETE] = 0,
}
local zero = string.byte("0")
if numpad[key] then key = zero + numpad[key] end
if (key < 32) or (key > 127) then return end -- normal ASCII key
local one = string.byte("1")
local nine = string.byte("9")
if (key >= one) and (key <= nine) then
key = key - one + 1
else
key = 0
end
if event:ShiftDown() and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) then
if (key >= 1) and (key <= 9) then
sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][key] = iff(sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][key], nil, key)
sudokuGUI.UpdateTable()
end
else
sudokuGUI.SetCellValue(sudokuGUI.focused_cell_id, key)
end
end
-- ----------------------------------------------------------------------------
-- Test to see if one of the possible values has been clicked on
function sudokuGUI.HitTestPossibleValue(mx, my)
local cell = sudokuGUI.focused_cell_id
if (cell < 1) or (cell > 81) then return nil end
local w = sudokuGUI.possiblePosCache.width
local h = sudokuGUI.possiblePosCache.height
local rect = wx.wxRect(0, 0, w, h)
for n = 1, 9 do
rect.X = sudokuGUI.possiblePosCache.pos[n].x
rect.Y = sudokuGUI.possiblePosCache.pos[n].y
if rect:Inside(mx, my) then
return n
end
end
return nil
end
-- ----------------------------------------------------------------------------
-- Left down click handler for the cell windows, hide the cell editor
function sudokuGUI.OnLeftClickCellWindow(event)
event:Skip()
local win = event:GetEventObject():DynamicCast("wxWindow")
local winId = win:GetId()
if sudokuGUI.cellTextCtrl then
sudokuGUI.SaveCellTextCtrlValue()
sudokuGUI.DestroyCellTextCtrl()
end
sudokuGUI.SetFocusWindow(winId)
if event:ShiftDown() and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) then
local p = sudokuGUI.HitTestPossibleValue(event:GetX(), event:GetY())
if p then
sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][p] = iff(sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][p], nil, p)
sudokuGUI.cellWindows[winId]:Refresh(false)
end
end
end
-- Left double click handler for the cell windows, hide old, show new cell editor
function sudokuGUI.OnLeftDClickCellWindow(event)
event:Skip()
local win = event:GetEventObject():DynamicCast("wxWindow")
local winId = win:GetId()
local winWidth, winHeight = win:GetSizeWH()
if event:ShiftDown() then return end
if sudokuGUI.cellTextCtrl then
if sudokuGUI.cellTextCtrl:GetId() == winId then
sudokuGUI.cellTextCtrl:Show(true)
return
end
sudokuGUI.SaveCellTextCtrlValue()
sudokuGUI.DestroyCellTextCtrl()
end
local is_creating = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE)
local value, is_init = sudokuGUI.GetCellValueString(winId)
if is_init and (not is_creating) then return end
sudokuGUI.cellTextCtrl = wx.wxTextCtrl(win, winId, value,
wx.wxPoint(0, 0), wx.wxSize(winWidth, winHeight),
wx.wxTE_PROCESS_ENTER+wx.wxTE_CENTRE)
sudokuGUI.cellTextCtrl:SetFont(sudokuGUI.valueFont.wxfont)
sudokuGUI.cellTextCtrl:SetMaxLength(1)
--local valid = wx.wxTextValidator(wx.wxFILTER_INCLUDE_LIST)
--valid:SetIncludeList(
--cellTextCtrl:SetValidator(valid)
sudokuGUI.cellTextCtrl:Connect(winId, wx.wxEVT_COMMAND_TEXT_ENTER,
function (event)
local win = event:GetEventObject():DynamicCast("wxWindow")
sudokuGUI.SaveCellTextCtrlValue()
win:Show(false) -- just hide it, we'll destroy it later
end)
sudokuGUI.cellTextCtrl:Connect(wx.wxEVT_CHAR,
function (event)
if (event:GetKeyCode() == wx.WXK_ESCAPE) then
sudokuGUI.cellTextCtrl:Show(false)
sudokuGUI.cellTextCtrl:SetValue("")
end
event:Skip()
end)
end
function sudokuGUI.DestroyCellTextCtrl()
if sudokuGUI.cellTextCtrl then
sudokuGUI.cellTextCtrl:Show(false)
sudokuGUI.cellTextCtrl:Destroy()
sudokuGUI.cellTextCtrl = nil
end
end
-- Save the value of the text editor back to the grid
function sudokuGUI.SaveCellTextCtrlValue()
if not sudokuGUI.cellTextCtrl then return end
local value = sudokuGUI.cellTextCtrl:GetValue()
local cell = sudokuGUI.cellTextCtrl:GetId()
sudokuGUI.SetCellValue(cell, value)
end
-- ----------------------------------------------------------------------------
-- Set the value of the cell from the value string
function sudokuGUI.SetCellValue(cell, value)
-- fix the value to something reasonable
if type(value) == "string" then
if (value == "") or (value == " ") or (value == "0") then
value = 0
elseif (string.len(value) ~= 1) or (not string.find("123456789", value)) then
return
else
value = tonumber(value)
end
end
-- if value is still bad, just exit
if not ((value == 0) or sudoku.IsValidValueN(value)) then return end
local is_creating = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE)
local row, col = sudoku.CellToRowCol(cell)
local is_init = sudoku.HasCellValue(sudokuGUI.sudokuTables[1], cell)
if is_creating then
if sudoku.GetCellValue(sudokuGUI.sudokuTables[1], cell) ~= value then
-- add the value to all the tables since it's an init value
for n = 1, TableCount(sudokuGUI.sudokuTables) do
sudoku.SetValue(sudokuGUI.sudokuTables[n], row, col, value)
sudoku.UpdateTable(sudokuGUI.sudokuTables[n])
end
-- refresh all in case it's invalid also if not at 1st table update possible
sudokuGUI.UpdateTable(true)
sudokuGUI.sudokuSolnTable = nil -- don't know anymore
end
else
local s = sudokuGUI.GetCurrentTable()
if (not is_init) and (sudoku.GetCellValue(s, cell) ~= value) then
local s = TableCopy(s)
sudoku.SetValue(s, row, col, value)
sudokuGUI.AddTable(s)
end
end
sudokuGUI.UpdateGUI()
end
-- ----------------------------------------------------------------------------
-- Get the initial sudoku table
function sudokuGUI.GetInitTable()
return sudokuGUI.sudokuTables[1]
end
-- Set the initial sudoku table, clearing all others
function sudokuGUI.SetInitTable(sudokuTable, solnTable)
sudokuGUI.sudokuSolnTable = solnTable
sudokuGUI.sudokuTables_pos = 1
sudokuGUI.sudokuTables = {}
table.insert(sudokuGUI.sudokuTables, sudokuTable)
sudokuGUI.UpdateTable() -- resets possible and refreshes too
end
-- ----------------------------------------------------------------------------
-- Get the current sudoku table to use
function sudokuGUI.GetCurrentTable()
return sudokuGUI.sudokuTables[sudokuGUI.sudokuTables_pos]
end
-- Set the current sudoku table to this table
function sudokuGUI.SetCurrentTable(sudokuTable)
sudokuGUI.sudokuTables[sudokuGUI.sudokuTables_pos] = sudokuTable
end
-- ----------------------------------------------------------------------------
-- Add a sudoku table to the list of tables, removing any past the current position,
-- find possible, and refresh
function sudokuGUI.AddTable(sudokuTable)
while TableCount(sudokuGUI.sudokuTables) > sudokuGUI.sudokuTables_pos do
table.remove(sudokuGUI.sudokuTables)
end
-- clear calculated values to save memory
for n = 2, sudokuGUI.sudokuTables_pos do
sudokuGUI.sudokuTables[n].row_values = {}
sudokuGUI.sudokuTables[n].col_values = {}
sudokuGUI.sudokuTables[n].block_values = {}
sudokuGUI.sudokuTables[n].possible = {}
sudokuGUI.sudokuTables[n].invalid = {}
end
table.insert(sudokuGUI.sudokuTables, sudokuTable)
sudokuGUI.sudokuTables_pos = sudokuGUI.sudokuTables_pos + 1
sudokuGUI.UpdateTable()
end
-- ----------------------------------------------------------------------------
-- Get the value of the cell as a printable string
function sudokuGUI.GetCellValueString(cell)
local value = ""
if sudoku.HasCellValue(sudokuGUI.sudokuTables[1], cell) then
return tostring(sudoku.GetCellValue(sudokuGUI.sudokuTables[1], cell)), true
elseif sudoku.HasCellValue(sudokuGUI.GetCurrentTable(), cell) then
value = tostring(sudoku.GetCellValue(sudokuGUI.GetCurrentTable(), cell))
end
return value, false
end
-- ----------------------------------------------------------------------------
-- refresh all the grid cells
function sudokuGUI.Refresh()
for i = 1, 81 do
if sudokuGUI.cellWindows[i] then
sudokuGUI.cellWindows[i]:Refresh(false)
end
end
sudokuGUI.UpdateGUI()
end
-- ----------------------------------------------------------------------------
-- Create a new empty puzzle
function sudokuGUI.NewPuzzle()
local ret = wx.wxMessageBox("Clear all the values in the current puzzle and start anew?\n"..
"Use 'Create' to enter the initial values.",
"wxLuaSudoku - New puzzle?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxOK then
sudokuGUI.SetInitTable(sudoku.CreateTable(), nil)
end
end
-- ----------------------------------------------------------------------------
-- Create a puzzle by hand
function sudokuGUI.CreatePuzzle(init)
local enableIds =
{
sudokuGUI.ID_GENERATE,
sudokuGUI.ID_OPEN,
sudokuGUI.ID_SAVEAS,
sudokuGUI.ID_RESET,
sudokuGUI.ID_UNDO,
sudokuGUI.ID_REDO,
sudokuGUI.ID_SOLVE_SCANSINGLES,
sudokuGUI.ID_SOLVE_SCANROWS,
sudokuGUI.ID_SOLVE_SCANCOLS,
sudokuGUI.ID_SOLVE_SCANBLOCKS,
sudokuGUI.ID_SOLVE_SCANNING,
sudokuGUI.ID_SOLVE_BRUTEFORCE
}
sudokuGUI.CheckMenuItem(sudokuGUI.ID_CREATE, init)
if init then
local ret = wx.wxMessageBox(
"Enter values in the cells to initialize the puzzle with.\n"..
"Previous cell values will be overwritten.\n"..
"Don't forget to uncheck 'Create' before playing.",
"wxLuaSudoku - Initialize puzzle?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxCANCEL then
sudokuGUI.CheckMenuItem(sudokuGUI.ID_CREATE, false)
return
end
else
sudokuGUI.sudokuSolnTable = nil -- reset to unknown
if not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid) then
-- try to make them correct the puzzle
local ret = wx.wxMessageBox(
"The initial puzzle you've created has invalid values.\n"..
"Press 'Ok' to correct them before continuing.\n"..
"If you press 'Cancel' showing mistakes will be disabled and "..
"don't blame me if things don't work out for you.",
"wxLuaSudoku - Invalid initial puzzle!",
wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR,
sudokuGUI.frame )
if ret == wx.wxOK then
sudokuGUI.CheckMenuItem(sudokuGUI.ID_CREATE, true)
init = true
end
else --if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) then
sudokuGUI.sudokuSolnTable = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable())
end
if (not sudokuGUI.sudokuSolnTable) and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) then
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_MISTAKES, false)
end
end
for n, id in pairs(enableIds) do
sudokuGUI.frame:GetMenuBar():Enable(id, not init)
sudokuGUI.frame:GetToolBar():EnableTool(id, not init)
end
sudokuGUI.UpdateTable()
end
-- ----------------------------------------------------------------------------
-- Generate a new puzzle automatically
function sudokuGUI.GeneratePuzzle()
local keep = wx.wxGetNumberFromUser("Set the difficulty of the new puzzle by clearing cells.\n"..
"Note: The minimum number of cells to show for a unique puzzle is 17.",
"Number of cell values to show",
"wxLuaSudoku - Generate puzzle?",
sudokuGUI.difficulty, 1, 81,
sudokuGUI.frame)
if keep < 1 then return end -- canceled
sudokuGUI.difficulty = keep
local solve_progress = 0
local start_time = os.time()
local last_time = start_time
local solve_ok = true
local msg_idx = 1
local progressDialog = wx.wxProgressDialog("wxLuaSudoku - Generating...",
string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[1], 0, 0),
1000, sudokuGUI.frame,
wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME)
-- define handler function here so it'll work w/o gui
function sudoku.GeneratePuzzleHook(count, cell)
if solve_ok == false then return false end -- canceled
solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1)
if solve_progress%10 ~= 0 then return true end
if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then
msg_idx = msg_idx + 1
last_time = os.time()
end
local msg = string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[msg_idx], count, cell)
solve_ok = progressDialog:Update(solve_progress, msg)
return solve_ok
end
local s, count = sudoku.GeneratePuzzle()
progressDialog:Destroy()
if not s then return end
-- have complete puzzle, now remove cells
local diff_count = 0
local diff_i = 0
local diff_cell = 0
local diff_open = 0
local diff_trial = 0
-- define handler function here so it'll work w/o gui
function sudoku.GeneratePuzzleDifficultyHook(count, i, cell, open_cells, trial)
diff_count = count
diff_i = i
diff_cell = cell
diff_open = open_cells
diff_trial = trial
if solve_ok == false then return false end -- canceled
if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then
msg_idx = msg_idx + 1
last_time = os.time()
end
local msg = string.format("%s\nTrial %d, Iteration # %d, current cell %d, cells to go %d, available cells %d ", sudokuGUI.sayings[msg_idx], trial, count, cell, 81-keep-i, open_cells)
solve_ok = progressDialog:Update(i, msg)
return solve_ok
end
-- hook into brute force solver to update the generate puzzle progress dialog
function sudoku.SolveBruteForceHook(guesses, cell)
solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1)
if solve_progress%10 ~= 0 then return true end
return sudoku.GeneratePuzzleDifficultyHook(diff_count, diff_i, diff_cell, diff_open, diff_trial)
end
local ensure_unique = true
while 1 do
diff_count = 0
diff_i = 0
diff_cell = 0
diff_open = 0
diff_trial = 0
solve_progress = 0
start_time = os.time()
last_time = start_time
solve_ok = true
msg_idx = 1
local caption = "wxLuaSudoku - Ensuring unique solution..."
if ensure_unique == false then
caption = "wxLuaSudoku - Removing values randomly..."
end
progressDialog = wx.wxProgressDialog(caption,
string.format("%s\nTrial %d, Iteration # %d, current cell %d, cells to go %d, available cells %d ", sudokuGUI.sayings[msg_idx], 0, count, 0, 81, 0),
81 - sudokuGUI.difficulty + 1, sudokuGUI.frame,
wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME)
local s1 = sudoku.GeneratePuzzleDifficulty(TableCopy(s), sudokuGUI.difficulty, ensure_unique)
progressDialog:Destroy()
if s1 then
if ensure_unique then
sudokuGUI.SetInitTable(s1, TableCopy(s))
else
-- verify the puzzle anyway to let them know the status
local s2 = sudokuGUI.VerifyUniquePuzzle(s1)
sudokuGUI.SetInitTable(s1, s2)
end
break
else
local ret = wx.wxMessageBox("The puzzle was not fully generated. "..
"Press 'Ok' to randomly remove cell values which may or may not "..
"yield a unique puzzle or 'Cancel' to abort",
"wxLuaSudoku - Unfinished generation",
wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR,
sudokuGUI.frame)
if ret == wx.wxOK then
ensure_unique = false
else
break
end
end
end
end
-- ----------------------------------------------------------------------------
-- Open a puzzle from a file
function sudokuGUI.OpenPuzzle()
local fileDialog = wx.wxFileDialog(sudokuGUI.frame, "Open file",
sudokuGUI.filePath, sudokuGUI.fileName,
"wxLuaSudoku files (*.sudoku)|*.sudoku|All files (*)|*",
wx.wxOPEN + wx.wxFILE_MUST_EXIST)
if fileDialog:ShowModal() == wx.wxID_OK then
local fileName = fileDialog:GetPath()
local fn = wx.wxFileName(fileName)
sudokuGUI.filePath = fn:GetPath()
sudokuGUI.fileName = fn:GetFullName()
local s, msg = sudoku.Open(fileName)
if s then
sudokuGUI.frame:SetTitle("wxLuaSudoku - "..sudokuGUI.fileName)
sudokuGUI.SetInitTable(s, nil)
if not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid) then
-- make them correct the puzzle
local ret = wx.wxMessageBox(
"The puzzle you've opened has invalid values.\n"..
"Press 'Ok' to correct them using 'Create' before continuing "..
"otherwise 'Cancel' to ignore them.",
"wxLuaSudoku - Invalid puzzle",
wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR,
sudokuGUI.frame )
if ret == wx.wxOK then
sudokuGUI.CreatePuzzle(true)
end
else --if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) then
sudokuGUI.sudokuSolnTable = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable())
end
else
wx.wxMessageBox( msg,
"wxLuaSudoku - Open file error",
wx.wxOK + wx.wxICON_ERROR,
sudokuGUI.frame )
end
end
fileDialog:Destroy()
end
-- ----------------------------------------------------------------------------
-- Save the puzzle to a file
function sudokuGUI.SaveAsPuzzle()
local fileDialog = wx.wxFileDialog(sudokuGUI.frame, "Save puzzle",
sudokuGUI.filePath, sudokuGUI.fileName,
"wxLuaSudoku files (*.sudoku)|*.sudoku|All files (*)|*",
wx.wxSAVE + wx.wxOVERWRITE_PROMPT)
local result = false
if fileDialog:ShowModal() == wx.wxID_OK then
local fileName = fileDialog:GetPath()
local fn = wx.wxFileName(fileName)
sudokuGUI.filePath = fn:GetPath()
sudokuGUI.fileName = fn:GetFullName()
result = sudoku.Save(sudokuGUI.GetCurrentTable(), fileName)
if result then
sudokuGUI.frame:SetTitle("wxLuaSudoku - "..sudokuGUI.fileName)
else
wx.wxMessageBox( "Unable to save file\n'"..fileName.."'",
"wxLuaSudoku - Save file error",
wx.wxOK + wx.wxICON_ERROR,
sudokuGUI.frame )
end
end
fileDialog:Destroy()
return result
end
-- ----------------------------------------------------------------------------
function sudokuGUI.Undo()
if sudokuGUI.sudokuTables_pos > 1 then
sudokuGUI.sudokuTables_pos = sudokuGUI.sudokuTables_pos - 1
sudokuGUI.UpdateTable(true)
end
end
function sudokuGUI.Redo()
if sudokuGUI.sudokuTables_pos < TableCount(sudokuGUI.sudokuTables) then
sudokuGUI.sudokuTables_pos = sudokuGUI.sudokuTables_pos + 1
sudokuGUI.UpdateTable(true)
end
end
-- ----------------------------------------------------------------------------
-- Try to fix the invalid cells if possible
function sudokuGUI.FixInvalid(sudokuTable, show_dialog)
local s = TableCopy(sudokuTable)
local solnTable = sudokuGUI.sudokuSolnTable
if solnTable and TableIsEmpty(solnTable.invalid) then
for cell = 1, 81 do
if sudoku.HasCellValue(s, cell) then
local current_value = sudoku.GetCellValue(s, cell)
local correct_value = sudoku.GetCellValue(solnTable, cell)
if current_value ~= correct_value then
sudoku.SetCellValue(s, cell, correct_value)
end
end
end
sudoku.UpdateTable(s)
return s
else
if show_dialog then
local msg = "The initial puzzle must be solved first.\n"..
"Would you like me to try to solve it?"
local flags = wx.wxYES_NO
local invalid = true
if solnTable and not TableIsEmpty(solnTable.invalid) then
invalid = true
end
if invalid then
msg = "The initial puzzle has invalid values.\n"..
"Please correct them first using Create."
flags = wx.wxOK
end
local ret = wx.wxMessageBox(msg,
"wxLuaSudoku - Invalid puzzle",
flags + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if not invalid and (ret == wx.wxOK) then
s = sudokuGUI.SolveBruteForce(sudokuGUI.sudokuTables[1])
if not s then
wx.wxMessageBox("Unable to solve or or solving was aborted, giving up.",
"wxLuaSudoku - Invalid puzzle",
wx.wxOK + wx.wxICON_INFORMATION,
sudokuGUI.frame )
return nil
end
sudokuGUI.sudokuSolnTable = s
sudoku.UpdateTable(sudokuGUI.sudokuSolnTable)
return sudokuGUI.FixInvalid(sudokuTable, show_dialog)
else
return nil
end
else
return nil
end
end
end
-- ----------------------------------------------------------------------------
function sudokuGUI.VerifyUniquePuzzle(sudokuTable)
sudoku.CalcInvalidCells(sudokuTable)
local invalid_count = TableCount(sudokuTable.invalid)
if invalid_count > 0 then
local ret = wx.wxMessageBox(
string.format("The initial values of the puzzle are invalid.\n"..
"There are %d cells with duplicate values.\n"..
"Please select Create and fix them before trying to solve.\n", invalid_count),
"wxLuaSudoku - Invalid puzzle",
wx.wxOK + wx.wxICON_ERROR,
sudokuGUI.frame )
return
end
local solve_progress = 0
local start_time = os.time()
local last_time = start_time
local solve_ok = true
local msg_idx = 1
-- define handler function here so it'll work w/o gui
function sudoku.SolveBruteForceHook(guesses, cell)
if solve_ok == false then return false end -- canceled
solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1)
if (solve_progress-1)%10 ~= 0 then return true end
if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then
msg_idx = msg_idx + 1
last_time = os.time()
end
local msg = string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[msg_idx], guesses.current, cell)
solve_ok = progressDialog:Update(solve_progress, msg)
return solve_ok
end
local ret = wx.wxOK
while ret == wx.wxOK do
solve_progress = 0
start_time = os.time()
last_time = start_time
solve_ok = true
msg_idx = 1
progressDialog = wx.wxProgressDialog("wxLuaSudoku - Verifying puzzle...",
string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[1], 0, 0),
1000, sudokuGUI.frame,
wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME)
local s1, s2 = sudoku.IsUniquePuzzle(sudokuTable)
progressDialog:Destroy()
if s1 and (s2 == nil) then
return s1
elseif solve_ok == false then
ret = wx.wxMessageBox("The puzzle was not fully verified and therefore may not have a unique solution or a solution at all.\n"..
"Press 'Ok' to restart checking or 'Cancel' to quit.",
"wxLuaSudoku - Unfinished check",
wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR,
sudokuGUI.frame)
elseif s1 and s2 then
wx.wxMessageBox("The puzzle does not have a unique solution.\n"..
"Use 'Create' to fix the problem, showing mistakes will be disabled.",
"wxLuaSudoku - Nonunique puzzle",
wx.wxOK + wx.wxICON_ERROR,
sudokuGUI.frame)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_MISTAKES, false)
return nil
else
wx.wxMessageBox("The puzzle does not have a solution.\n"..
"Use 'Create' to fix the problem, showing mistakes will be disabled.",
"wxLuaSudoku - Unsolvable puzzle",
wx.wxOK + wx.wxICON_ERROR,
sudokuGUI.frame)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_MISTAKES, false)
return nil
end
end
return nil
end
-- ----------------------------------------------------------------------------
-- Use the scanning method to solve it
function sudokuGUI.SolveScanning()
local s = TableCopy(sudokuGUI.GetCurrentTable())
--[[
local invalid = not TableIsEmpty(s.invalid)
if invalid then
if not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid) then
local ret = wx.wxMessageBox("The initial values in the puzzle are invalid.\n"..
"Please select Create and fix them before trying to solve.\n"..
"Press 'Cancel' to try to solve it anyway.",
"wxLuaSudoku - Invalid puzzle",
wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR,
sudokuGUI.frame )
if ret == wx.wxOK then return end
else
local ret = wx.wxMessageBox("The current puzzle has invalid cell values.\n"..
"Would you like me to try to fix those cells or press cancel to abort solving.",
"wxLuaSudoku - Invalid puzzle",
wx.wxYES_NO + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxCANCEL then
return
elseif ret == wx.wxYES then
local fixedS = sudokuGUI.FixInvalid(s, true)
if fixedS then
sudokuGUI.AddTable(fixedS)
s = TableCopy(sudokuGUI.GetCurrentTable())
else
return
end
end
end
end
]]
local count, changed_cells = sudoku.SolveScan(s)
local changed_count = 0
if changed_cells then
sudokuGUI.AddTable(s)
changed_count = TableCount(changed_cells)
end
local msg = string.format("Scanned rows, cols, and blocks %d times.\n"..
"Found %d new values.\n"..
"You may be able to do better using 'Eliminate groups'", count, changed_count)
wx.wxMessageBox( msg,
"wxLuaSudoku - Finished scanning",
wx.wxOK + wx.wxICON_INFORMATION,
sudokuGUI.frame )
end
-- ----------------------------------------------------------------------------
-- Use the brute force method to solve it
function sudokuGUI.SolveBruteForce(sudokuTable)
local s
if sudokuTable then
s = TableCopy(sudokuTable)
else
s = TableCopy(sudokuGUI.GetCurrentTable())
end
--[[
local invalid = not TableIsEmpty(s.invalid)
if invalid then
if (sudokuTable == nil) and (not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid)) then
wx.wxMessageBox("The initial values in the puzzle are invalid.\n"..
"Please select Create and fix them before trying to solve.",
"wxLuaSudoku - Invalid puzzle",
wx.wxOK + wx.wxICON_ERROR,
sudokuGUI.frame )
return
end
local ret = wx.wxMessageBox("The current puzzle has invalid cell values.\n"..
"Would you like me to try to fix those cells or press cancel to abort solving.",
"wxLuaSudoku - Invalid puzzle",
wx.wxYES_NO + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxCANCEL then
return
elseif ret == wx.wxYES then
local fixedS = sudokuGUI.FixInvalid(s, true)
if fixedS then
sudokuGUI.AddTable(fixedS)
s = TableCopy(sudokuGUI.GetCurrentTable())
else
return
end
end
end
]]
local progressDialog = wx.wxProgressDialog("wxLuaSudoku - Solving...",
string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[1], 0, 0),
1000, sudokuGUI.frame,
wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME)
local solve_progress = 0
local start_time = os.time()
local last_time = start_time
local solve_ok = true
local msg_idx = 1
-- define handler function here so it'll work w/o gui
function sudoku.SolveBruteForceHook(guesses, cell)
if solve_ok == false then return false end -- canceled
solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1)
if (solve_progress-1)%10 ~= 0 then return true end
if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then
msg_idx = msg_idx + 1
last_time = os.time()
end
local msg = string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[msg_idx], guesses.current, cell)
solve_ok = progressDialog:Update(solve_progress, msg)
return solve_ok
end
-- "cheat" a little by using SolveScan to get easy to find values
--local flags = TableCopy(s.flags)
--for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do
-- s.flags[n] = true
--end
--local count, changed_cells = sudoku.SolveScan(s)
local s, g = sudoku.SolveBruteForce(s)
progressDialog:Destroy()
if not s then
if solve_ok then
wx.wxMessageBox("Sorry, no solutions found!",
"wxLuaSudoku - error",
wx.wxOK + wx.wxICON_INFORMATION,
sudokuGUI.frame )
end
elseif not sudokuTable then
--s.flags = flags -- restore flags
sudokuGUI.AddTable(s) -- we solved the current grid
else
--s.flags = flags -- restore flags
return s -- we solved the input grid
end
end
-- ----------------------------------------------------------------------------
-- Reset the grid to the original values
function sudokuGUI.ResetPuzzle(dont_query_user)
dont_query_user = dont_query_user or false
local ret = wx.wxOK
if not dont_query_user then
ret = wx.wxMessageBox("Reset the puzzle to the initial state?",
"wxLuaSudoku - reset puzzle?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
end
if ret == wx.wxCANCEL then
return
else
sudokuGUI.sudokuTables_pos = 1
while TableCount(sudokuGUI.sudokuTables) > 1 do
table.remove(sudokuGUI.sudokuTables, 2)
end
end
sudokuGUI.UpdateTable() -- redo it anyway
end
-- ----------------------------------------------------------------------------
function sudokuGUI.UpdateTable(refresh)
if refresh == nil then refresh = true end
local sudokuTable = sudokuGUI.GetCurrentTable()
sudokuGUI.block_refresh = true
local has_show_flag = false
for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do
local id = n + sudokuGUI.ID_ELIMINATE_NAKEDPAIRS - sudoku.ELIMINATE_FLAG_MIN
sudokuTable.flags[n] = sudokuGUI.IsCheckedMenuItem(id)
local show_id = n + sudokuGUI.ID_SHOW_NAKEDPAIRS - sudoku.ELIMINATE_FLAG_MIN
if (not has_show_flag) and (sudokuGUI.IsCheckedMenuItem(show_id) == true) then
has_show_flag = true
end
end
sudoku.UpdateTable(sudokuTable)
if has_show_flag == true then
-- swap out the possible table temporarily to calc pencil marks
if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) then
local p = sudokuTable.possible
sudokuTable.possible = sudokuGUI.pencilMarks
sudokuGUI.pencilMarksNakedTable, sudokuGUI.pencilMarksHiddenTable = sudoku.FindAllNakedHiddenGroups(sudokuTable, true)
sudokuTable.possible = p
else
sudokuGUI.possNakedTable, sudokuGUI.possHiddenTable = sudoku.FindAllNakedHiddenGroups(sudokuTable, true)
end
end
sudokuGUI.SetCurrentTable(sudokuTable)
sudokuGUI.block_refresh = false
if (refresh == true) then
sudokuGUI.Refresh()
end
sudokuGUI.UpdateGUI()
end
-- ----------------------------------------------------------------------------
function sudokuGUI.UpdateGUI()
local table_count = #sudokuGUI.sudokuTables
local table_pos = sudokuGUI.sudokuTables_pos
sudokuGUI.frame:GetMenuBar():Enable(sudokuGUI.ID_UNDO, table_pos > 1)
sudokuGUI.frame:GetMenuBar():Enable(sudokuGUI.ID_REDO, table_pos < table_count)
sudokuGUI.frame:GetToolBar():EnableTool(sudokuGUI.ID_UNDO, table_pos > 1)
sudokuGUI.frame:GetToolBar():EnableTool(sudokuGUI.ID_REDO, table_pos < table_count)
sudokuGUI.frame:SetStatusText(string.format("Step : %d", table_pos), 1)
end
-- ----------------------------------------------------------------------------
-- The preference pages are in a table so they can be accessed easily
sudokuGUI.PreferencesDialogPageUI = {}
function sudokuGUI.PreferencesDialogPageUI.Create(parent)
local panel = wx.wxPanel(parent, wx.wxID_ANY)
local ID_LISTBOX = 10
local ID_SAMPLE_TEXT = 11
local ID_FONT_BUTTON = 12
local ID_COLOUR_BUTTON = 13
local ID_RESET_BUTTON = 14
local listStrings = -- in same order as the colours
{
"Values",
"Initial values",
"Possible values",
"Invalid values",
"Background",
"Odd background",
"Focused cell",
"Naked pairs",
"Naked triplets",
"Naked quads",
"Hidden pairs",
"Hidden triplets",
"Hidden quads"
}
local listBoxValues = {}
for n = 1, sudokuGUI.COLOUR_MAX do
table.insert(listBoxValues, {colour = wx.wxColour(sudokuGUI.Colours[n])})
end
listBoxValues[sudokuGUI.VALUE_COLOUR].font = wx.wxFont(sudokuGUI.valueFont.wxfont)
listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font = wx.wxFont(sudokuGUI.possibleFont.wxfont)
local reset_fonts = true
-- Create the dialog ------------------------------------------------------
local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL )
local fcFlexSizer = wx.wxFlexGridSizer( 1, 2, 0, 0 )
fcFlexSizer:AddGrowableCol( 0 )
fcFlexSizer:AddGrowableRow( 0 )
local fcListBox = wx.wxListBox( panel, ID_LISTBOX, wx.wxDefaultPosition, wx.wxSize(80,100), listStrings, wx.wxLB_SINGLE )
fcListBox:SetSelection(0)
fcFlexSizer:Add( fcListBox, 0, wx.wxGROW+wx.wxALIGN_CENTER_HORIZONTAL+wx.wxALL, 5 )
local fcBoxSizer = wx.wxBoxSizer( wx.wxVERTICAL )
local sampleWin = wx.wxWindow(panel, ID_SAMPLE_TEXT, wx.wxDefaultPosition, wx.wxSize(140,140))
fcBoxSizer:Add( sampleWin, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 );
local fontButton = wx.wxButton( panel, ID_FONT_BUTTON, "Choose Font", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
fcBoxSizer:Add( fontButton, 0, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL+wx.wxALL, 5 )
local colourButton = wx.wxButton( panel, ID_COLOUR_BUTTON, "Choose Color", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
fcBoxSizer:Add( colourButton, 0, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL+wx.wxALL, 5 )
local resetButton = wx.wxButton( panel, ID_RESET_BUTTON, "Reset Value...", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
fcBoxSizer:Add( resetButton, 0, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL+wx.wxALL, 5 )
fcFlexSizer:Add( fcBoxSizer, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 )
mainSizer:Add( fcFlexSizer, 1, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL, 5 )
panel:SetSizer( mainSizer )
sampleWin:Connect(wx.wxEVT_PAINT,
function (event)
local win = event:GetEventObject():DynamicCast("wxWindow")
local sel = fcListBox:GetSelection() + 1
local width, height = win:GetClientSizeWH()
local dc = wx.wxPaintDC(win)
local function SetFontSize(size, width, height, font)
-- alternate way, but it fails for fonts that can't scale large enough
--local f = wx.wxNullFont:NewSize(wx.wxSize(width, height), font:GetFamily(), font:GetStyle(), font:GetWeight(), font:GetUnderlined(), font:GetFaceName())
--font:SetPointSize(f:GetPointSize())
local font_width = 0
local font_height = 0
while (font_width < width) and (font_height < height) do
font:SetPointSize(size)
dc:SetFont(font)
font_width, font_height = dc:GetTextExtent("5")
size = size + 2
if size > 200 then break end -- oops bad font?
end
font:SetPointSize(size-1)
end
-- clear background
local c = listBoxValues[sudokuGUI.BACKGROUND_COLOUR].colour
if (sel == sudokuGUI.ODD_BACKGROUND_COLOUR) or (sel == sudokuGUI.FOCUS_CELL_COLOUR) then
c = listBoxValues[sel].colour
end
local brush = wx.wxBrush(c, wx.wxSOLID)
dc:SetBrush(brush)
brush:delete()
dc:DrawRectangle(0, 0, width, height)
-- draw possible values
dc:SetTextForeground(listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].colour)
local font = listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font
if reset_fonts then SetFontSize(4, width/4, height/4, font) end
dc:SetFont(font)
local font_width, font_height = dc:GetTextExtent("5")
local pos =
{
[1] = { x = 2, y = 2 },
[3] = { x = width-font_width-2, y = 2 },
[4] = { x = 2, y = (height-font_height)/2-2 },
[6] = { x = width-font_width-2, y = (height-font_height)/2-2 },
[7] = { x = 2, y = height-font_height-2 },
[9] = { x = width-font_width-2, y = height-font_height-2 }
}
dc:SetBrush(wx.wxTRANSPARENT_BRUSH)
local function DrawPossible(idx, n, value, hidden)
dc:DrawText(value, pos[n].x, pos[n].y)
local pen = wx.wxPen(listBoxValues[idx].colour, 1, wx.wxSOLID)
dc:SetPen(pen); pen:delete()
if hidden ~= true then
dc:DrawRectangle(pos[n].x, pos[n].y, font_width, font_height)
else
dc:DrawRoundedRectangle(pos[n].x, pos[n].y, font_width, font_height, 20)
end
end
DrawPossible(sudokuGUI.NAKED_PAIRS_COLOUR, 1, "2")
DrawPossible(sudokuGUI.NAKED_TRIPLETS_COLOUR, 4, "3")
DrawPossible(sudokuGUI.NAKED_QUADS_COLOUR, 7, "4")
DrawPossible(sudokuGUI.HIDDEN_PAIRS_COLOUR, 3, "2", true)
DrawPossible(sudokuGUI.HIDDEN_TRIPLETS_COLOUR, 6, "3", true)
DrawPossible(sudokuGUI.HIDDEN_QUADS_COLOUR, 9, "4", true)
-- draw invalid marker
local pen = wx.wxPen(listBoxValues[sudokuGUI.INVALID_VALUE_COLOUR].colour, 1, wx.wxSOLID)
dc:SetPen(pen); pen:delete()
dc:DrawLine(0, 0, width, height)
-- draw value
if (sel == sudokuGUI.INIT_VALUE_COLOUR) then
dc:SetTextForeground(listBoxValues[sudokuGUI.INIT_VALUE_COLOUR].colour)
else
dc:SetTextForeground(listBoxValues[sudokuGUI.VALUE_COLOUR].colour)
end
local old_font = font
local font = listBoxValues[sudokuGUI.VALUE_COLOUR].font
if reset_fonts then SetFontSize(old_font:GetPointSize(), width-2, height-2, font) end
dc:SetFont(font)
local font_width, font_height = dc:GetTextExtent("9")
dc:DrawText("9", (width-font_width)/2, (height-font_height)/2)
reset_fonts = false
dc:delete()
end)
panel:Connect(ID_LISTBOX, wx.wxEVT_COMMAND_LISTBOX_SELECTED,
function (event)
local sel = event:GetSelection() + 1
panel:FindWindow(ID_FONT_BUTTON):Enable(listBoxValues[sel].font ~= nil)
colourButton:SetForegroundColour(listBoxValues[sel].colour)
sampleWin:Refresh(false)
end)
panel:Connect(ID_FONT_BUTTON, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function (event)
local sel = fcListBox:GetSelection() + 1
local f = listBoxValues[sel].font
f = wx.wxGetFontFromUser(panel, f)
if f:Ok() then
listBoxValues[sel].font:delete()
listBoxValues[sel].font = f
reset_fonts = true
else
f:delete()
end
sampleWin:Refresh(false)
end)
panel:Connect(ID_COLOUR_BUTTON, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function (event)
local sel = fcListBox:GetSelection() + 1
local c = listBoxValues[sel].colour
c = wx.wxGetColourFromUser(panel, c)
if c:Ok() then
listBoxValues[sel].colour:delete()
listBoxValues[sel].colour = c
colourButton:SetForegroundColour(c)
else
c:delete()
end
sampleWin:Refresh(false)
end)
panel:Connect(ID_RESET_BUTTON, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function (event)
local sel = fcListBox:GetSelection() + 1
local ret = wx.wxMessageBox(
"Press 'Yes' to reset all the colors and fonts or 'No' to reset only just the selected item.",
"wxLuaSudoku - Reset colors or fonts?",
wx.wxYES_NO + wx.wxCANCEL + wx.wxICON_INFORMATION,
panel )
if ret == wx.wxYES then
for n = 1, sudokuGUI.COLOUR_MAX do
listBoxValues[n].colour:delete()
listBoxValues[n].colour = wx.wxColour(sudokuGUI.Colours_[n])
end
listBoxValues[sudokuGUI.VALUE_COLOUR].font:delete()
listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font:delete()
listBoxValues[sudokuGUI.VALUE_COLOUR].font = wx.wxFont(sudokuGUI.valueFont_wxfont_)
listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font = wx.wxFont(sudokuGUI.possibleFont_wxfont_)
elseif ret == wx.wxNO then
listBoxValues[sel].colour:delete()
listBoxValues[sel].colour = wx.wxColour(sudokuGUI.Colours_[sel])
if (sel == sudokuGUI.VALUE_COLOUR) then
listBoxValues[sel].font:delete()
listBoxValues[sel].font = wx.wxFont(sudokuGUI.valueFont_wxfont_)
elseif (sel == sudokuGUI.POSS_VALUE_COLOUR) then
listBoxValues[sel].font:delete()
listBoxValues[sel].font = wx.wxFont(sudokuGUI.possibleFont_wxfont_)
end
end
colourButton:SetForegroundColour(listBoxValues[sel].colour)
reset_fonts = true
sampleWin:Refresh(false)
end)
function sudokuGUI.PreferencesDialogPageUI.Apply()
for n = 1, sudokuGUI.COLOUR_MAX do
sudokuGUI.Colours[n]:delete()
sudokuGUI.Colours[n] = wx.wxColour(listBoxValues[n].colour)
end
-- copy the fonts since when applied their size will change
sudokuGUI.valueFont.wxfont:delete()
sudokuGUI.valueFont.wxfont = wx.wxFont(listBoxValues[sudokuGUI.VALUE_COLOUR].font)
sudokuGUI.possibleFont.wxfont:delete()
sudokuGUI.possibleFont.wxfont = wx.wxFont(listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font)
sudokuGUI.valueFont_cache = {} -- clear cache so GetCellBestSize recreates it
sudokuGUI.possibleFont_cache = {}
for winID = 1, 81 do
if sudokuGUI.IsOddBlockCell(winID) then
sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR])
else
sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR])
end
end
local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH()
sudokuGUI.GetCellBestSize(width, height)
sudokuGUI.Refresh()
end
function sudokuGUI.PreferencesDialogPageUI.Destroy()
for n = 1, sudokuGUI.COLOUR_MAX do
listBoxValues[n].colour:delete()
if listBoxValues[n].font then listBoxValues[n].font:delete() end
end
end
return panel
end
function sudokuGUI.CheckListBoxCheck(clBox, n_start, n_end, check)
for n = n_start, n_end do
clBox:Check(n, check)
end
end
function sudokuGUI.CheckListBoxIsChecked(clBox, n_start, n_end)
for n = n_start, n_end do
if not clBox:IsChecked(n) then return false end
end
return true
end
sudokuGUI.PreferencesDialogPageShow = {}
function sudokuGUI.PreferencesDialogPageShow.Create(parent)
local panel = wx.wxPanel(parent, wx.wxID_ANY)
local ID_LISTBOX = 10
local listStrings =
{
"All naked groups",
"All hidden groups",
"Naked pairs",
"Naked triplets",
"Naked quads",
"Hidden pairs",
"Hidden triplets",
"Hidden quads"
}
local listBoxValues =
{
sudokuGUI.ID_SHOW_NAKED,
sudokuGUI.ID_SHOW_HIDDEN,
sudokuGUI.ID_SHOW_NAKEDPAIRS,
sudokuGUI.ID_SHOW_NAKEDTRIPLETS,
sudokuGUI.ID_SHOW_NAKEDQUADS,
sudokuGUI.ID_SHOW_HIDDENPAIRS,
sudokuGUI.ID_SHOW_HIDDENTRIPLETS,
sudokuGUI.ID_SHOW_HIDDENQUADS
}
-- Create the dialog ------------------------------------------------------
local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL )
local showListBox = wx.wxCheckListBox( panel, ID_LISTBOX, wx.wxDefaultPosition, wx.wxSize(80,100), listStrings, wx.wxLB_SINGLE )
mainSizer:Add( showListBox, 1, wx.wxGROW+wx.wxALIGN_CENTER_HORIZONTAL+wx.wxALL, 5 )
panel:SetSizer( mainSizer )
for n = 1, showListBox:GetCount() do
showListBox:Check(n-1, sudokuGUI.IsCheckedMenuItem(listBoxValues[n]))
end
panel:Connect(ID_LISTBOX, wx.wxEVT_COMMAND_CHECKLISTBOX_TOGGLED,
function (event)
local sel = event:GetSelection()
local checked = showListBox:IsChecked(sel)
local id = listBoxValues[sel+1]
if id == sudokuGUI.ID_SHOW_NAKED then
sudokuGUI.CheckListBoxCheck(showListBox, 2, 4, checked)
elseif id == sudokuGUI.ID_SHOW_HIDDEN then
sudokuGUI.CheckListBoxCheck(showListBox, 5, 7, checked)
else
showListBox:Check(0, sudokuGUI.CheckListBoxIsChecked(showListBox, 2, 4))
showListBox:Check(1, sudokuGUI.CheckListBoxIsChecked(showListBox, 5, 7))
end
end)
function sudokuGUI.PreferencesDialogPageShow.Apply()
for n = 1, showListBox:GetCount() do
sudokuGUI.CheckMenuItem(listBoxValues[n], showListBox:IsChecked(n-1))
end
sudokuGUI.UpdateTable()
end
function sudokuGUI.PreferencesDialogPageShow.Destroy()
end
return panel
end
sudokuGUI.PreferencesDialogPageSolve = {}
function sudokuGUI.PreferencesDialogPageSolve.Create(parent)
local panel = wx.wxPanel(parent, wx.wxID_ANY)
local ID_LISTBOX = 10
local listStrings =
{
"All naked groups",
"All hidden groups",
"Naked pairs",
"Naked triplets",
"Naked quads",
"Hidden pairs",
"Hidden triplets",
"Hidden quads"
}
local listBoxValues =
{
sudokuGUI.ID_ELIMINATE_NAKED,
sudokuGUI.ID_ELIMINATE_HIDDEN,
sudokuGUI.ID_ELIMINATE_NAKEDPAIRS,
sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS,
sudokuGUI.ID_ELIMINATE_NAKEDQUADS,
sudokuGUI.ID_ELIMINATE_HIDDENPAIRS,
sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS,
sudokuGUI.ID_ELIMINATE_HIDDENQUADS
}
-- Create the dialog ------------------------------------------------------
local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL )
local showListBox = wx.wxCheckListBox( panel, ID_LISTBOX, wx.wxDefaultPosition, wx.wxSize(80,100), listStrings, wx.wxLB_SINGLE )
mainSizer:Add( showListBox, 1, wx.wxGROW+wx.wxALIGN_CENTER_HORIZONTAL+wx.wxALL, 5 )
panel:SetSizer( mainSizer )
for n = 1, showListBox:GetCount() do
showListBox:Check(n-1, sudokuGUI.IsCheckedMenuItem(listBoxValues[n]))
end
panel:Connect(ID_LISTBOX, wx.wxEVT_COMMAND_CHECKLISTBOX_TOGGLED,
function (event)
local sel = event:GetSelection()
local checked = showListBox:IsChecked(sel)
local id = listBoxValues[sel+1]
if id == sudokuGUI.ID_ELIMINATE_NAKED then
sudokuGUI.CheckListBoxCheck(showListBox, 2, 4, checked)
elseif id == sudokuGUI.ID_ELIMINATE_HIDDEN then
sudokuGUI.CheckListBoxCheck(showListBox, 5, 7, checked)
else
showListBox:Check(0, sudokuGUI.CheckListBoxIsChecked(showListBox, 2, 4))
showListBox:Check(1, sudokuGUI.CheckListBoxIsChecked(showListBox, 5, 7))
end
end)
function sudokuGUI.PreferencesDialogPageSolve.Apply()
for n = 1, showListBox:GetCount() do
sudokuGUI.CheckMenuItem(listBoxValues[n], showListBox:IsChecked(n-1))
end
sudokuGUI.UpdateTable()
end
function sudokuGUI.PreferencesDialogPageSolve.Destroy()
end
return panel
end
function sudokuGUI.PreferencesDialog()
local dialog = wx.wxDialog(sudokuGUI.frame, wx.wxID_ANY,
"wxLuaSudoku - Preferences",
wx.wxDefaultPosition, wx.wxDefaultSize,
wx.wxDEFAULT_DIALOG_STYLE+wx.wxRESIZE_BORDER)
local panel = wx.wxPanel(dialog, wx.wxID_ANY)
local notebook = wx.wxNotebook(panel, wx.wxID_ANY)
local notebookPages = {}
local page1 = sudokuGUI.PreferencesDialogPageUI.Create(notebook)
notebook:AddPage(page1, "Fonts and Colors", true)
table.insert(notebookPages, sudokuGUI.PreferencesDialogPageUI)
local page2 = sudokuGUI.PreferencesDialogPageShow.Create(notebook)
notebook:AddPage(page2, "Mark groups", false)
table.insert(notebookPages, sudokuGUI.PreferencesDialogPageShow)
local page3 = sudokuGUI.PreferencesDialogPageSolve.Create(notebook)
notebook:AddPage(page3, "Eliminate groups", false)
table.insert(notebookPages, sudokuGUI.PreferencesDialogPageSolve)
local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL )
local buttonSizer = wx.wxBoxSizer( wx.wxHORIZONTAL )
local okButton = wx.wxButton( panel, wx.wxID_OK, "&OK", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
buttonSizer:Add( okButton, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 )
local cancelButton = wx.wxButton( panel, wx.wxID_CANCEL, "&Cancel", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
buttonSizer:Add( cancelButton, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 )
local applyButton = wx.wxButton( panel, wx.wxID_APPLY, "&Apply", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
buttonSizer:Add( applyButton, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 )
mainSizer:Add( notebook, 1, wx.wxGROW+wx.wxALIGN_CENTER, 0 )
mainSizer:Add( buttonSizer, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 )
panel:SetSizer( mainSizer )
mainSizer:SetSizeHints( dialog )
dialog:Connect(wx.wxID_APPLY, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function (event)
--local sel = notebook:GetSelection()
--if sel >= 0 then notebookPages[sel+1].Apply() end
for n = 1, #notebookPages do
notebookPages[n].Apply()
end
end)
dialog:Connect(wx.wxID_OK, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function (event)
for n = 1, #notebookPages do
notebookPages[n].Apply()
notebookPages[n].Destroy()
end
event:Skip() -- wxDialog will cancel automatically
end)
dialog:Connect(wx.wxID_CANCEL, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function (event)
for n = 1, #notebookPages do
notebookPages[n].Destroy()
end
event:Skip() -- wxDialog will cancel automatically
end)
dialog:ShowModal()
end
-- ----------------------------------------------------------------------------
function sudokuGUI.ConfigSave(save_prefs)
if not sudokuGUI.config then
sudokuGUI.config = wx.wxFileConfig("wxLuaSudoku", "wxLua")
end
if not sudokuGUI.config then return end
-- write the frame position so we can restore it
local x, y = sudokuGUI.frame:GetPositionXY()
local w, h = sudokuGUI.frame:GetClientSizeWH()
local max = booltoint(sudokuGUI.frame:IsMaximized())
sudokuGUI.config:Write("wxLuaSudoku/Frame", string.format("x:%d y:%d w:%d h:%d maximized:%d", x, y, w, h, max))
if not save_prefs then return end
if sudokuGUI.query_save_prefs then
local ret = wx.wxMessageBox(
"Preferences are stored in an ini file which you may delete:\n"..
"MSW : Documents and Settings\\user\\wxLuaSudoku.ini\n"..
"Unix : /home/user/.wxLuaSudoku",
"wxLuaSudoku - Save preferences?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxCANCEL then
return
end
sudokuGUI.query_save_prefs = false
end
if sudokuGUI.config then
sudokuGUI.ConfigReadWrite(false, sudokuGUI.config)
sudokuGUI.config:Flush(true)
end
end
function sudokuGUI.ConfigLoad()
if not sudokuGUI.config then
sudokuGUI.config = wx.wxFileConfig("wxLuaSudoku", "wxLua")
end
if sudokuGUI.config then
local dispX, dispY, dispW, dispH = wx.wxClientDisplayRect()
local _, str = sudokuGUI.config:Read("wxLuaSudoku/Frame")
local x, y, w, h, max = string.match(str, "x:(%d+) y:(%d+) w:(%d+) h:(%d+) maximized:(%d+)")
if (x ~= nil) and (y ~= nil) and (w ~= nil) and (h ~= nil) and (max ~= nil) then
x = tonumber(x); y = tonumber(y); w = tonumber(w); h = tonumber(h)
max = inttobool(tonumber(max))
if max then
sudokuGUI.frame:Maximize(true)
else
if x < dispX - 5 then x = 0 end
if y < dispY - 5 then y = 0 end
if w > dispW then w = dispW end
if h > dispH then h = dispH end
sudokuGUI.frame:Move(x, y)
sudokuGUI.frame:SetClientSize(w, h)
end
end
sudokuGUI.ConfigReadWrite(true, sudokuGUI.config)
end
for winID = 1, 81 do
if sudokuGUI.IsOddBlockCell(winID) then
sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR])
else
sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR])
end
end
local show_toolbar = sudokuGUI.frame:GetMenuBar():IsChecked(sudokuGUI.ID_SHOW_TOOLBAR)
if sudokuGUI.frame:GetToolBar():IsShown() ~= show_toolbar then
-- generate fake event to simplify processing
local evt = wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.ID_SHOW_TOOLBAR)
evt:SetInt(booltoint(show_toolbar))
sudokuGUI.OnMenuEvent(evt)
end
local show_toolbar_labels = sudokuGUI.frame:GetMenuBar():IsChecked(sudokuGUI.ID_SHOW_TOOLBAR_LABELS)
if (bit.band(sudokuGUI.frame:GetToolBar():GetWindowStyleFlag(), wx.wxTB_TEXT) ~= 0) ~= show_toolbar_labels then
-- generate fake event to simplify processing
local evt = wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.ID_SHOW_TOOLBAR_LABELS)
evt:SetInt(booltoint(show_toolbar_labels))
sudokuGUI.OnMenuEvent(evt)
end
local show_statusbar = sudokuGUI.frame:GetMenuBar():IsChecked(sudokuGUI.ID_SHOW_STATUSBAR)
if sudokuGUI.frame:GetStatusBar():IsShown() ~= show_statusbar then
-- generate fake event to simplify processing
local evt = wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.ID_SHOW_STATUSBAR)
evt:SetInt(booltoint(show_statusbar))
sudokuGUI.OnMenuEvent(evt)
end
sudokuGUI.valueFont_cache = {} -- clear cache in case the font has changed
sudokuGUI.possibleFont_cache = {}
-- update font size
local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH()
sudokuGUI.GetCellBestSize(width, height)
-- update for preferences
sudokuGUI.UpdateTable()
sudokuGUI.Refresh()
end
function sudokuGUI.ConfigReadWrite(read, config)
local path = "wxLuaSudoku"
local function ReadWriteColour(key, c)
if read then
if config:HasEntry(key) then
local _, str = config:Read(key)
local r, g, b = string.match(str, "r:(%d+) g:(%d+) b:(%d+)")
if (r == nil) or (g == nil) or (b == nil) then return end
r = tonumber(r); g = tonumber(g); b = tonumber(b)
if (r < 0) or (r > 255) then return end -- sanity check
if (g < 0) or (g > 255) then return end
if (b < 0) or (b > 255) then return end
c:Set(r, g, b)
end
else
config:Write(key, string.format("r:%d g:%d b:%d", c:Red(), c:Green(), c:Blue()))
end
end
local function ReadWriteFont(key, f)
if read then
if config:HasEntry(key) then
local _, str = config:Read(key)
local face, family, style, underlined, weight = string.match(str, "face:(\"[%w ]+\") family:(%d+) style:(%d+) underlined:(%d+) weight:(%d+)")
if (face == nil) or (family == nil) or (style == nil) or (underlined == nil) or (weight == nil) then return end
family = tonumber(family); style = tonumber(style);
underlined = inttobool(tonumber(underlined)); weight = tonumber(weight)
-- remove quotes
if string.len(face) > 2 then face = string.sub(face, 2, -2) end
-- test so see if the values are any good
local ff = wx.wxFont(12, family, style, weight, underlined, face)
if not ff:Ok() then return end
local tempF = wx.wxFont(f)
f:SetFaceName(face)
f:SetFamily(family)
f:SetStyle(style)
f:SetUnderlined(underlined)
f:SetWeight(weight)
-- shouldn't happen but we always want a usable font
if not f:Ok() then
f:SetFaceName(tempF:GetFaceName())
f:SetFamily(tempF:GetFamily())
f:SetStyle(tempF:GetStyle())
f:SetUnderlined(tempF:GetUnderlined())
f:SetWeight(tempF:GetWeight())
end
end
else
config:Write(key, string.format("face:\"%s\" family:%d style:%d underlined:%d weight:%d",
f:GetFaceName(), f:GetFamily(), f:GetStyle(), booltoint(f:GetUnderlined()), f:GetWeight()))
end
end
if read then
local _
if config:HasEntry(path.."/LastOpenedFilepath") then
_, sudokuGUI.filePath = config:Read(path.."/LastOpenedFilepath", "")
end
if config:HasEntry(path.."/LastOpenedFilename") then
_, sudokuGUI.fileName = config:Read(path.."/LastOpenedFilename", "")
end
if config:HasEntry(path.."/GenerateDifficulty") then
_, sudokuGUI.difficulty = config:Read(path.."/GenerateDifficulty", 0)
end
else
config:Write(path.."/LastOpenedFilepath", sudokuGUI.filePath)
config:Write(path.."/LastOpenedFilename", sudokuGUI.fileName)
config:Write(path.."/GenerateDifficulty", sudokuGUI.difficulty)
end
ReadWriteColour(path.."/Colours/Value", sudokuGUI.Colours[sudokuGUI.VALUE_COLOUR])
ReadWriteColour(path.."/Colours/ValueInit", sudokuGUI.Colours[sudokuGUI.INIT_VALUE_COLOUR])
ReadWriteColour(path.."/Colours/ValuePossible", sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR])
ReadWriteColour(path.."/Colours/ValueInvalid", sudokuGUI.Colours[sudokuGUI.INVALID_VALUE_COLOUR])
ReadWriteColour(path.."/Colours/CellBackground", sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR])
ReadWriteColour(path.."/Colours/CellOddBackground", sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR])
ReadWriteColour(path.."/Colours/CellFocus", sudokuGUI.Colours[sudokuGUI.FOCUS_CELL_COLOUR])
ReadWriteColour(path.."/Colours/NakedPairs", sudokuGUI.Colours[sudokuGUI.NAKED_PAIRS_COLOUR])
ReadWriteColour(path.."/Colours/NakedTriplets", sudokuGUI.Colours[sudokuGUI.NAKED_TRIPLETS_COLOUR])
ReadWriteColour(path.."/Colours/NakedQuads", sudokuGUI.Colours[sudokuGUI.NAKED_QUADS_COLOUR])
ReadWriteColour(path.."/Colours/HiddenPairs", sudokuGUI.Colours[sudokuGUI.HIDDEN_PAIRS_COLOUR])
ReadWriteColour(path.."/Colours/HiddenTriplets", sudokuGUI.Colours[sudokuGUI.HIDDEN_TRIPLETS_COLOUR])
ReadWriteColour(path.."/Colours/HiddenQuads", sudokuGUI.Colours[sudokuGUI.HIDDEN_QUADS_COLOUR])
ReadWriteFont(path.."/Fonts/Value", sudokuGUI.valueFont.wxfont)
ReadWriteFont(path.."/Fonts/ValuePossible", sudokuGUI.possibleFont.wxfont)
local function ReadWritePref(key, pref)
if read then
if config:HasEntry(key) then
local _, v = config:Read(key, 0)
sudokuGUI.CheckMenuItem(pref, inttobool(v))
end
else
config:Write(key, booltoint(sudokuGUI.IsCheckedMenuItem(pref)))
end
end
ReadWritePref(path.."/Preferences/SHOW_ERRORS", sudokuGUI.ID_SHOW_ERRORS)
ReadWritePref(path.."/Preferences/SHOW_MISTAKES", sudokuGUI.ID_SHOW_MISTAKES)
ReadWritePref(path.."/Preferences/SHOW_TOOLBAR", sudokuGUI.ID_SHOW_TOOLBAR)
ReadWritePref(path.."/Preferences/SHOW_TOOLBAR_LABELS", sudokuGUI.ID_SHOW_TOOLBAR_LABELS)
ReadWritePref(path.."/Preferences/SHOW_STATUSBAR", sudokuGUI.ID_SHOW_STATUSBAR)
ReadWritePref(path.."/Preferences/SHOW_POSSIBLE", sudokuGUI.ID_SHOW_POSSIBLE)
ReadWritePref(path.."/Preferences/SHOW_USER_POSSIBLE", sudokuGUI.ID_SHOW_USER_POSSIBLE)
ReadWritePref(path.."/Preferences/SHOW_POSSIBLE_LINE", sudokuGUI.ID_SHOW_POSSIBLE_LINE)
ReadWritePref(path.."/Preferences/SHOW_NAKED", sudokuGUI.ID_SHOW_NAKED)
ReadWritePref(path.."/Preferences/SHOW_HIDDEN", sudokuGUI.ID_SHOW_HIDDEN)
ReadWritePref(path.."/Preferences/SHOW_NAKEDPAIRS", sudokuGUI.ID_SHOW_NAKEDPAIRS)
ReadWritePref(path.."/Preferences/SHOW_HIDDENPAIRS", sudokuGUI.ID_SHOW_HIDDENPAIRS)
ReadWritePref(path.."/Preferences/SHOW_NAKEDTRIPLETS", sudokuGUI.ID_SHOW_NAKEDTRIPLETS)
ReadWritePref(path.."/Preferences/SHOW_HIDDENTRIPLETS", sudokuGUI.ID_SHOW_HIDDENTRIPLETS)
ReadWritePref(path.."/Preferences/SHOW_NAKEDQUADS", sudokuGUI.ID_SHOW_NAKEDQUADS)
ReadWritePref(path.."/Preferences/SHOW_HIDDENQUADS", sudokuGUI.ID_SHOW_HIDDENQUADS)
ReadWritePref(path.."/Preferences/ELIMINATE_NAKED", sudokuGUI.ID_ELIMINATE_NAKED)
ReadWritePref(path.."/Preferences/ELIMINATE_HIDDEN", sudokuGUI.ID_ELIMINATE_HIDDEN)
ReadWritePref(path.."/Preferences/ELIMINATE_NAKEDPAIRS", sudokuGUI.ID_ELIMINATE_NAKEDPAIRS)
ReadWritePref(path.."/Preferences/ELIMINATE_HIDDENPAIRS", sudokuGUI.ID_ELIMINATE_HIDDENPAIRS)
ReadWritePref(path.."/Preferences/ELIMINATE_NAKEDTRIPLETS", sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS)
ReadWritePref(path.."/Preferences/ELIMINATE_HIDDENTRIPLETS", sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS)
ReadWritePref(path.."/Preferences/ELIMINATE_NAKEDQUADS", sudokuGUI.ID_ELIMINATE_NAKEDQUADS)
ReadWritePref(path.."/Preferences/ELIMINATE_HIDDENQUADS", sudokuGUI.ID_ELIMINATE_HIDDENQUADS)
end
-- ----------------------------------------------------------------------------
function sudokuGUI.InitFontsAndColours()
sudokuGUI.Colours =
{
[sudokuGUI.VALUE_COLOUR] = wx.wxColour(0, 0, 230),
[sudokuGUI.INIT_VALUE_COLOUR] = wx.wxColour(0, 0, 0),
[sudokuGUI.POSS_VALUE_COLOUR] = wx.wxColour(0, 0, 0),
[sudokuGUI.INVALID_VALUE_COLOUR] = wx.wxColour(255, 0, 0),
[sudokuGUI.BACKGROUND_COLOUR] = wx.wxColour(255, 255, 255),
[sudokuGUI.ODD_BACKGROUND_COLOUR] = wx.wxColour(250, 250, 210),
[sudokuGUI.FOCUS_CELL_COLOUR] = wx.wxColour(200, 220, 250),
[sudokuGUI.NAKED_PAIRS_COLOUR] = wx.wxColour(255, 0, 0),
[sudokuGUI.NAKED_TRIPLETS_COLOUR] = wx.wxColour(255, 180, 0),
[sudokuGUI.NAKED_QUADS_COLOUR] = wx.wxColour(255, 255, 0),
[sudokuGUI.HIDDEN_PAIRS_COLOUR] = wx.wxColour(0, 220, 0),
[sudokuGUI.HIDDEN_TRIPLETS_COLOUR] = wx.wxColour(0, 240, 160),
[sudokuGUI.HIDDEN_QUADS_COLOUR] = wx.wxColour(0, 220, 220)
}
sudokuGUI.Colours_ = {}
for n = 1, sudokuGUI.COLOUR_MAX do
sudokuGUI.Colours_[n] = wx.wxColour(sudokuGUI.Colours[n])
end
-- just use defaults since some XP systems may not even have wxMODERN
sudokuGUI.possibleFont_wxfont_ = wx.wxFont(wx.wxNORMAL_FONT)
sudokuGUI.valueFont_wxfont_ = wx.wxFont(wx.wxNORMAL_FONT)
sudokuGUI.valueFont_wxfont_:SetWeight(wx.wxFONTWEIGHT_BOLD)
if not sudokuGUI.valueFont_wxfont_:Ok() then
sudokuGUI.valueFont_wxfont_:Destroy()
sudokuGUI.valueFont_wxfont_ = wx.wxFont(wx.wxNORMAL_FONT)
end
sudokuGUI.possibleFont.wxfont = wx.wxFont(sudokuGUI.possibleFont_wxfont_)
sudokuGUI.valueFont.wxfont = wx.wxFont(sudokuGUI.valueFont_wxfont_)
end
-- ----------------------------------------------------------------------------
-- Create a table of the menu IDs to use as a "case" type statement
sudokuGUI.MenuId = {}
-- ----------------------------------------------------------------------------
sudokuGUI.MenuId[sudokuGUI.ID_NEW] = function() sudokuGUI.NewPuzzle(true) end
sudokuGUI.MenuId[sudokuGUI.ID_CREATE] = function(event) sudokuGUI.CreatePuzzle(event:IsChecked()) end
sudokuGUI.MenuId[sudokuGUI.ID_GENERATE] = function() sudokuGUI.GeneratePuzzle() end
sudokuGUI.MenuId[sudokuGUI.ID_OPEN] = function() sudokuGUI.OpenPuzzle() end
sudokuGUI.MenuId[sudokuGUI.ID_SAVEAS] = function() sudokuGUI.SaveAsPuzzle() end
sudokuGUI.MenuId[sudokuGUI.ID_PAGESETUP] = function() sudokuGUI.PageSetup() end
sudokuGUI.MenuId[sudokuGUI.ID_PRINTSETUP] = function() sudokuGUI.PrintSetup() end
sudokuGUI.MenuId[sudokuGUI.ID_PRINTPREVIEW] = function() sudokuGUI.PrintPreview() end
sudokuGUI.MenuId[sudokuGUI.ID_PRINT] = function() sudokuGUI.Print() end
sudokuGUI.MenuId[sudokuGUI.ID_EXIT] = function() sudokuGUI.frame:Close() end
-- ----------------------------------------------------------------------------
sudokuGUI.MenuId[sudokuGUI.ID_COPY_PUZZLE] =
function (event)
local str = sudoku.ToString(sudokuGUI.GetCurrentTable())
if wx.wxClipboard.Get():Open() then
wx.wxClipboard.Get():SetData(wx.wxTextDataObject(str))
wx.wxClipboard.Get():Close()
end
end
sudokuGUI.MenuId[sudokuGUI.ID_RESET] = function() sudokuGUI.ResetPuzzle() end
sudokuGUI.MenuId[sudokuGUI.ID_UNDO] = function() sudokuGUI.Undo() end
sudokuGUI.MenuId[sudokuGUI.ID_REDO] = function() sudokuGUI.Redo() end
sudokuGUI.MenuId[sudokuGUI.ID_PREFERENCES] = function() sudokuGUI.PreferencesDialog() end
sudokuGUI.MenuId[sudokuGUI.ID_SAVE_PREFERENCES] = function() sudokuGUI.ConfigSave(true) end
-- ----------------------------------------------------------------------------
-- Makes sure that menu and tool items are in sync and updates the table
function sudokuGUI.MenuCheckUpdate(event)
sudokuGUI.CheckMenuItem(event:GetId(), event:IsChecked())
sudokuGUI.UpdateTable()
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_ERRORS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_MISTAKES] =
function (event)
-- need to solve it ourselves first
if (not sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE)) and
(event:IsChecked()) and (not sudokuGUI.sudokuSolnTable) then
sudokuGUI.sudokuSolnTable = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable())
if not sudokuGUI.sudokuSolnTable then
event:SetInt(0) -- uncheck for MenuCheckUpdate function
sudokuGUI.frame:GetMenuBar():Check(sudokuGUI.ID_SHOW_MISTAKES, false)
end
end
sudokuGUI.MenuCheckUpdate(event)
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_TOOLBAR] =
function(event)
sudokuGUI.frame:GetToolBar():Show(event:IsChecked())
-- hack to make the wxFrame layout the child panel
local w, h = sudokuGUI.frame:GetSizeWH()
sudokuGUI.frame:SetSize(wx.wxSize(w, h+1))
sudokuGUI.frame:SetSize(wx.wxSize(w, h))
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_TOOLBAR, event:IsChecked())
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_TOOLBAR_LABELS] =
function(event)
local style = wx.wxNO_BORDER
if event:IsChecked() then
style = style + wx.wxTB_TEXT
end
sudokuGUI.frame:GetToolBar():SetWindowStyle(style)
sudokuGUI.frame:GetToolBar():Realize()
-- hack to make the wxFrame layout the child panel
local w, h = sudokuGUI.frame:GetSizeWH()
sudokuGUI.frame:SetSize(wx.wxSize(w, h+1))
sudokuGUI.frame:SetSize(wx.wxSize(w, h))
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_TOOLBAR_LABELS, event:IsChecked())
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_STATUSBAR] =
function(event)
sudokuGUI.frame:GetStatusBar():Show(event:IsChecked())
-- hack to make the wxFrame layout the child panel
local w, h = sudokuGUI.frame:GetSizeWH()
sudokuGUI.frame:SetSize(wx.wxSize(w, h+1))
sudokuGUI.frame:SetSize(wx.wxSize(w, h))
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_STATUSBAR, event:IsChecked())
end
-- ----------------------------------------------------------------------------
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_POSSIBLE] =
function (event)
if event:IsChecked() then
-- make this act like a radio item that can be unchecked
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE, false)
end
sudokuGUI.MenuCheckUpdate(event)
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_USER_POSSIBLE] =
function (event)
if event:IsChecked() then
-- make this act like a radio item that can be unchecked
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_POSSIBLE, false)
end
sudokuGUI.MenuCheckUpdate(event)
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_POSSIBLE_LINE] =
function (event)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE, event:IsChecked())
local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH()
sudokuGUI.GetCellBestSize(width-1, height-1)
sudokuGUI.Refresh()
end
sudokuGUI.MenuId[sudokuGUI.ID_USER_POSSIBLE_CLEAR] =
function (event)
local ret = wx.wxMessageBox("Clear all of your pencil marks?",
"wxLuaSudoku - clear pencil marks?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxOK then
sudokuGUI.pencilMarks = {}
for cell = 1, 81 do
sudokuGUI.pencilMarks[cell] = {}
end
sudokuGUI.UpdateTable()
end
end
sudokuGUI.MenuId[sudokuGUI.ID_USER_POSSIBLE_SETALL] =
function (event)
local ret = wx.wxMessageBox("Set all values as possible in the pencil marks?",
"wxLuaSudoku - set all pencil marks?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxOK then
sudokuGUI.pencilMarks = {}
for cell = 1, 81 do
sudokuGUI.pencilMarks[cell] = {}
for v = 1, 9 do
sudokuGUI.pencilMarks[cell][v] = v
end
end
sudokuGUI.UpdateTable()
end
end
sudokuGUI.MenuId[sudokuGUI.ID_USER_POSSIBLE_INIT] =
function (event)
local ret = wx.wxMessageBox("Initialize the pencil marks to the calculated possible values?",
"wxLuaSudoku - initialize pencil marks?",
wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION,
sudokuGUI.frame )
if ret == wx.wxOK then
local s = sudokuGUI.GetCurrentTable()
for cell = 1, 81 do
sudokuGUI.pencilMarks[cell] = {}
for v = 1, 9 do
sudokuGUI.pencilMarks[cell][v] = s.possible[cell][v]
end
end
sudokuGUI.UpdateTable()
end
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKED] =
function (event)
local checked = event:IsChecked()
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_NAKEDPAIRS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_NAKEDTRIPLETS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_NAKEDQUADS, checked)
sudokuGUI.UpdateTable()
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDEN] =
function (event)
local checked = event:IsChecked()
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_HIDDENPAIRS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_HIDDENTRIPLETS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_HIDDENQUADS, checked)
sudokuGUI.UpdateTable()
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKEDPAIRS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDENPAIRS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKEDTRIPLETS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDENTRIPLETS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKEDQUADS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDENQUADS] = sudokuGUI.MenuCheckUpdate
-- ----------------------------------------------------------------------------
sudokuGUI.MenuId[sudokuGUI.ID_VERIFY_PUZZLE] =
function (event)
local s = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable())
if s then
sudokuGUI.sudokuSolnTable = s
end
end
sudokuGUI.MenuId[sudokuGUI.ID_SHOW_SOLUTION] =
function (event)
if not sudokuGUI.sudokuSolnTable then
local s = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable())
if s then
sudokuGUI.sudokuSolnTable = s
end
end
if sudokuGUI.sudokuSolnTable then
sudokuGUI.AddTable(sudokuGUI.sudokuSolnTable)
end
end
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKED] =
function (event)
local checked = event:IsChecked()
sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_NAKEDPAIRS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_NAKEDQUADS, checked)
sudokuGUI.UpdateTable()
end
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDEN] =
function (event)
local checked = event:IsChecked()
sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_HIDDENPAIRS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS, checked)
sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_HIDDENQUADS, checked)
sudokuGUI.UpdateTable()
end
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKEDPAIRS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDENPAIRS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKEDQUADS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDENQUADS] = sudokuGUI.MenuCheckUpdate
sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANSINGLES] =
function (event)
local s = TableCopy(sudokuGUI.GetCurrentTable())
local changed_cells = sudoku.SolveScanSingles(s)
if changed_cells then sudokuGUI.AddTable(s) end
end
sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANROWS] =
function (event)
local s = TableCopy(sudokuGUI.GetCurrentTable())
local changed_cells = sudoku.SolveScanRows(s)
if changed_cells then sudokuGUI.AddTable(s) end
end
sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANCOLS] =
function (event)
local s = TableCopy(sudokuGUI.GetCurrentTable())
local changed_cells = sudoku.SolveScanCols(s)
if changed_cells then sudokuGUI.AddTable(s) end
end
sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANBLOCKS] =
function (event)
local s = TableCopy(sudokuGUI.GetCurrentTable())
local changed_cells = sudoku.SolveScanBlocks(s)
if changed_cells then sudokuGUI.AddTable(s) end
end
sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANNING] = function (event) sudokuGUI.SolveScanning() end
sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_BRUTEFORCE] = function (event) sudokuGUI.SolveBruteForce() end
-- ----------------------------------------------------------------------------
sudokuGUI.MenuId[sudokuGUI.ID_ABOUT] =
function (event)
wx.wxMessageBox("Welcome to wxLuaSudoku!\nWritten by John Labenski\nCopyright 2006.\n"..
wxlua.wxLUA_VERSION_STRING.." built with "..wx.wxVERSION_STRING,
"About wxLuaSudoku",
wx.wxOK + wx.wxICON_INFORMATION,
sudokuGUI.frame )
end
sudokuGUI.MenuId[sudokuGUI.ID_HELP] =
function (event)
local helpFrame = wx.wxFrame(sudokuGUI.frame, wx.wxID_ANY, "Help on wxLuaSudoku", wx.wxDefaultPosition, wx.wxSize(600,400))
local htmlWin = wx.wxHtmlWindow(helpFrame)
if (htmlWin:SetPage(sudokuGUIhelp)) then
helpFrame:Centre()
helpFrame:Show(true)
else
helpFrame:Destroy()
end
end
-- ----------------------------------------------------------------------------
function sudokuGUI.OnMenuEvent(event)
local id = event:GetId()
if sudokuGUI.MenuId[id] then
sudokuGUI.MenuId[id](event)
return
end
end
-- ----------------------------------------------------------------------------
-- Unify all checking and unchecking of the menu items and
-- make sure menu/toolbar are in sync
function sudokuGUI.CheckMenuItem(id, check)
sudokuGUI.frame:GetMenuBar():Check(id, check)
sudokuGUI.frame:GetToolBar():ToggleTool(id, check) -- doesn't care if id doesn't exist
sudokuGUI.menuCheckIDs[id] = check
end
function sudokuGUI.IsCheckedMenuItem(id)
if sudokuGUI.menuCheckIDs[id] == nil then
sudokuGUI.menuCheckIDs[id] = sudokuGUI.frame:GetMenuBar():IsChecked(id)
end
return sudokuGUI.menuCheckIDs[id]
end
-- ----------------------------------------------------------------------------
function main()
sudokuGUI.block_refresh = true
-- initialize the fonts and colours to use (must always exist)
sudokuGUI.InitFontsAndColours()
-- initialize the printing defaults
sudokuGUI.printData:SetPaperId(wx.wxPAPER_LETTER);
sudokuGUI.pageSetupData:SetMarginTopLeft(wx.wxPoint(25, 25));
sudokuGUI.pageSetupData:SetMarginBottomRight(wx.wxPoint(25, 25));
-- Create the main frame for the program
sudokuGUI.frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "wxLuaSudoku",
wx.wxDefaultPosition, wx.wxSize(300,320))
sudokuGUI.frame:SetSizeHints(300, 300);
local bitmap = wx.wxBitmap(sudokuGUIxpmdata)
local icon = wx.wxIcon()
icon:CopyFromBitmap(bitmap)
sudokuGUI.frame:SetIcon(icon)
local function MItem(menu, id, text, help, bmp)
local m = wx.wxMenuItem(menu, id, text, help)
m:SetBitmap(bmp)
bmp:delete()
return m
end
local fileMenu = wx.wxMenu("", 0)
fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_NEW, "&New...\tCtrl-N", "Clear the current puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_NEW, wx.wxART_TOOLBAR)))
fileMenu:AppendCheckItem(sudokuGUI.ID_CREATE, "&Create...\tCtrl-T", "Enter the initial values for the puzzle")
fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_GENERATE, "&Generate...\tCtrl-G", "Generate a new puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_EXECUTABLE_FILE, wx.wxART_TOOLBAR)))
fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_OPEN, "&Open...\tCtrl-O", "Open a puzzle file", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_OPEN, wx.wxART_TOOLBAR)))
fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_SAVEAS, "&Save as...\tCtrl-S", "Save the current puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_SAVE_AS, wx.wxART_TOOLBAR)))
fileMenu:AppendSeparator()
fileMenu:Append(sudokuGUI.ID_PAGESETUP, "Page S&etup...", "Setup the printout page")
--fileMenu:Append(sudokuGUI.ID_PRINTSETUP, "Print Se&tup...", "Setup the printer")
fileMenu:Append(sudokuGUI.ID_PRINTPREVIEW, "Print Pre&view...", "Preview the printout")
fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_PRINT, "&Print...", "Print the puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_PRINT, wx.wxART_TOOLBAR)))
fileMenu:AppendSeparator()
fileMenu:Append(sudokuGUI.ID_EXIT, "E&xit\tCtrl-X", "Quit the program")
local editMenu = wx.wxMenu("", 0)
editMenu:Append(sudokuGUI.ID_COPY_PUZZLE, "Copy puzzle", "Copy the puzzle to the clipboard")
editMenu:AppendSeparator()
editMenu:Append(sudokuGUI.ID_RESET, "Re&set...\tCtrl-R", "Reset the puzzle to the initial state")
editMenu:AppendSeparator()
editMenu:Append(MItem(editMenu, sudokuGUI.ID_UNDO, "&Undo\tCtrl-Z", "Undo the last entry", wx.wxArtProvider.GetBitmap(wx.wxART_UNDO, wx.wxART_TOOLBAR)))
editMenu:Append(MItem(editMenu, sudokuGUI.ID_REDO, "&Redo\tCtrl-Y", "Redo the last entry", wx.wxArtProvider.GetBitmap(wx.wxART_REDO, wx.wxART_TOOLBAR)))
editMenu:AppendSeparator()
editMenu:Append(sudokuGUI.ID_PREFERENCES, "P&references...", "Show the preferences dialog")
editMenu:AppendSeparator()
editMenu:Append(sudokuGUI.ID_SAVE_PREFERENCES, "Sa&ve preferences...", "Save the preferences")
local viewMenu = wx.wxMenu("", 0)
viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_ERRORS, "Mark &errors\tCtrl-E", "Mark duplicate values in puzzle")
viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_MISTAKES, "Mark &mistakes\tCtrl-M", "Mark wrong values in puzzle")
viewMenu:AppendSeparator()
viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_TOOLBAR, "Show toolbar", "Show the toolbar")
viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_TOOLBAR_LABELS, "Show toolbar labels", "Show labels on the toolbar")
viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_STATUSBAR, "Show statusbar", "Show the statusbar")
viewMenu:Check(sudokuGUI.ID_SHOW_TOOLBAR, true)
viewMenu:Check(sudokuGUI.ID_SHOW_TOOLBAR_LABELS, true)
viewMenu:Check(sudokuGUI.ID_SHOW_STATUSBAR, true)
local possibleMenu = wx.wxMenu("", 0)
possibleMenu:AppendCheckItem( sudokuGUI.ID_SHOW_POSSIBLE, "Show calculated &possible\tCtrl-P", "Show calculated possible values for the cells")
possibleMenu:AppendCheckItem( sudokuGUI.ID_SHOW_USER_POSSIBLE, "Show/Edit pencil marks\tCtrl-l", "Show and edit user set possible values for the cells")
possibleMenu:AppendSeparator()
possibleMenu:AppendCheckItem( sudokuGUI.ID_SHOW_POSSIBLE_LINE, "Show possible in a &line", "Show possible values for the cells in a line")
possibleMenu:AppendSeparator()
local userPossMenu = wx.wxMenu("", 0)
userPossMenu:Append( sudokuGUI.ID_USER_POSSIBLE_CLEAR, "Clear all...", "Clear all pencil marks")
userPossMenu:Append( sudokuGUI.ID_USER_POSSIBLE_SETALL, "Set all...", "Set all pencil marks")
userPossMenu:Append( sudokuGUI.ID_USER_POSSIBLE_INIT, "Calculate...", "Initialize pencil marks to calculated possible")
possibleMenu:Append(sudokuGUI.ID_USER_POSSIBLE_MENU, "Pencil marks", userPossMenu, "Setup user possible values")
possibleMenu:AppendSeparator()
local possViewMenu = wx.wxMenu("", 0)
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKED, "Mark &naked groups", "Mark all naked groups in possible values")
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDEN, "Mark &hidden groups", "Mark all hidden groups in possible values")
possViewMenu:AppendSeparator()
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKEDPAIRS, "Mark naked pairs", "Mark naked pairs in possible values")
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDENPAIRS, "Mark hidden pairs", "Mark hidden pairs in possible values")
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKEDTRIPLETS, "Mark naked triplets", "Mark naked triplets in possible values")
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDENTRIPLETS, "Mark hidden triplets", "Mark hidden triplets in possible values")
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKEDQUADS, "Mark naked quads", "Mark naked quads in possible values")
possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDENQUADS, "Mark hidden quads", "Mark hidden quads in possible values")
possibleMenu:Append(sudokuGUI.ID_SHOW_MENU, "Mark &groups", possViewMenu, "Mark naked/hidden groups")
local solveMenu = wx.wxMenu("", 0)
solveMenu:Append(sudokuGUI.ID_VERIFY_PUZZLE, "Verify unique solution...", "Verify that the puzzle has only one solution")
solveMenu:AppendSeparator()
solveMenu:Append(sudokuGUI.ID_SHOW_SOLUTION, "Show solution", "Show the solution to the puzzle")
solveMenu:AppendSeparator()
local elimSolveMenu = wx.wxMenu("", 0)
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKED, "Eliminate &naked groups", "Eliminate all naked groups from possible values")
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDEN, "Eliminate &hidden groups", "Eliminate all hidden groups from possible values")
elimSolveMenu:AppendSeparator()
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKEDPAIRS, "Eliminate naked pairs", "Eliminate naked pairs from possible values")
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDENPAIRS, "Eliminate hidden pairs", "Eliminate hidden pairs from possible values")
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS, "Eliminate naked triplets", "Eliminate naked triplets from possible values")
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS, "Eliminate hidden triplets", "Eliminate hidden triplets from possible values")
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKEDQUADS, "Eliminate naked quads", "Eliminate naked quads from possible values")
elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDENQUADS, "Eliminate hidden quads", "Eliminate hidden quads from possible values")
solveMenu:Append(sudokuGUI.ID_ELIMINATE_MENU, "&Eliminate groups", elimSolveMenu, "Remove possible values using naked and hidden groups")
solveMenu:AppendSeparator()
solveMenu:Append(sudokuGUI.ID_SOLVE_SCANSINGLES, "Solve (scan singles)\tCtrl-1", "Solve all cells with only one possibility")
solveMenu:Append(sudokuGUI.ID_SOLVE_SCANROWS, "Solve (scan rows)\tCtrl-2", "Solve cells in rows with only one possible value")
solveMenu:Append(sudokuGUI.ID_SOLVE_SCANCOLS, "Solve (scan cols)\tCtrl-3", "Solve cells in cols with only one possible value")
solveMenu:Append(sudokuGUI.ID_SOLVE_SCANBLOCKS, "Solve (scan blocks)\tCtrl-4", "Solve cells in blocks with only one possible value")
solveMenu:AppendSeparator()
solveMenu:Append(sudokuGUI.ID_SOLVE_SCANNING, "Solve (&scanning)\tCtrl-L", "Solve the puzzle by only scanning")
solveMenu:Append(sudokuGUI.ID_SOLVE_BRUTEFORCE, "Solve (&brute force)\tCtrl-B", "Solve the puzzle by guessing values")
local helpMenu = wx.wxMenu("", 0)
helpMenu:Append(sudokuGUI.ID_ABOUT, "&About...", "About the wxLuaSudoku Application")
helpMenu:Append(MItem(helpMenu, sudokuGUI.ID_HELP, "&Help...", "Help using the wxLuaSudoku application", wx.wxArtProvider.GetBitmap(wx.wxART_HELP, wx.wxART_TOOLBAR)))
local menuBar = wx.wxMenuBar()
menuBar:Append(fileMenu, "&File")
menuBar:Append(editMenu, "&Edit")
menuBar:Append(viewMenu, "&View")
menuBar:Append(possibleMenu, "&Possible")
menuBar:Append(solveMenu, "&Solve")
menuBar:Append(helpMenu, "&Help")
sudokuGUI.frame:SetMenuBar(menuBar)
local toolBar = sudokuGUI.frame:CreateToolBar(wx.wxNO_BORDER + wx.wxTB_TEXT)
local tbSize = toolBar:GetToolBitmapSize() -- required to force help icon to right size in MSW
toolBar:AddTool(sudokuGUI.ID_NEW, "New", wx.wxArtProvider.GetBitmap(wx.wxART_NEW, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "New...", "Clear the current puzzle")
toolBar:AddCheckTool(sudokuGUI.ID_CREATE, "Create", wx.wxArtProvider.GetBitmap(wx.wxART_ADD_BOOKMARK, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, "Create...", "Enter initial values for the puzzle")
toolBar:AddTool(sudokuGUI.ID_GENERATE, "Generate", wx.wxArtProvider.GetBitmap(wx.wxART_EXECUTABLE_FILE, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Generate...", "Generate a new puzzle")
toolBar:AddTool(sudokuGUI.ID_OPEN, "Open", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_OPEN, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Open...", "Open a puzzle file")
toolBar:AddTool(sudokuGUI.ID_SAVEAS, "Save", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_SAVE_AS, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Save as...", "Save the current puzzle")
toolBar:AddTool(sudokuGUI.ID_PRINT, "Print", wx.wxArtProvider.GetBitmap(wx.wxART_PRINT, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Print...", "Print the puzzle")
toolBar:AddSeparator()
toolBar:AddTool(sudokuGUI.ID_UNDO, "Undo", wx.wxArtProvider.GetBitmap(wx.wxART_UNDO, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Undo", "Undo the last entry")
toolBar:AddTool(sudokuGUI.ID_REDO, "Redo", wx.wxArtProvider.GetBitmap(wx.wxART_REDO, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Redo", "Redo the last entry")
toolBar:AddSeparator()
toolBar:AddTool(sudokuGUI.ID_HELP, "Help", wx.wxArtProvider.GetBitmap(wx.wxART_HELP, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Help...", "Help on wxLuaSudoku")
toolBar:Realize()
sudokuGUI.frame:CreateStatusBar(2)
local stat_width = sudokuGUI.frame:GetStatusBar():GetTextExtent("Step : 00000")
sudokuGUI.frame:SetStatusWidths({-1, stat_width})
sudokuGUI.frame:SetStatusText("Welcome to wxLuaSudoku.", 0)
-- ------------------------------------------------------------------------
-- Use single centralized menu/toolbar event handler
sudokuGUI.frame:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED,
sudokuGUI.OnMenuEvent)
-- ------------------------------------------------------------------------
local values =
{
5,0,0, 8,0,3, 0,6,0,
1,0,6, 0,9,2, 0,8,5,
0,0,8, 5,0,7, 0,4,0,
0,0,1, 0,3,4, 0,7,0,
0,9,0, 0,0,8, 1,3,4,
3,0,0, 0,2,0, 5,9,0,
0,0,5, 1,0,0, 0,0,3,
0,0,0, 0,0,9, 0,0,0,
0,0,7, 3,0,0, 4,0,9
}
local solution =
{
5,4,9, 8,1,3, 2,6,7,
1,7,6, 4,9,2, 3,8,5,
2,3,8, 5,6,7, 9,4,1,
8,5,1, 9,3,4, 6,7,2,
7,9,2, 6,5,8, 1,3,4,
3,6,4, 7,2,1, 5,9,8,
9,8,5, 1,4,6, 7,2,3,
4,1,3, 2,7,9, 8,5,6,
6,2,7, 3,8,5, 4,1,9
}
local s = sudoku.CreateTable()
sudoku.SetValues(s, values)
sudokuGUI.sudokuTables_pos = 1
sudokuGUI.sudokuTables[1] = s
sudokuGUI.sudokuSolnTable = sudoku.CreateTable()
sudoku.SetValues(sudokuGUI.sudokuSolnTable, solution)
sudokuGUI.panel = wx.wxPanel(sudokuGUI.frame, wx.wxID_ANY)
--sudokuGUI.panel:SetBackgroundColour(wx.wxColour(0,0,0))
local gridsizer = wx.wxGridSizer(9, 9, 2, 2)
for i = 1, 81 do
local win = sudokuGUI.CreateCellWindow( sudokuGUI.panel, i, size )
gridsizer:Add(win, 1, wx.wxALL+wx.wxGROW+ wx.wxALIGN_CENTER, 0)
sudokuGUI.cellWindows[i] = win
end
local topsizer = wx.wxBoxSizer(wx.wxVERTICAL)
topsizer:Add(gridsizer, 1, wx.wxALL+wx.wxGROW+wx.wxALIGN_CENTER, 0)
sudokuGUI.panel:SetSizer( topsizer )
--topsizer:Fit(sudokuGUI.frame)
--topsizer:SetSizeHints( sudokuGUI.frame )
-- ------------------------------------------------------------------------
-- After being created - connect the size event to help MSW repaint the
-- child windows
sudokuGUI.cellWindows[1]:Connect(wx.wxEVT_SIZE,
function (event)
local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH()
sudokuGUI.GetCellBestSize(width, height)
sudokuGUI.Refresh()
event:Skip(true)
end )
-- save the config when closing the frame
sudokuGUI.frame:Connect(wx.wxEVT_CLOSE_WINDOW,
function (event)
event:Skip(true) -- allow it to really exit
sudokuGUI.ConfigSave(false)
end )
local cell_width, cell_height = sudokuGUI.cellWindows[1]:GetClientSizeWH()
sudokuGUI.GetCellBestSize(cell_width, cell_height)
--sudokuGUI.UpdateTable()
sudokuGUI.frame:SetClientSize(300,300)
sudokuGUI.block_refresh = false
sudokuGUI.ConfigLoad()
sudokuGUI.frame:Show(true)
collectgarbage("collect") -- cleanup any locals
end
main()
if false then
function ProfileBegin()
Profile_Counters = {}
Profile_Names = {}
local function hook ()
local f = debug.getinfo(2, "f").func
if Profile_Counters[f] == nil then -- first time `f' is called?
Profile_Counters[f] = 1
Profile_Names[f] = debug.getinfo(2, "Sn")
--TableDump(Profile_Names[f])
else -- only increment the counter
Profile_Counters[f] = Profile_Counters[f] + 1
end
end
debug.sethook(hook, "c") -- turn on the hook
end
function ProfileEnd()
debug.sethook() -- turn off the hook
function getname (func)
local n = Profile_Names[func]
if n.what == "C" then
return n.name
end
local loc = string.format("[%s]:%s", n.short_src, n.linedefined)
if n.namewhat ~= "" then
return string.format("%s (%s)", loc, n.name)
else
return string.format("%s", loc)
end
end
for func, count in pairs(Profile_Counters) do
print(getname(func), count)
end
end
s = sudoku.CreateTable()
s.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] = true
s.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] = true
s.flags[sudoku.ELIMINATE_HIDDEN_QUADS] = true
t = os.time()
--ProfileBegin()
for n = 1, 10 do
a, b = sudoku.FindAllNakedHiddenGroups(s, true)
--a, b, c = sudoku.FindPossibleCountRowColBlock(s)
--a, b, c = sudoku.FindAllPossibleGroups(s)
end
--ProfileEnd()
print(os.time()-t)
end
--[[
for i = 1, 1 do
local s = sudoku.GeneratePuzzle()
s = sudoku.GeneratePuzzleDifficulty(s, 35, true)
sudoku.UpdateTable(s)
local n, h = sudoku.FindAllNakedHiddenGroups(s, true)
--TableDump(n)
--TableDump(h)
local c = 0
cnp = TableCount(n.pairs.cells)
cnt = TableCount(n.triplets.cells)
cnq = TableCount(n.quads.cells)
chp = TableCount(h.pairs.cells)
cht = TableCount(h.triplets.cells)
chq = TableCount(h.quads.cells)
print(i, string.format("n %03d %03d %03d h %03d %03d %03d", cnp, cnt, cnq, chp, cht, chq))
a[string.format("n %03d %03d %03d h %03d %03d %03d", cnp, cnt, cnq, chp, cht, chq)] = TableCopy(s.values)
if (cnp > 0) and (cnt > 0) and (cnq > 0) and (chp > 0) and (cht > 0) and (chq > 0) then
break
end
end
]]
-- Call wx.wxGetApp():MainLoop() last to start the wxWidgets event loop,
-- otherwise the wxLua program will exit immediately.
-- Does nothing if running from wxLua, wxLuaFreeze, or wxLuaEdit since the
-- MainLoop is already running or will be started by the C++ program.
wx.wxGetApp():MainLoop()