5215 lines
212 KiB
Lua
5215 lines
212 KiB
Lua
-------------------------------------------------------------------------=---
|
|
-- 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
|
|
package.cpath = package.cpath..";./?.dll;./?.so;../lib/?.so;../lib/vc_dll/?.dll;../lib/bcc_dll/?.dll;../lib/mingw_dll/?.dll;"
|
|
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
|
|
|
|
local start_n = iff(backwards, 9, 1)
|
|
local end_n = iff(backwards, 1, 9)
|
|
local dir_n = iff(backwards, -1, 1)
|
|
|
|
for n = start_n, end_n, dir_n do
|
|
if possible[n] then
|
|
-- try a number and remove it as a possibility
|
|
sudoku.RemoveCellPossible(s, cell, n)
|
|
|
|
-- 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] = n --sudoku.SetValue(s1, row, col, n)
|
|
sudoku.RemovePossibleAll(s1, cell, n, 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 == 0) or (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 =
|
|
[[
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
|
|
|
<meta name="generator" content=
|
|
"HTML Tidy for Linux/x86 (vers 1st December 2004), see www.w3.org">
|
|
|
|
<title>wxLuaSudoku</title>
|
|
<meta content="John Labenski" name="author">
|
|
<meta content="Documentation for the wxLuaSudoku program" name=
|
|
"description">
|
|
|
|
<body bgcolor="#FFFFCC">
|
|
<h1>wxLuaSudoku</h1>Copyright : John Labenski, 2006<br>
|
|
License : wxWindows license.<br>
|
|
<br>
|
|
<i>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.<br>
|
|
<br>
|
|
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.</i><br>
|
|
<br>
|
|
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.<br>
|
|
|
|
<h2>Purpose</h2>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.<br>
|
|
<br>
|
|
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 <a href=
|
|
"http://wxlua.sourceforge.net">wxlua.sourceforge.net</a>. Information about
|
|
the wxWidgets library can be found on <a href=
|
|
"http://www.wxwidgets.org">www.wxwidgets.org</a> and the Lua programming
|
|
language on <a href="http://www.lua.org">www.lua.org</a>.<br>
|
|
|
|
<h2>Program features</h2>
|
|
|
|
<ul>
|
|
<li>Enter your own initial values for a puzzle to solve
|
|
|
|
<li>Generate random unique new puzzles
|
|
|
|
<li>Load/Save puzzles to files
|
|
|
|
<li>Print the puzzle
|
|
|
|
<li>Undo/Redo of the steps you've taken
|
|
|
|
<li>Mark errors and/or mistakes
|
|
|
|
<li>Show calculated possible values for each cell
|
|
|
|
<li>Pencil marks, possible values for cells that you have determined
|
|
|
|
<li>Automatic marking of hidden/naked pairs, triplets, and quads of
|
|
possible values
|
|
|
|
<li>Eliminate possible values by finding hidden/naked pairs, triplets, and
|
|
quads of possible values
|
|
|
|
<li>Solve the puzzle by scanning for values, can use eliminate
|
|
naked/hidden groups to do better
|
|
|
|
<li>Solve the puzzle by brute force, by guessing values from the possible
|
|
values
|
|
|
|
<li>No installer, registry entries, or other nonsense. Merely delete the
|
|
program when you're done with it.
|
|
</ul>
|
|
|
|
<h2>How to play</h2>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
|
|
<a href=
|
|
"http://en.wikipedia.org/wiki/Sudoku">http://en.wikipedia.org/wiki/Sudoku</a>,
|
|
so lets move on...<br>
|
|
|
|
<h2><a name="Some_Sudoku_terms" id="Some_Sudoku_terms"></a>Some Sudoku
|
|
terms</h2>
|
|
|
|
<ul>
|
|
<li><b>Initial values :</b> 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.
|
|
|
|
<li><b>Possible values :</b> These are values that an empty cell might
|
|
take given the existing values in the row, col, and block that the cell is
|
|
in.
|
|
|
|
<li>
|
|
<b>Naked pairs, triplets, quads :</b> 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.
|
|
|
|
<ul>
|
|
<li>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.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Hidden pairs, triplets, quads :</b> 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.
|
|
|
|
<ul>
|
|
<li>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.
|
|
</ul>
|
|
</ul>
|
|
|
|
<h2>Entering values</h2>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 <i>File->Create</i>.
|
|
Navigate through the cells with the cursor keys or mouse.<br>
|
|
|
|
<h2><a name="Pencil_marks" id="Pencil_marks"></a>Pencil marks</h2>You can
|
|
annotate the empty cells with a list of possible values that may work by
|
|
checking the menu item <i>Possible->Show/Edit pencil marks</i>. If you
|
|
want initialize the pencil marks to be all selected, none selected, or set
|
|
to the calculated values use the menu items under <i>Possible->Pencil
|
|
marks</i>. 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.<br>
|
|
<br>
|
|
Note that this is different than the menu item <i>Possible->Show
|
|
calculated possible</i> 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.
|
|
|
|
<h2><a name="Creating_a_puzzle" id="Creating_a_puzzle"></a>Creating a
|
|
puzzle</h2>To create a new puzzle by hand check the menu item
|
|
<i>File->Create</i>. 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 <i>File->Create</i> item. The program will
|
|
then try to find a solution to the puzzle and verify that there is only one
|
|
unique solution.<br>
|
|
<br>
|
|
If you want to check the state of the puzzle during creation to confirm that
|
|
it has a unique solution, use the <i>Solve->Verify unique solution</i>
|
|
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 <i>View->Mark mistakes</i>, the program
|
|
will try to solve the initial puzzle again, with the same result, no
|
|
solution.<br>
|
|
<br>
|
|
If you want to start with a completely empty grid use <i>File->New</i>.
|
|
If on the other hand, you want to start with a completely filled grid of
|
|
random values, use <i>File->Generate</i> with the number of cell values
|
|
to show set to 81, all shown.
|
|
|
|
<h2>Cell markings</h2>The colors and fonts for the cells can be changed with
|
|
the preferences dialog from the menu item <i>View->Preferences</i>. 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.<br>
|
|
<br>
|
|
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.
|
|
|
|
<h2>Menu item descriptions</h2>All items followed by ... denote that a
|
|
dialog will be shown that can be canceled before any action is taken.
|
|
|
|
<ul>
|
|
<li>
|
|
<b>File</b>
|
|
|
|
<ul>
|
|
<li><b>New...</b> : Clear the whole puzzle to an uninitialized state so
|
|
that you can then "Create" it from scratch.
|
|
|
|
<li>
|
|
<b>Create...</b> : Check this item to enter the initial values,
|
|
uncheck when done to play.
|
|
|
|
<ul>
|
|
<li>See the section on <a href="#Creating_a_puzzle">Creating a
|
|
puzzle</a>.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Generate...</b> : Have the program create a new random unique
|
|
puzzle for you.
|
|
|
|
<ul>
|
|
<li>Set the number of cell values to show, the more that are
|
|
visible, the easier it'll probably be.
|
|
|
|
<li>This program does not try to categorize how difficult the
|
|
generated puzzle is.
|
|
|
|
<li>Note: If you have less than 17 cells shown there will not be a
|
|
unique solution.
|
|
|
|
<li>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.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Open...</b> : Open a puzzle from a file, the puzzle should be a
|
|
9x9 grid of numbers separated by spaces or commas.
|
|
|
|
<ul>
|
|
<li>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.
|
|
</ul>
|
|
|
|
<li><b>Save as...</b> : Save the current puzzle to disk.
|
|
|
|
<li><b>Page Setup...</b> : Adjust the paper and margins for printing.
|
|
|
|
<li><b>Print Preview...</b> : Preview what the printout will look
|
|
like.
|
|
|
|
<li><b>Print...</b> : Print the current puzzle, WYSIWYG, what you see
|
|
is what you get.
|
|
|
|
<li><b>Exit</b> : Exit the program.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Edit</b>
|
|
|
|
<ul>
|
|
<li><b>Copy puzzle</b> : Copy the puzzle to the clipboard to paste
|
|
into other programs.
|
|
|
|
<li><b>Reset...</b> : 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.
|
|
|
|
<li><b>Undo/Redo</b> : 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.
|
|
|
|
<li><b>Preferences</b> : Set the colors to use and whatnot in a
|
|
dialog. See the <a href="#Preferences_dialog">preferences dialog</a>
|
|
section.
|
|
|
|
<li>
|
|
<b>Save preferences</b> : Save the current preferences, including
|
|
what you've set to show and exclude.
|
|
|
|
<ul>
|
|
<li>This program does not automatically save the preferences on
|
|
exit, but rather only when you explicitly request it to do so.
|
|
|
|
<li>MSW : preferences are stored in "Documents and
|
|
Settings\username\wxLuaSudoku.ini"
|
|
|
|
<li>Unix : preferences are stored in "~/.wxLuaSudoku.ini"
|
|
|
|
<li>You may delete these files if you want to reset the program to
|
|
the defaults or are done using it.
|
|
</ul>
|
|
</ul>
|
|
|
|
<li>
|
|
<b>View</b>
|
|
|
|
<ul>
|
|
<li><b>Mark errors</b> : 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.
|
|
|
|
<li>
|
|
<b>Mark mistakes</b> : Mark the cells that have "wrong" values that
|
|
will not lead to a solution by comparing the values to a
|
|
precalculated solution.
|
|
|
|
<ul>
|
|
<li>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.
|
|
</ul>
|
|
|
|
<li><b>Show toolbar</b> : Show or hide the toolbar at the top of the
|
|
program.
|
|
|
|
<li><b>Show statusbar</b> : Show or hide the statusbar at the bottom
|
|
of the program.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Possible</b>
|
|
|
|
<ul>
|
|
<li>
|
|
<b>Show calculated possible</b> : Show the possible values that the
|
|
program has calculated for the empty cells.
|
|
|
|
<ul>
|
|
<li>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.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Show/Edit pencil marks</b> : Allows you to enter the possible
|
|
values for the cells by hand.
|
|
|
|
<ul>
|
|
<li>See the section above about <a href="#Pencil_marks">entering
|
|
pencil marks</a>.
|
|
|
|
<li>You can show the calculated possible values or the pencil
|
|
marks or neither, but not both at the same time.
|
|
</ul>
|
|
|
|
<li><b>Show possible line</b> : Show the possible values in a single
|
|
line, useful if you want to print it out to play later.
|
|
|
|
<li>
|
|
<b>Pencil marks</b>
|
|
|
|
<ul>
|
|
<li><b>Clear all...</b> : Clear all the pencil marks for the whole
|
|
puzzle.
|
|
|
|
<li><b>Set all...</b> : Set all values for the pencil marks for
|
|
the whole puzzle.
|
|
|
|
<li><b>Calculate...</b> : Set all the pencil marks to the same
|
|
values as the calculated possible values.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Mark groups</b>
|
|
|
|
<ul>
|
|
<li><b>Mark naked groups</b> : Mark all naked groups of cells;
|
|
pairs, triplets, and quads.
|
|
|
|
<li><b>Mark hidden groups</b> : Mark all hidden groups of cells;
|
|
pairs, triplets, and quads.
|
|
|
|
<li>... mark each group individually
|
|
|
|
<li><i>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.</i>
|
|
</ul>
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Solve</b>
|
|
|
|
<ul>
|
|
<li>
|
|
<b>Verify unique solution...</b> : 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.
|
|
|
|
<ul>
|
|
<li>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.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Show solution</b> : Show the solution to the puzzle that should
|
|
have been automatically found after opening, creating, or generating
|
|
a puzzle.
|
|
|
|
<ul>
|
|
<li>If you had canceled out of the puzzle verification procedure
|
|
the program will have to solve it using the initial values in the
|
|
puzzle.
|
|
|
|
<li>Use undo to return back to playing the game if you wish to
|
|
just take a peek at some answers.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Eliminate groups</b>
|
|
|
|
<ul>
|
|
<li><b>Eliminate naked groups</b> : 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 <a href="#Some_Sudoku_terms">Sudoku terms</a>
|
|
section about naked groups.
|
|
|
|
<li><b>Eliminate hidden groups</b> : 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 <a href="#Some_Sudoku_terms">Sudoku
|
|
terms</a> section about hidden groups.
|
|
|
|
<li>... eliminate each group individually
|
|
|
|
<li><i>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.</i>
|
|
</ul>
|
|
|
|
<li><i><u>Note on solving</u>: 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.</i>
|
|
|
|
<li><b>Solve (scan singles)</b> : Fill in the values for all cells
|
|
that can only take one value.
|
|
|
|
<li><b>Solve (scan rows)</b> : Fill in the values of cells that are
|
|
the only ones in the row to have a particular possible value.
|
|
|
|
<li><b>Solve (scan cols)</b> : Same for columns
|
|
|
|
<li><b>Solve (scan blocks)</b> : Same for blocks
|
|
|
|
<li><i><u>Note on solve scanning</u> : You can try iterating through
|
|
scanning singles, rows, columns, blocks to see which new values can be
|
|
found after other values have been set.</i>
|
|
|
|
<li>
|
|
<b>Solve (scanning)</b> : Try solving the puzzle by scanning over
|
|
and over until no new cell values can be found.
|
|
|
|
<ul>
|
|
<li>Check different eliminate groups to allow the solver to take
|
|
advantage of the reduced number of possible values.
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Solve (brute force)</b> : Solve the puzzle by guessing values
|
|
from the possible values using the current puzzle values.
|
|
|
|
<ul>
|
|
<li>This will fail if the puzzle cannot be solved (maybe you've
|
|
made a mistake?)
|
|
</ul>
|
|
</ul>
|
|
|
|
<li>
|
|
<b>Help</b>
|
|
|
|
<ul>
|
|
<li><b>About...</b> : A simple about this program dialog.
|
|
|
|
<li><b>Help...</b> : Show this document.
|
|
</ul>
|
|
</ul>
|
|
|
|
<h2><a name="Preferences_dialog" id="Preferences_dialog"></a>Preferences
|
|
dialog</h2>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.<br>
|
|
<br>
|
|
Additionally, a simpler interface to the menu items <i>Possible->Mark
|
|
groups</i> and <i>Solve->Eliminate groups</i> 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()
|