-------------------------------------------------------------------------=--- -- Name: wxluasudoku.wx.lua -- Purpose: wxLuaSudoku - a wxLua program to generate/solve/play Sudoku puzzles -- Author: John Labenski -- Created: 2006 -- Copyright: (c) 2006 John Labenski. All rights reserved. -- Licence: wxWindows licence -------------------------------------------------------------------------=--- -- Load the wxLua module, does nothing if running from wxLua, wxLuaFreeze, or wxLuaEdit require("wx") -- Coding notes: -- All non gui sudoku functions are in the "sudoku" table and all gui related -- functions are in the "sudokuGui" table. -- sudoku.GetXXX() retrieve precalculated or set values from the sudoku table -- sudoku.SetXXX() set values to the sudoku table -- sudoku.FindXXX() search the table for "stuff", but do not modify it, returns result -- sudoku.CalcXXX() search the table for "stuff" and store the values to it for GetXXX functions sudoku = sudoku or {} -- the table to hold the sudoku solver functions -- ============================================================================ -- A simple function to implement "cond ? A : B", eg "result = iff(cond, A, B)" -- note all terms must be able to be evaluated function iff(cond, A, B) if cond then return A else return B end end -- make the number or bool into a bool or number function inttobool(n) if (n == nil) or (n == 0) then return false end return true end function booltoint(n) if (n == nil) or (n == false) then return 0 end return 1 end -- ============================================================================ -- Simple functions to count the total number of elements in a table -- Notes about speed : using for loop and pairs() in TableCount is 2X faster -- than using while loop and next(table). However using next() is slightly -- faster than using for k,v in pairs(t) do return true end in TableIsEmpty. function TableCount(atable) local count = 0 for k, v in pairs(atable) do count = count + 1 end return count end function TableIsEmpty(atable) return next(atable) == nil end -- Set a value in the table or subtable, first making sure that the subtables -- are created first. Modifies the input table. -- TableSetValue(3, atable, "How", "Are", 4, "You") ==> atable.How.Are[4].You = 3 function TableSetValue(value, atable, ...) if type(atable) ~= "table" then atable = {} end local t = atable -- t moves up levels through atable local args = {...} for n = 1, #args-1 do local a = args[n] if not t[a] then t[a] = {} end t = t[a] end t[args[#args]] = value end -- Remove a value in the table or subtable, first making sure that the subtables -- exist. Modifies the input table. -- TableRemoveValue(atable, false, "How", "Are", 4, "You") ==> atable.How.Are[4].You = nil function TableRemoveValue(atable, only_if_empty, ...) if type(atable) ~= "table" then return end local t = atable -- t moves up levels through atable local args = {...} for n = 1, #args-1 do t = t[args[n]] if not t then return end -- already gone end if (not only_if_empty) or ((type(t[args[#args]]) == "table") and TableIsEmpty(t[args[#args]])) then t[args[#args]] = nil end end -- ============================================================================ -- completely dump the contents of a table -- atable is the input table to dump the contents of -- prefix is a string prefix for debugging purposes -- tablelevel is tracker for recursive calls to TableDump (do not use initially) function TableDump(atable, prefix, tablelevel) local function print_val(v) local t = type(v) if t == "number" then return tostring(v) elseif t == "string" then return "\""..v.."\"" end return "'"..tostring(v).."'" end prefix = prefix or "" if tablelevel == nil then tablelevel = "" print(prefix.."-Dumping Table "..tostring(atable)) end prefix = prefix.." " local n = 0 for k, v in pairs(atable) do n = n + 1 print(string.format("%s %d: %s[%s] = %s", prefix, n, tablelevel, print_val(k), print_val(v))) if type(v) == "table" then TableDump(v, prefix.." ", tablelevel.."["..print_val(k).."]") end end end -- ============================================================================ -- Make a deep copy of a table, including all sub tables, fails on recursive tables. -- returns a new table function TableCopy(atable) if not atable then return nil end local newtable = {} for k, v in pairs(atable) do if type(v) == "table" then newtable[k] = TableCopy(v) else newtable[k] = v end end return newtable end -- Merge the two tables together, adding or replacing values in original_table -- with those in new_table, returns a new table and doesn't modify inputs -- fails on recursive tables, returns the new table function TableMerge(new_table, original_table) new_table = new_table or {} local out_table = TableCopy(original_table or {}) for k, v in pairs(new_table) do if type(v) == "table" then if out_table[k] and (type(out_table[k]) == "table") then out_table[k] = TableMerge(v, out_table[k]) elseif out_table[k] then local ov = out_table[k] out_table[k] = TableCopy(v) table.insert(out_table[k], ov) else out_table[k] = TableCopy(v) end elseif out_table[k] and (type(out_table[k]) == "table") then table.insert(out_table[k], v) else out_table[k] = v end end return out_table end -- ============================================================================ -- Flags for the sudokuTable.flags[flag] = true/false/nil sudoku.ELIMINATE_NAKED_PAIRS = 1 -- for sudoku.CalcAllPossible(sudokuTable) sudoku.ELIMINATE_HIDDEN_PAIRS = 2 -- set to true to have it run sudoku.ELIMINATE_NAKED_TRIPLETS = 3 -- for sudoku.CalcAllPossible(sudokuTable) sudoku.ELIMINATE_HIDDEN_TRIPLETS = 4 -- set to true to have it run sudoku.ELIMINATE_NAKED_QUADS = 5 -- for sudoku.CalcAllPossible(sudokuTable) sudoku.ELIMINATE_HIDDEN_QUADS = 6 -- set to true to have it run sudoku.FILENAME = 7 -- store the fileName from Open/Save functions sudoku.ELIMINATE_FLAG_MIN = 1 -- for iterating the ELIMINATE flags sudoku.ELIMINATE_FLAG_MAX = 6 -- given the upper left cell of block you can iterate through the block using -- for n = 1, 9 do cell = n + block_cell + sudoku.LinearBlockCellTable[n] ... end sudoku.LinearBlockCellTable = { -1, -1, -1, 5, 5, 5, 11, 11, 11 } sudoku.CellToRowTable = {} sudoku.CellToColTable = {} sudoku.BlockToRowTable = {} sudoku.BlockToColTable = {} sudoku.CellToBlockTable = {} sudoku.BlockToCellTable = {} for cell = 1, 81 do local row = math.floor(cell/9.1)+1 local col = cell-(row-1)*9 sudoku.CellToRowTable[cell] = row sudoku.CellToColTable[cell] = col local block_row = math.floor(row/3.5)+1 local block_col = math.floor(col/3.5)+1 sudoku.CellToBlockTable[cell] = (block_row-1)*3 + block_col end for block = 1, 9 do local row = math.floor(block/3.5)*3+1 local col = math.fmod(block-1,3)*3+1 sudoku.BlockToRowTable[block] = row sudoku.BlockToColTable[block] = col sudoku.BlockToCellTable[block] = (row-1)*9 + col end -- ============================================================================ -- Create a sudoku table to be used with the rest of the sudoku functions function sudoku.CreateTable() local sudokuTable = { values = {}, -- array (1-81) of values[cell#] = value (0 means unset) row_values = {}, -- array (1-9) of values[row#][value] = { cell1, cell2... } col_values = {}, -- array (1-9) of values[col#][value] = { cell1, cell2... } block_values = {}, -- array (1-9) of values[block#][value] = { cell1, cell2... } possible = {}, -- possible values per cell, possible[cell# 1-81] = { val1, val2... } invalid = {}, -- array (1-81) of known invalid[cell#] = true/nil flags = {} -- extra flags for puzzle, eg. ELIMINATE_NAKED_PAIRS } for i = 1, 81 do sudokuTable.values[i] = 0 -- initialize to unknown sudokuTable.possible[i] = {} -- initialize to empty end for i = 1, 9 do sudokuTable.row_values[i] = {} sudokuTable.col_values[i] = {} sudokuTable.block_values[i] = {} end sudoku.UpdateTable(sudokuTable) return sudokuTable end -- Update all the values in the table using only the cell values, modifies input sudokuTable. function sudoku.UpdateTable(sudokuTable) sudoku.CalcRowColBlockValues(sudokuTable) sudoku.CalcInvalidCells(sudokuTable) sudoku.CalcAllPossible(sudokuTable) end -- Set the values table in the sudokuTable and update everything, modifies input sudokuTable. function sudoku.SetValues(sudokuTable, values) sudokuTable.values = values sudoku.UpdateTable(sudokuTable) end -- Open a sudoku table from a file, the file should be formatted as 9x9 numbers -- with 9 numbers per row and 9 columns. -- returns a sudoku.CreateTable() with the values set and "" on success -- or nil, error_message on failure function sudoku.Open(fileName) local values = {} local value_count = 0 -- number of cols in line local row_count = 0 -- number of rows read local line_n = 0 -- actual line number in file for line in io.lines(fileName) do line_n = line_n + 1 local col_count = 0 for k, v in string.gmatch(line, "%d") do k = tonumber(k) if (k >= 0) and (k <= 9) then table.insert(values, k) col_count = col_count + 1 value_count = value_count + 1 else return nil, string.format("Error loading sudoku file : '%s' invalid number '%d' on line %d.", fileName, k, line_n) end end if col_count == 9 then row_count = row_count + 1 elseif (col_count ~= 0) and (col_count ~= 9) then return nil, string.format("Error loading sudoku file : '%s' on line %d.\nExpecting 9 columns, found %d.", fileName, line_n, col_count) end end if line_n == 0 then return nil, string.format("Error opening sudoku file : '%s'.", fileName) elseif row_count ~= 9 then return nil, string.format("Error loading sudoku file : '%s', expected 9 rows, found %d.", fileName, row_count) elseif value_count ~= 81 then return nil, string.format("Error loading sudoku file : '%s', expected 81 numbers, found %d.", fileName, value_count) end local s = sudoku.CreateTable() s.flags[sudoku.FILENAME] = fileName sudoku.SetValues(s, values) return s, "" end -- Save a sudoku grid as a 9x9 comma separated table to a file, returns success function sudoku.Save(sudokuTable, fileName) local f = io.open(fileName, "w+") if not f then return false end local str = sudoku.ToString(sudokuTable) f:write(str) io.close(f) sudokuTable.flags[sudoku.FILENAME] = fileName return true end -- ============================================================================ -- Neatly print the sudoku grid function sudoku.PrintGrid(sudokuTable) local str = string.rep("-", 13).."\n" for r = 1, 9 do str = str.."|" for c = 1, 9 do local v = " " if sudoku.HasValue(sudokuTable, r, c) then v = tostring(sudoku.GetValue(sudokuTable, r, c)) end str = str..v if math.fmod(c, 3) == 0 then str = str.."|" end end str = str.."\n" if math.fmod(r, 3) == 0 then str = str..string.rep("-", 13).."\n" end end print(str) end -- Write the grid itself to a string as 9x9 with a space/line separating blocks function sudoku.ToString(sudokuTable) local str = "" for r = 1, 9 do for c = 1, 9 do local v = "0" if sudoku.HasValue(sudokuTable, r, c) then v = tostring(sudoku.GetValue(sudokuTable, r, c)) end str = str..v.."," if math.fmod(c, 3) == 0 then str = str.." " end end str = str.."\n" if (r < 9) and (math.fmod(r, 3) == 0) then str = str.."\n" end end return str end -- Neatly print the possible values for each cell (you must calculate it first) function sudoku.PrintPossible(sudokuTable) local str = string.rep("-", 103).."\n" for r = 1, 9 do str = str.."|" for c = 1, 9 do local has_value = sudoku.HasValue(sudokuTable, r, c) if has_value then str = str.."<" else str = str.."[" end local p = sudoku.GetPossible(sudokuTable, r, c) for i = 1, 9 do str = str..(p[i] or " ") end if has_value then str = str..">" else str = str.."]" end if math.fmod(c, 3) == 0 then str = str.."|" end end str = str.."\n" if math.fmod(r, 3) == 0 then str = str..string.rep("-", 103).."\n" end end print(str) end -- ============================================================================ -- Convert a row, col cell index (1-9) into a linear position in the grid (1-81) function sudoku.RowColToCell(row, col) return (row-1)*9 + col end -- Convert a linear cell index (1-81) into a row, col cell index (1-9) function sudoku.CellToRowCol(cell) return sudoku.CellToRowTable[cell], sudoku.CellToColTable[cell] end function sudoku.CellToRow(cell) return sudoku.CellToRowTable[cell] end function sudoku.CellToCol(cell) return sudoku.CellToColTable[cell] end -- ============================================================================ -- Check the validity of rows, cols, cells, blocks, values function sudoku.IsValidValueN(value) return (value >= 1) and (value <= 9) end -- ============================================================================ -- Convert a row, col cell index (1-9) into the linear block number (1-9) function sudoku.RowColToBlock(row, col) return sudoku.CellToBlockTable[sudoku.RowColToCell(row, col)] end -- Get the block (1-9) that this cell (1-81) is in function sudoku.CellToBlock(cell) return sudoku.CellToBlockTable[cell] end -- Get the upper left cell of this block function sudoku.BlockToCell(block) return sudoku.BlockToCellTable[block] end -- Convert a linear block index (1-9) into upper left row, col cell index (1-9) function sudoku.BlockToRowCol(block) return sudoku.BlockToRowTable[block], sudoku.BlockToColTable[block] end function sudoku.BlockToRow(block) return sudoku.BlockToRowTable[block] end function sudoku.BlockToCol(block) return sudoku.BlockToColTable[block] end -- Get the upper left row, col cell of the block given by row, col function sudoku.RowColToBlockRowCol(row, col) local block = sudoku.RowColToBlock(row, col) return sudoku.BlockToRowTable[block], sudoku.BlockToColTable[block] end -- Generate a table of {[cell] = {hash table of cells that are in the row, col, block of this cell}} sudoku.cellToRowColBlockCellsTable = {} for cell = 1, 81 do local row, col = sudoku.CellToRowCol(cell) local block_cell = sudoku.BlockToCell(sudoku.CellToBlock(cell)) sudoku.cellToRowColBlockCellsTable[cell] = {} for rcb = 1, 9 do local c = sudoku.RowColToCell(rcb, col) sudoku.cellToRowColBlockCellsTable[cell][c] = true c = sudoku.RowColToCell(row, rcb) sudoku.cellToRowColBlockCellsTable[cell][c] = true c = rcb + block_cell + sudoku.LinearBlockCellTable[rcb] sudoku.cellToRowColBlockCellsTable[cell][c] = true end end -- Generate a table of {[cell] = {array of cells that are in the row, col, block of this cell}} sudoku.cellToRowColBlockCellsArray = {} for cell = 1, 81 do sudoku.cellToRowColBlockCellsArray[cell] = {} for k, v in pairs(sudoku.cellToRowColBlockCellsTable[cell]) do table.insert(sudoku.cellToRowColBlockCellsArray[cell], k) end end sudoku.RowCellTable = {} sudoku.ColCellTable = {} sudoku.BlockCellTable = {} sudoku.BlockCellShiftTable = {0, 3, 6, 27, 30, 33, 54, 57, 60} for n = 1, 9 do local nn = (n-1)*9 sudoku.RowCellTable[n] = {1+nn, 2+nn, 3+nn, 4+nn, 5+nn, 6+nn, 7+nn, 8+nn, 9+nn} nn = n - 1 sudoku.ColCellTable[n] = {1+nn, 10+nn, 19+nn, 28+nn, 37+nn, 46+nn, 55+nn, 64+nn, 73+nn} nn = sudoku.BlockCellShiftTable[n] sudoku.BlockCellTable[n] = {1+nn, 2+nn, 3+nn, 10+nn, 11+nn, 12+nn, 19+nn, 20+nn, 21+nn} end -- ============================================================================ -- Get the cell value at a specific row, col function sudoku.GetValue(sudokuTable, row, col) return sudoku.GetCellValue(sudokuTable, sudoku.RowColToCell(row, col)) end -- Set the cell value at a specific row, col, modifies input sudokuTable. function sudoku.SetValue(sudokuTable, row, col, value) local cell = sudoku.RowColToCell(row, col) local block = sudoku.CellToBlock(cell) local old_value = sudokuTable.values[cell] if not sudoku.IsValidValueN(value) then value = 0 end sudokuTable.values[cell] = value --remove the old_value from the row, col, block values if sudoku.IsValidValueN(old_value) then if sudokuTable.row_values[row] and sudokuTable.row_values[row][old_value] then sudokuTable.row_values[row][old_value][cell] = nil if TableIsEmpty(sudokuTable.row_values[row][old_value]) then sudokuTable.row_values[row][old_value] = nil end end if sudokuTable.col_values[col] and sudokuTable.col_values[col][old_value] then sudokuTable.col_values[col][old_value][cell] = nil if TableIsEmpty(sudokuTable.col_values[col][old_value]) then sudokuTable.col_values[col][old_value] = nil end end if sudokuTable.block_values[block] and sudokuTable.block_values[block][old_value] then sudokuTable.block_values[block][old_value][cell] = nil if TableIsEmpty(sudokuTable.block_values[block][old_value]) then sudokuTable.block_values[block][old_value] = nil end end end --add new value to the row, col, block values if value ~= 0 then if not sudokuTable.row_values[row] then sudokuTable.row_values[row] = {[value] = {[cell] = cell}} elseif not sudokuTable.row_values[row][value] then sudokuTable.row_values[row][value] = {[cell] = cell} else sudokuTable.row_values[row][value][cell] = cell end if not sudokuTable.col_values[col] then sudokuTable.col_values[col] = {[value] = {[cell] = cell}} elseif not sudokuTable.col_values[col][value] then sudokuTable.col_values[col][value] = {[cell] = cell} else sudokuTable.col_values[col][value][cell] = cell end if not sudokuTable.block_values[block] then sudokuTable.block_values[block] = {[value] = {[cell] = cell}} elseif not sudokuTable.block_values[block][value] then sudokuTable.block_values[block][value] = {[cell] = cell} else sudokuTable.block_values[block][value][cell] = cell end end end -- Does the cell have a value at a specific row, col function sudoku.HasValue(sudokuTable, row, col) return sudoku.HasCellValue(sudokuTable, sudoku.RowColToCell(row, col)) end -- Set the cell value at a specific cell function sudoku.GetCellValue(sudokuTable, cell) return sudokuTable.values[cell] end -- Set the cell value at a specific cell, modifies input sudokuTable. function sudoku.SetCellValue(sudokuTable, cell, value) local row, col = sudoku.CellToRowCol(cell) sudoku.SetValue(sudokuTable, row, col, value) end -- Does the cell have a value at a specific cell function sudoku.HasCellValue(sudokuTable, cell) return sudoku.IsValidValueN(sudokuTable.values[cell]) end -- ============================================================================ -- Set the row_values, col_values, block_values tables of the input sudokuTable -- eg. row values table is row_values[row#][value][cell#] = cell# -- if no value then row_values[row#][value] = nil function sudoku.CalcRowColBlockValues(sudokuTable) sudokuTable.row_values = {} sudokuTable.col_values = {} sudokuTable.block_values = {} for cell = 1, 81 do local row, col = sudoku.CellToRowCol(cell) local block = sudoku.CellToBlock(cell) if not sudokuTable.row_values[row] then sudokuTable.row_values[row] = {} end if not sudokuTable.col_values[col] then sudokuTable.col_values[col] = {} end if not sudokuTable.block_values[block] then sudokuTable.block_values[block] = {} end local value = sudoku.GetCellValue(sudokuTable, cell) if sudoku.IsValidValueN(value) then if not sudokuTable.row_values[row][value] then sudokuTable.row_values[row][value] = {[cell] = cell} else sudokuTable.row_values[row][value][cell] = cell end if not sudokuTable.col_values[col][value] then sudokuTable.col_values[col][value] = {[cell] = cell} else sudokuTable.col_values[col][value][cell] = cell end if not sudokuTable.block_values[block][value] then sudokuTable.block_values[block][value] = {[cell] = cell} else sudokuTable.block_values[block][value][cell] = cell end end end end -- ============================================================================ -- Can this value be put into this cell given the other existing values? function sudoku.IsValidValue(sudokuTable, row, col, value) if sudokuTable.row_values[row][value] or sudokuTable.col_values[col][value] or sudokuTable.block_values[sudoku.RowColToBlock(row, col)][value] then return false end return true end -- Find all the invalid cells by looking for duplicates, modifies input sudokuTable. -- fills sudokuTable.invalid table with the values function sudoku.CalcInvalidCells(sudokuTable) sudokuTable.invalid = {} -- reset to all good for n = 1, 9 do for i, cell_table in pairs(sudokuTable.row_values[n]) do if TableCount(cell_table) > 1 then for j, cell in pairs(cell_table) do sudokuTable.invalid[cell] = true end end end for i, cell_table in pairs(sudokuTable.col_values[n]) do if TableCount(cell_table) > 1 then for j, cell in pairs(cell_table) do sudokuTable.invalid[cell] = true end end end for i, cell_table in pairs(sudokuTable.block_values[n]) do if TableCount(cell_table) > 1 then for j, cell in pairs(cell_table) do sudokuTable.invalid[cell] = true end end end end end -- ============================================================================ -- Get the possible values at a specific row, col cell -- Must be previously set from sudoku.CalcAllPossible function sudoku.GetPossible(sudokuTable, row, col) return sudokuTable.possible[sudoku.RowColToCell(row, col)] end function sudoku.GetCellPossible(sudokuTable, cell) return sudokuTable.possible[cell] end -- Set the possible values at a specific row, col cell. Modifies input sudokuTable. function sudoku.SetPossible(sudokuTable, row, col, possibleTable) sudokuTable.possible[sudoku.RowColToCell(row, col)] = possibleTable end function sudoku.SetCellPossible(sudokuTable, cell, possibleTable) sudokuTable.possible[cell] = possibleTable end -- Remove a possible value at a specific row, col cell only. Modifies input sudokuTable. function sudoku.RemovePossible(sudokuTable, row, col, value) return sudoku.RemoveCellPossible(sudokuTable, sudoku.RowColToCell(row, col), value) end function sudoku.RemoveCellPossible(sudokuTable, cell, value) sudokuTable.possible[cell][value] = nil end -- Remove a possible values from the row, col, block. Modifies input sudokuTable. -- if exceptTable then don't remove it from exceptTable[cell#] = true function sudoku.RemovePossibleAll(sudokuTable, cell, value, exceptTable, break_if_empty) exceptTable = exceptTable or {} break_if_empty = break_if_empty or false for i, c in ipairs(sudoku.cellToRowColBlockCellsArray[cell]) do if (not exceptTable[c]) and sudokuTable.possible[c][value] then sudokuTable.possible[c][value] = nil if break_if_empty and (not sudoku.HasCellValue(sudokuTable, c)) and TableIsEmpty(sudokuTable.possible[c]) then return end end end end -- Remove a possible values from the row. Modifies input sudokuTable. -- if exceptTable then don't remove it from exceptTable[cell#] = true function sudoku.RemovePossibleRow(sudokuTable, row, value, exceptTable) exceptTable = exceptTable or {} for col = 1, 9 do local cell = sudoku.RowColToCell(row, col) if (not exceptTable[cell]) and sudokuTable.possible[cell][value] then sudokuTable.possible[cell][value] = nil end end end -- Remove a possible values from the col. Modifies input sudokuTable. -- if exceptTable then don't remove it from exceptTable[cell#] = true function sudoku.RemovePossibleCol(sudokuTable, col, value, exceptTable) exceptTable = exceptTable or {} for row = 1, 9 do local cell = sudoku.RowColToCell(row, col) if (not exceptTable[cell]) and sudokuTable.possible[cell][value] then sudokuTable.possible[cell][value] = nil end end end -- Remove a possible values from the block. Modifies input sudokuTable. -- if exceptTable then don't remove it from exceptTable[cell#] = true function sudoku.RemovePossibleBlock(sudokuTable, block, value, exceptTable) exceptTable = exceptTable or {} local block_cell = sudoku.BlockToCell(block) for n = 1, 9 do local cell = n + block_cell + sudoku.LinearBlockCellTable[n] if (not exceptTable[cell]) and sudokuTable.possible[cell][value] then sudokuTable.possible[cell][value] = nil end end end -- Get the count of all possible values for rows, cols, and blocks -- returns 3 tables row_possible[row#][value] = #times possible value occurs in row -- and the same for col_possible, block_possible -- if no possible values (all values set) then row_possible[row#] = nil function sudoku.FindPossibleCountRowColBlock(sudokuTable) local row_possible = {} local col_possible = {} local block_possible = {} for cell = 1, 81 do local row, col = sudoku.CellToRowCol(cell) local block = sudoku.CellToBlock(cell) local cell_possible = sudoku.GetCellPossible(sudokuTable, cell) for pvalue, is_possible in pairs(cell_possible) do if not row_possible[row] then row_possible[row] = {} end if not col_possible[col] then col_possible[col] = {} end if not block_possible[block] then block_possible[block] = {} end row_possible[row][pvalue] = (row_possible[row][pvalue] or 0) + 1 col_possible[col][pvalue] = (col_possible[col][pvalue] or 0) + 1 block_possible[block][pvalue] = (block_possible[block][pvalue] or 0) + 1 end end return row_possible, col_possible, block_possible end -- Find all the possible values for row, col cell -- returns a table of possible[value] = true function sudoku.FindPossibleCell(sudokuTable, row, col) local possible = {} -- gather up all the set values in row, col, and block local rowValues = sudokuTable.row_values[row] local colValues = sudokuTable.col_values[col] local blockValues = sudokuTable.block_values[sudoku.RowColToBlock(row, col)] -- remove the set values from the possible values for v = 1, 9 do if (rowValues[v] == nil) and (colValues[v] == nil) and (blockValues[v] == nil) then possible[v] = v end end return possible end -- Find all the possible values for the whole table by filling out the -- possible table in the input sudokuTable. Modifies input sudokuTable. function sudoku.CalcAllPossible(sudokuTable) for cell = 1, 81 do local row, col = sudoku.CellToRowCol(cell) local possible = {} if not sudoku.HasCellValue(sudokuTable, cell) then local block = sudoku.CellToBlock(cell) for v = 1, 9 do if (sudokuTable.row_values[row][v] == nil) and (sudokuTable.col_values[col][v] == nil) and (sudokuTable.block_values[block][v] == nil) then possible[v] = v end end end sudoku.SetCellPossible(sudokuTable, cell, possible) end -- this function checks flags to see if it should run sudoku.RemovePossibleGroups(sudokuTable) end -- Find all the possible pairs, triplets, quads in the table -- must run CalcAllPossible first, does not eliminate any. -- returns 3 tables, possible_pairs.rows[row#][key] = { cell1, cell2... }, -- possible_pairs.cols[col#][key] = { cell1, cell2... }, -- possible_pairs.blocks[block#][key] = { cell1, cell2... }, -- and the same for possible_triplets, possible_quads -- key is constructed from the number group as string.char(val1, val2...) sudoku.FindAllPossibleGroups_Cache = {} function sudoku.FindAllPossibleGroups(sudokuTable) local possible_pairs = {rows = {}, cols = {}, blocks = {}} local possible_triplets = {rows = {}, cols = {}, blocks = {}} local possible_quads = {rows = {}, cols = {}, blocks = {}} local char0 = string.byte("0") local cache_key_flags = 1*booltoint(sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_PAIRS]) + 2*booltoint(sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS]) + 4*booltoint(sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_QUADS]) local cache_keys = { 10^1, 10^2, 10^3, 10^4, 10^5, 10^6, 10^7, 10^8, 10^9 } local function add_possible(atable, rcb_key, key, cell) local a = atable[rcb_key] if not a then atable[rcb_key] = { [key] = {cell} } elseif not a[key] then a[key] = {cell} else a[key][#a[key]+1] = cell end end for cell = 1, 81 do local row, col = sudoku.CellToRowCol(cell) local block = sudoku.CellToBlock(cell) local cell_possible = sudoku.GetCellPossible(sudokuTable, cell) local cell_possible_table = {} local cache_key = cache_key_flags -- convert key, value table to indexed table and a key for the cache local count = 0 for n = 1, 9 do if cell_possible[n] then cell_possible_table[#cell_possible_table+1] = char0+n cache_key = cache_key + cache_keys[n] count = count + 1 end end local possible_pairs_keys = {} local possible_triplets_keys = {} local possible_quads_keys = {} -- either use the cached key table or create a new key table for the possible -- Note: cache cuts time for 100 calls to this fn w/ empty puzzle from 8 to 1 sec if (count > 1) and sudoku.FindAllPossibleGroups_Cache[cache_key] then possible_pairs_keys = sudoku.FindAllPossibleGroups_Cache[cache_key].possible_pairs possible_triplets_keys = sudoku.FindAllPossibleGroups_Cache[cache_key].possible_triplets possible_quads_keys = sudoku.FindAllPossibleGroups_Cache[cache_key].possible_quads elseif (count > 1) then local elim_pairs = (count == 2) or sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] local elim_triplets = (count == 3) or sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] local elim_quads = (count == 4) or sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_QUADS] for i = 1, count do for j = i+1, count do local pkey = string.char(cell_possible_table[i], cell_possible_table[j]) if elim_pairs then possible_pairs_keys[#possible_pairs_keys+1] = pkey end for k = j+1, count do local tkey = pkey..string.char(cell_possible_table[k]) if elim_triplets then possible_triplets_keys[#possible_triplets_keys+1] = tkey end if elim_quads then for l = k+1, count do local qkey = tkey..string.char(cell_possible_table[l]) possible_quads_keys[#possible_quads_keys+1] = qkey end end end end end sudoku.FindAllPossibleGroups_Cache[cache_key] = {} sudoku.FindAllPossibleGroups_Cache[cache_key].possible_pairs = possible_pairs_keys sudoku.FindAllPossibleGroups_Cache[cache_key].possible_triplets = possible_triplets_keys sudoku.FindAllPossibleGroups_Cache[cache_key].possible_quads = possible_quads_keys end for k, key in pairs(possible_pairs_keys) do add_possible(possible_pairs.rows, row, key, cell) add_possible(possible_pairs.cols, col, key, cell) add_possible(possible_pairs.blocks, block, key, cell) end for k, key in pairs(possible_triplets_keys) do add_possible(possible_triplets.rows, row, key, cell) add_possible(possible_triplets.cols, col, key, cell) add_possible(possible_triplets.blocks, block, key, cell) end for k, key in pairs(possible_quads_keys) do add_possible(possible_quads.rows, row, key, cell) add_possible(possible_quads.cols, col, key, cell) add_possible(possible_quads.blocks, block, key, cell) end end return possible_pairs, possible_triplets, possible_quads end -- Find all the naked and hidden pairs, triplets, quads in the table -- must run CalcAllPossible first, does not eliminate any. -- returns 2 tables, naked.rows[row#][key] = { cell1, cell2... }, -- naked.cols[col#][key] = { cell1, cell2... }, -- naked.blocks[block#][key] = { cell1, cell2... }, -- and the same for hidden -- key is constructed from the number group as string.char(val1, val2...) function sudoku.FindAllNakedHiddenGroups(sudokuTable, find_all) local flags = sudokuTable.flags if find_all == true then sudokuTable.flags = TableCopy(flags) -- unref the table -- turn all ELIMINATE_XXX on for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do sudokuTable.flags[n] = true end end local row_possible, col_possible, block_possible = sudoku.FindPossibleCountRowColBlock(sudokuTable) local possible_pairs, possible_triplets, possible_quads = sudoku.FindAllPossibleGroups(sudokuTable) local char0 = string.byte("0") local all_groups = { [2] = 36, [3] = 84, [4] = 126 } -- eg. 9!/(2! * (9-2)!) if find_all == true then sudokuTable.flags = flags -- put the flags back to how they were end local naked = { pairs = {rows = {}, cols = {}, blocks = {}, cells = {}}, triplets = {rows = {}, cols = {}, blocks = {}, cells = {}}, quads = {rows = {}, cols = {}, blocks = {}, cells = {}} } local hidden = { pairs = {rows = {}, cols = {}, blocks = {}, cells = {}}, triplets = {rows = {}, cols = {}, blocks = {}, cells = {}}, quads = {rows = {}, cols = {}, blocks = {}, cells = {}} } -- cache all the cell possible value counts local cell_possible_count = {} for n = 1, 81 do cell_possible_count[n] = TableCount(sudoku.GetCellPossible(sudokuTable, n) or {}) end local function dofind(rcb_table, num, key, cell_table_, rcb, rcb_possible) local naked_cell_table = {} local naked_cell_count = 0 local hidden_cell_table = {} local hidden_cell_count = 0 local is_hidden = true -- can only be exactly as many nums in key as in rcb for hidden for n = 1, num do if rcb_possible[string.byte(key, n)-char0] ~= num then is_hidden = false break end end for n, cell in ipairs(cell_table_) do if cell_possible_count[cell] == num then naked_cell_table[#naked_cell_table+1] = cell naked_cell_count = naked_cell_count + 1 end if is_hidden then hidden_cell_table[#hidden_cell_table+1] = cell hidden_cell_count = hidden_cell_count + 1 end end -- has to be at least the same cell_count as num, if more then error, but... if (naked_cell_count >= num) then if not rcb_table.naked_table[rcb] then rcb_table.naked_table[rcb] = {} end rcb_table.naked_table[rcb][key] = naked_cell_table local cell_table = rcb_table.naked_table_base.cells for n, cell in pairs(naked_cell_table) do if not cell_table[cell] then cell_table[cell] = {} end table.insert(cell_table[cell], key) end end -- has to be at least the same cell_count as num, if more then error, but... if is_hidden and (hidden_cell_count >= num) then if not rcb_table.hidden_table[rcb] then rcb_table.hidden_table[rcb] = {} end rcb_table.hidden_table[rcb][key] = hidden_cell_table local cell_table = rcb_table.hidden_table_base.cells for n, cell in pairs(hidden_cell_table) do if not cell_table[cell] then cell_table[cell] = {} end table.insert(cell_table[cell], key) end end end local function find(naked_table, hidden_table, possible_table, num) local rcb_table = {} rcb_table.naked_table_base = naked_table rcb_table.hidden_table_base = hidden_table rcb_table.naked_table = naked_table.rows rcb_table.hidden_table = hidden_table.rows for row, key_table in pairs(possible_table.rows) do for key, cell_table in pairs(key_table) do dofind(rcb_table, num, key, cell_table, row, row_possible[row]) end end rcb_table.naked_table = naked_table.cols rcb_table.hidden_table = hidden_table.cols for col, key_table in pairs(possible_table.cols) do for key, cell_table in pairs(key_table) do dofind(rcb_table, num, key, cell_table, col, col_possible[col]) end end rcb_table.naked_table = naked_table.blocks rcb_table.hidden_table = hidden_table.blocks for block, key_table in pairs(possible_table.blocks) do for key, cell_table in pairs(key_table) do dofind(rcb_table, num, key, cell_table, block, block_possible[block]) end end return naked_table, hidden_table end naked.pairs, hidden.pairs = find(naked.pairs, hidden.pairs, possible_pairs, 2) naked.triplets, hidden.triplets = find(naked.triplets, hidden.triplets, possible_triplets, 3) naked.quads, hidden.quads = find(naked.quads, hidden.quads, possible_quads, 4) return naked, hidden end -- ============================================================================ -- Find all pairs, triplets, quads of values in rows and reset the possible -- values for the row to exclude these values. Modifies input sudokuTable. function sudoku.RemovePossibleGroups(sudokuTable) -- must have at least one flag set local has_elim_flags = false for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do if sudokuTable.flags[n] == true then has_elim_flags = true break end end if has_elim_flags == false then return end local naked, hidden = sudoku.FindAllNakedHiddenGroups(sudokuTable, false) local char0 = string.byte("0") local function clear_possible(group_table, num, remove_fn) for n = 1, 9 do if group_table[n] then for key, cell_table in pairs(group_table[n]) do local exceptTable = {} for k, v in pairs(cell_table) do exceptTable[v] = v end for k = 1, num do local val = string.byte(key, k)-char0 remove_fn(sudokuTable, n, val, exceptTable) end end end end end if (sudokuTable.flags[sudoku.ELIMINATE_NAKED_PAIRS] == true) then clear_possible(naked.pairs.rows, 2, sudoku.RemovePossibleRow) clear_possible(naked.pairs.cols, 2, sudoku.RemovePossibleCol) clear_possible(naked.pairs.blocks, 2, sudoku.RemovePossibleBlock) end if (sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] == true) then clear_possible(hidden.pairs.rows, 2, sudoku.RemovePossibleRow) clear_possible(hidden.pairs.cols, 2, sudoku.RemovePossibleCol) clear_possible(hidden.pairs.blocks, 2, sudoku.RemovePossibleBlock) end if (sudokuTable.flags[sudoku.ELIMINATE_NAKED_TRIPLETS] == true) then clear_possible(naked.triplets.rows, 3, sudoku.RemovePossibleRow) clear_possible(naked.triplets.cols, 3, sudoku.RemovePossibleCol) clear_possible(naked.triplets.blocks, 3, sudoku.RemovePossibleBlock) end if (sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] == true) then clear_possible(hidden.triplets.rows, 3, sudoku.RemovePossibleRow) clear_possible(hidden.triplets.cols, 3, sudoku.RemovePossibleCol) clear_possible(hidden.triplets.blocks, 3, sudoku.RemovePossibleBlock) end if (sudokuTable.flags[sudoku.ELIMINATE_NAKED_QUADS] == true) then clear_possible(naked.quads.rows, 4, sudoku.RemovePossibleRow) clear_possible(naked.quads.cols, 4, sudoku.RemovePossibleCol) clear_possible(naked.quads.blocks, 4, sudoku.RemovePossibleBlock) end if (sudokuTable.flags[sudoku.ELIMINATE_HIDDEN_QUADS] == true) then clear_possible(hidden.quads.rows, 4, sudoku.RemovePossibleRow) clear_possible(hidden.quads.cols, 4, sudoku.RemovePossibleCol) clear_possible(hidden.quads.blocks, 4, sudoku.RemovePossibleBlock) end end -- ============================================================================ -- Find all the cells that only have a single possible value and set it. -- Modifies input sudokuTable. function sudoku.SolveScanSingles(sudokuTable) sudoku.CalcAllPossible(sudokuTable) local changed_cells = {} for row = 1, 9 do for col = 1, 9 do if not sudoku.HasValue(sudokuTable, row, col) then local possible = sudoku.GetPossible(sudokuTable, row, col) local count = 0 local value = nil for pvalue, is_possible in pairs(possible) do -- count possible values count = count + 1 value = pvalue end if count == 1 then local cell = sudoku.RowColToCell(row, col) sudoku.SetValue(sudokuTable, row, col, value) sudoku.RemovePossibleAll(sudokuTable, cell, value) changed_cells[cell] = value end end end end if TableIsEmpty(changed_cells) then changed_cells = nil end -- reset if not used return changed_cells end -- Find all the cells that only have a single possible value per row and set it. -- Modifies input sudokuTable. function sudoku.SolveScanRows(sudokuTable) sudoku.SolveScanRowsCols(sudokuTable, true) end -- Find all the cells that only have a single possible value per col and set it -- Modifies input sudokuTable. function sudoku.SolveScanCols(sudokuTable) sudoku.SolveScanRowsCols(sudokuTable, false) end function sudoku.SolveScanRowsCols(sudokuTable, scan_rows) sudoku.CalcAllPossible(sudokuTable) local changed_cells = {} for row_col1 = 1, 9 do local row = nil -- set row or col depending on scan_rows local col = nil if scan_rows then row = row_col1 else col = row_col1 end local possible = {} -- all the possible values along the row or col for i = 1, 9 do possible[i] = {} end -- fill possible[pvalue] = { cell1, cell2... } along row or col for row_col2 = 1, 9 do if scan_rows then col = row_col2 else row = row_col2 end if not sudoku.HasValue(sudokuTable, row, col) then local cell_possible = sudoku.GetPossible(sudokuTable, row, col) for pvalue, is_possible in pairs(cell_possible) do table.insert(possible[pvalue], sudoku.RowColToCell(row, col)) end end end -- iterate through the values and if only one possibility set it for value = 1, 9 do if TableCount(possible[value]) == 1 then local cell = possible[value][1] sudoku.SetCellValue(sudokuTable, cell, value) sudoku.RemovePossibleAll(sudokuTable, cell, value) changed_cells[cell] = value end end end if TableIsEmpty(changed_cells) then changed_cells = nil end -- reset if not used return changed_cells end -- Find all the cells that only have a single possible value per block and set it -- Modifies input sudokuTable. function sudoku.SolveScanBlocks(sudokuTable) sudoku.CalcAllPossible(sudokuTable) local changed_cells = {} for block = 1, 9 do local block_row, block_col = sudoku.BlockToRowCol(block) local possible = {} for i = 1, 9 do possible[i] = {} end -- fill possible[pvalue] = { cell1, cell2... } for whole block for row = block_row, block_row+2 do for col = block_col, block_col+2 do if not sudoku.HasValue(sudokuTable, row, col) then local cell_possible = sudoku.GetPossible(sudokuTable, row, col) for pvalue, is_possible in pairs(cell_possible) do table.insert(possible[pvalue], sudoku.RowColToCell(row, col)) end end end end -- iterate through the values and if only one possibility set it for value = 1, 9 do if TableCount(possible[value]) == 1 then local cell = possible[value][1] sudoku.SetCellValue(sudokuTable, cell, value) sudoku.RemovePossibleAll(sudokuTable, cell, value) changed_cells[cell] = value end end end if TableIsEmpty(changed_cells) then changed_cells = nil end -- reset if not used return changed_cells end -- Find all the cells that only have a single possible value per row, col, block and set it -- Modifies input sudokuTable. function sudoku.SolveScan(sudokuTable) local changed_single = {} local changed_rows = {} local changed_cols = {} local changed_blocks = {} local changed_cells = {} -- total cells changed local count = 0 local function add_changed(changed_table, changed_cells) if changed_table then changed_cells = TableMerge(changed_table, changed_cells) end return changed_cells end while (count < 10000) and (changed_single or changed_rows or changed_cols or changed_blocks) do changed_single = sudoku.SolveScanSingles(sudokuTable) changed_rows = sudoku.SolveScanRows(sudokuTable) changed_cols = sudoku.SolveScanCols(sudokuTable) changed_blocks = sudoku.SolveScanBlocks(sudokuTable) changed_cells = add_changed(changed_single, changed_cells) changed_cells = add_changed(changed_rows, changed_cells) changed_cells = add_changed(changed_cols, changed_cells) changed_cells = add_changed(changed_blocks, changed_cells) count = count + 1 end if TableIsEmpty(changed_cells) then changed_cells = nil end -- nothing done return count, changed_cells end -- Brute force recursive solver, returns a new table and does not the input sudokuTable. -- (call with only the SudokuTable, don't enter other parameters) function sudoku.SolveBruteForce(sudokuTable, backwards) -- first time through find possible to limit choices, subsequent calls ok local s = sudoku.CreateTable() -- finding all the possibilities is slow, but at least do solve scan --s.flags[sudoku.ELIMINATE_NAKED_PAIRS] = true --s.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] = true --s.flags[sudoku.ELIMINATE_NAKED_TRIPLETS] = true --s.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] = true --s.flags[sudoku.ELIMINATE_NAKED_QUADS] = true --s.flags[sudoku.ELIMINATE_HIDDEN_QUADS] = true s.values = TableCopy(sudokuTable.values) sudoku.CalcRowColBlockValues(s) sudoku.CalcAllPossible(s) sudoku.SolveScan(s) -- table consists of guesses[cell] = #num -- guesses.current is current guess # local guesses = { current = 0 } for n = 1, 81 do guesses[n] = 0 end -- we don't need these for this and they just slow TableCopy down -- they're recreated at the end using UpdateTable s.row_values = nil s.col_values = nil s.block_values = nil s.invalid = nil s.flags = nil return sudoku.DoSolveBruteForce(sudokuTable, backwards, s, guesses, 1) end function sudoku.DoSolveBruteForce(sudokuTable, backwards, simpleTable, guesses, cell) local s = simpleTable local g, empty_possible if sudoku.SolveBruteForceHook then if not sudoku.SolveBruteForceHook(guesses, cell) then return nil, guesses, cell end end while cell <= 81 do if not sudoku.HasCellValue(s, cell) then local possible = sudoku.GetCellPossible(s, cell) --for k, v in pairs(possible) do -- use for loop to ensure direction for n = 1, 9 do local k = n if backwards == true then k = 10 - n end if possible[k] then -- try a number and remove it as a possibility sudoku.RemoveCellPossible(s, cell, k) -- start a new table and test out this guess local s1 = TableCopy(s) -- don't use SetValue since we only care about possible s1.values[cell] = k --sudoku.SetValue(s1, row, col, k) sudoku.RemovePossibleAll(s1, cell, k, nil, true) guesses[cell] = guesses[cell] + 1 guesses.current = guesses.current + 1 -- check for nil return from RemovePossibleAll for break_if_empty if s1 then s1, g = sudoku.DoSolveBruteForce(sudokuTable, backwards, s1, guesses, cell+1) -- if s1 then success! we're all done if s1 then -- copy all original data back and just set the values local s2 = TableCopy(sudokuTable) sudoku.SetValues(s2, s1.values) return s2, g end end end end return nil, guesses -- tried all values for cell with no solution end cell = cell + 1 end local s2 = TableCopy(sudokuTable) sudoku.SetValues(s2, s.values) return s2, guesses end -- Does this puzzle have a unique solution. It works by trying the brute force -- solution method iterating from low to high numbers and then from high to low. -- This should always find if there are at least two solutions. -- returns nil if no solution or [s1, s2] if at least two solutions, else -- just the single unique solution. -- Returns a new solved table or nil on failure, doesn't modify input sudokuTable. function sudoku.IsUniquePuzzle(sudokuTable) local s1, g1 = sudoku.SolveBruteForce(sudokuTable, false) if not s1 then return nil end local s2, g2 = sudoku.SolveBruteForce(sudokuTable, true) if not s2 then return nil end if not sudoku.IsSamePuzzle(s1, s2) then return s1, s2 end return s1 end -- Do these two puzzles have the same cell values? Returns true/false. function sudoku.IsSamePuzzle(s1, s2) for cell = 1, 81 do if sudoku.GetCellValue(s1, cell) ~= sudoku.GetCellValue(s2, cell) then return false end end return true end -- ============================================================================ -- Create a full puzzle with all the values -- returns sudokuTable, count where count is the number of iterations function sudoku.GeneratePuzzle() local cell = 0 local count = 0 local stuck = {} -- count how many times we've backtracked stuck[cell/9] = count math.randomseed(os.time()) local sudokuTable = sudoku.CreateTable() while cell < 81 do cell = cell + 1 count = count + 1 if sudoku.GeneratePuzzleHook then if not sudoku.GeneratePuzzleHook(count, cell) then return nil, count end end local value = math.random(9) local row, col = sudoku.CellToRowCol(cell) if sudoku.IsValidValue(sudokuTable, row, col, value) then sudoku.SetCellValue(sudokuTable, cell, value) sudoku.RemovePossibleAll(sudokuTable, cell, value) else -- try other values starting at value+1 and wrapping around local set_value = false local i = value + 1 if i > 9 then i = 1 end while i ~= value do if sudoku.IsValidValue(sudokuTable, row, col, i) then sudoku.SetCellValue(sudokuTable, cell, i) sudoku.RemovePossibleAll(sudokuTable, cell, i) set_value = true break end i = i + 1 if i > 9 then i = 1 end end -- whoops, go back a row or more and start over just to be sure if not set_value then local block = math.floor(cell/9) + 1 stuck[block] = (stuck[block] or 0) + 1 local goback = 9 if stuck[block] and (stuck[block] > 5) then goback = 2 * stuck[block] + 1 end local cell_start = cell - goback if cell_start < 1 then cell_start = 1 elseif cell_start < 10 then stuck = {} -- really start all over end for i = cell_start, cell do sudoku.SetCellValue(sudokuTable, i, 0) end cell = cell_start - 1 end end end return sudokuTable, count end function sudoku.GeneratePuzzleDifficulty(sudokuTable, num_cells_to_keep, ensure_unique) if num_cells_to_keep < 1 then return sudoku.CreateTable() end if ensure_unique == nil then ensure_unique = true end math.randomseed(os.time()+1) local trial = 1 local i = 0 local count = 0 local soln = TableCopy(sudokuTable) local cellTable = {} for n = 1, 81 do cellTable[n] = n end local cell_count, cell_n, cell while i < 81 - num_cells_to_keep do cell_count = #cellTable -- restart this function if we run out of cells to try if cell_count + i < num_cells_to_keep then trial = trial + 1 i = 0 count = 0 cellTable = {} for n = 1, 81 do cellTable[n] = n end cell_count = #cellTable cell_n = math.random(cell_count) elseif cell_count == 1 then cell_n = 1 else cell_n = math.random(cell_count) end cell = cellTable[cell_n] count = count + 1 if sudoku.GeneratePuzzleDifficultyHook then if not sudoku.GeneratePuzzleDifficultyHook(count, i, cell, cell_count, trial) then return nil, count end end if ensure_unique == true then -- test if soln going forward is same as soln backwards and original local s = TableCopy(sudokuTable) sudoku.SetCellValue(s, cell, 0) local soln1 = sudoku.SolveBruteForce(s, false) if not soln1 then return nil, count end if not sudoku.IsSamePuzzle(soln, soln1) then table.remove(cellTable, cell_n) else local soln2 = sudoku.SolveBruteForce(s, true) if not soln2 then return nil, count end if not sudoku.IsSamePuzzle(soln, soln2) then table.remove(cellTable, cell_n) else table.remove(cellTable, cell_n) sudoku.SetCellValue(sudokuTable, cell, 0) i = i + 1 end end else table.remove(cellTable, cell_n) sudoku.SetCellValue(sudokuTable, cell, 0) i = i + 1 end end return sudokuTable, count end -- ============================================================================ -- ============================================================================ sudokuGUIxpmdata = { "16 15 7 1", " c None", "a c Black", "b c #808080", "c c #FFFF00", "d c #FF0000", "e c #0000FF", "g c #00FF00", " aaaaaaaaaaaaa ", " addaggaeeaccab ", " addaggaeeaccab ", " aaaaaaaaaaaaab ", " accaddaggaeeab ", " accaddaggaeeab ", " aaaaaaaaaaaaab ", " aeeaccaddaggab ", " aeeaccaddaggab ", " aaaaaaaaaaaaab ", " aggaeeaccaddab ", " aggaeeaccaddab ", " aaaaaaaaaaaaab ", " bbbbbbbbbbbbb ", " " } -- NOTE: HTML generated using NVU and "tidy -wrap 79 -i -omit -o l.html input.html" sudokuGUIhelp = [[ wxLuaSudoku

wxLuaSudoku

Copyright : John Labenski, 2006
License : wxWindows license.

This program is free software; you can redistribute it and/or modify it under the terms of the wxWindows License; either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the wxWindows License for more details.


If you use this program and find any bugs or have added any features that you feel may be generally useful, please feel free to contact me by e-mail at jrl1[at]sourceforge[dot]net or on the wxlua-users mailing list.

Purpose

The purpose of this program is to demonstrate programming in wxLua. It may not be the best, fastest, or most complete sudoku solver program available, but the code should be easily understandable with a straightforward implementation. However, you may find that it is capable enough and since it's free, you got what you paid for.

wxLua is a binding of the wxWidgets cross-platform GUI library to the Lua programming language. It is available as an application, library, or source code for MSW, Unix like systems, and OSX. More information about wxLua can be found on wxlua.sourceforge.net. Information about the wxWidgets library can be found on www.wxwidgets.org and the Lua programming language on www.lua.org.

Program features

How to play

Sudoku is a puzzle game of choosing where to place the numbers 1 to 9 in a 9x9 grid of cells. There are many varieties of this game, but wxLuaSudoku only works with the 9x9 grid. The grid of 81 cells has 9 rows, 9 columns, and 9 blocks that consist of smaller 3x3 grids of cells. Each cell in the grid can take a value of 1 to 9, but there are limitations. Each row, column, and block must have the full array of numbers from 1 to 9. Therefore, there can be no duplicate values in any row, column, or block. For a well formed puzzle there's just one possible solution for the initial seed of values. There are numerous tutorials on the internet, for example http://en.wikipedia.org/wiki/Sudoku, so lets move on...

Some Sudoku terms

Entering values

Enter values into the cells by clicking on a cell to highlight it and then type a number. Use 0, space, or any non number to clear the cell's value. The cells that the puzzle is initialized with are read-only, meaning that you cannot change their values unless you are actually creating a puzzle using the menu item File->Create. Navigate through the cells with the cursor keys or mouse.

Pencil marks

You can annotate the empty cells with a list of possible values that may work by checking the menu item Possible->Show/Edit pencil marks. If you want initialize the pencil marks to be all selected, none selected, or set to the calculated values use the menu items under Possible->Pencil marks. In order to toggle the values, hold down the shift key and all of the possible values will be shown. Click on a value to either select or deselect it. Alternatively, you can use shift-Number to toggle the selection state of the pencil marks.

Note that this is different than the menu item Possible->Show calculated possible in that, obviously, this will show a list of calculated possible values that could go into the cells based on the current state of the puzzle. You can switch between the two views or not show the possible values altogether by unchecking both of them.

Creating a puzzle

To create a new puzzle by hand check the menu item File->Create. You've now put the program in a state where any values that you enter will be the initial values of the puzzle. Error checking of invalid values, values that appear more than once in the same row, column, or block, is automatically turned on since it's fairly pointless to create puzzles that cannot be solved, though this program generously allows you to do so. Enter the values as described above and when done be sure to uncheck the File->Create item. The program will then try to find a solution to the puzzle and verify that there is only one unique solution.

If you want to check the state of the puzzle during creation to confirm that it has a unique solution, use the Solve->Verify unique solution menu item. If you're determined to create an invalid sudoku puzzle this program will not stop you and you can merely cancel out of the warning dialogs to continue and you're on your own. However, showing mistakes will be disabled and "solving" using the scanning technique may yield strange results that are mathematically consistent with the rules of the game, but bring you no closer to the nonexistent solution. If at some later point you wish to have mistakes shown using View->Mark mistakes, the program will try to solve the initial puzzle again, with the same result, no solution.

If you want to start with a completely empty grid use File->New. If on the other hand, you want to start with a completely filled grid of random values, use File->Generate with the number of cell values to show set to 81, all shown.

Cell markings

The colors and fonts for the cells can be changed with the preferences dialog from the menu item View->Preferences. The values in the cells have one color if it's an initial value and a different color if you've entered the value into the cell during play. The possible values that could go into a particular cell use a smaller font and can be drawn as a 3x3 grid or in a single line which may be useful if you want to print it and work it out by hand.

Possible groups of values for the cells may be set to be marked if they are naked and or hidden. Note: It may happen that, for example, there's a triplet of possible values in a row and some of the values are shared by a pair in a column. In that case the pair values get the appropriate markings overriding the triplet markings since it's usually best to try to work with them first. Also, if you get really off track the program will happily mark groups that would obviously make no sense if there weren't any mistakes. This is by design since some people apparently find it interesting to do whatever it is that they want to do.

Menu item descriptions

All items followed by ... denote that a dialog will be shown that can be canceled before any action is taken.

Preferences dialog

In the preferences dialog you can adjust the colors to use for the different elements in a cell. A sample cell is shown to give you an idea of what it'll look like when done. Note that some fonts you may choose will not render properly or may be badly placed in the cells. This is a problem with the font itself, just pick a different font. Also, bitmapped fonts only come in a limited number of sizes and cannot be scaled to arbitrary sizes, again, pick another font that works better on your system.

Additionally, a simpler interface to the menu items Possible->Mark groups and Solve->Eliminate groups is provided to make it easier to check and uncheck multiple items. ]] -- Simple way to generate unique window or menu ids and ensure they're in order function NewID() if not sudokuGUI_ID_New then sudokuGUI_ID_New = wx.wxID_HIGHEST end sudokuGUI_ID_New = sudokuGUI_ID_New + 1 return sudokuGUI_ID_New end sudokuGUI = { frame = nil, -- The wxFrame of the program panel = nil, -- The main wxPanel child of the wxFrame cellWindows = {}, -- The 81 grid cell wxWindows, children of panel cellTextCtrl = nil, -- Single wxTextCtrl editor for entering cell values focused_cell_id = 0, -- window id of the currently focused cell, 0 for none block_refresh = false, -- temporarily block refreshing when true filePath = "", -- last opened filePath fileName = "", -- last opened fileName printData = wx.wxPrintData(), pageSetupData = wx.wxPageSetupDialogData(), config = nil, -- the wxFileConfig for saving preferences Colours = {}, -- table of wxColours to use, indexes below VALUE_COLOUR = 1, INIT_VALUE_COLOUR = 2, POSS_VALUE_COLOUR = 3, INVALID_VALUE_COLOUR = 4, BACKGROUND_COLOUR = 5, ODD_BACKGROUND_COLOUR = 6, FOCUS_CELL_COLOUR = 7, NAKED_PAIRS_COLOUR = 8, NAKED_TRIPLETS_COLOUR = 9, NAKED_QUADS_COLOUR = 10, HIDDEN_PAIRS_COLOUR = 11, HIDDEN_TRIPLETS_COLOUR = 12, HIDDEN_QUADS_COLOUR = 13, COLOUR_MAX = 13, -- A large wxFont for the cell values valueFont = { wxfont = nil, size = 8, width = 0, height = 0 }, -- A smallish wxFont for the possible values possibleFont = { wxfont = nil, size = 6, width = 0, height = 0 }, valueFont_cache = {}, -- valueFont_cache[size] = { width, height } possibleFont_cache = {}, -- possibleFont_cache[size] = { width, height } -- calc positions once in PaintCell -- recalc if any of these parameters change -- pos[value].x and .y are upper left corners possiblePosCache = { pos = {}, width = 1, height = 1, line = false, cell_width = 1, cell_height = 1 }, query_save_prefs = true, -- tell user that prefs are stored as fileconfig ID_NEW = NewID(), ID_CREATE = NewID(), ID_GENERATE = NewID(), ID_OPEN = NewID(), ID_SAVEAS = NewID(), ID_PAGESETUP = NewID(), ID_PRINTSETUP = NewID(), ID_PRINTPREVIEW = NewID(), ID_PRINT = NewID(), ID_EXIT = wx.wxID_EXIT, ID_COPY_PUZZLE = NewID(), ID_RESET = NewID(), ID_UNDO = NewID(), ID_REDO = NewID(), ID_PREFERENCES = NewID(), -- show preferences dialog ID_SAVE_PREFERENCES = NewID(), -- save preferences ID_SHOW_TOOLBAR = NewID(), ID_SHOW_TOOLBAR_LABELS = NewID(), ID_SHOW_STATUSBAR = NewID(), ID_SHOW_ERRORS = NewID(), -- Show errors in the grid ID_SHOW_MISTAKES = NewID(), -- Show mistakes in the grid ID_SHOW_POSSIBLE = NewID(), -- Show the possible values ID_SHOW_USER_POSSIBLE = NewID(), -- Show the user's possible values ID_SHOW_POSSIBLE_LINE = NewID(), -- Show the possible values in a line ID_USER_POSSIBLE_MENU = NewID(), ID_USER_POSSIBLE_CLEAR = NewID(), ID_USER_POSSIBLE_SETALL = NewID(), ID_USER_POSSIBLE_INIT = NewID(), ID_SHOW_MENU = NewID(), ID_SHOW_NAKED = NewID(), -- mark naked groups ID_SHOW_HIDDEN = NewID(), -- mark hidden groups ID_SHOW_NAKEDPAIRS = NewID(), -- mark naked pairs ID_SHOW_HIDDENPAIRS = NewID(), -- mark hidden pairs ID_SHOW_NAKEDTRIPLETS = NewID(), -- mark naked triplets ID_SHOW_HIDDENTRIPLETS = NewID(), -- mark hidden triplets ID_SHOW_NAKEDQUADS = NewID(), -- mark naked quads ID_SHOW_HIDDENQUADS = NewID(), -- mark hidden quads ID_VERIFY_PUZZLE = NewID(), ID_SHOW_SOLUTION = NewID(), -- Show the solution to the puzzle ID_ELIMINATE_MENU = NewID(), ID_ELIMINATE_NAKED = NewID(), -- eliminate naked groups ID_ELIMINATE_HIDDEN = NewID(), -- eliminate hidden groups ID_ELIMINATE_NAKEDPAIRS = NewID(), -- eliminate naked pairs ID_ELIMINATE_HIDDENPAIRS = NewID(), -- eliminate hidden pairs ID_ELIMINATE_NAKEDTRIPLETS = NewID(), -- eliminate naked triplets ID_ELIMINATE_HIDDENTRIPLETS = NewID(), -- eliminate hidden triplets ID_ELIMINATE_NAKEDQUADS = NewID(), -- eliminate naked quads ID_ELIMINATE_HIDDENQUADS = NewID(), -- eliminate hidden quads ID_SOLVE_SCANSINGLES = NewID(), -- Solve for single lone values ID_SOLVE_SCANROWS = NewID(), -- Solve for single values in rows ID_SOLVE_SCANCOLS = NewID(), -- Solve for single values in cols ID_SOLVE_SCANBLOCKS = NewID(), -- Solve for single values in blocks ID_SOLVE_SCANNING = NewID(), -- Solve the puzzle by scanning ID_SOLVE_BRUTEFORCE = NewID(), -- Solve the puzzle by brute force ID_ABOUT = NewID(), ID_HELP = NewID(), menuCheckIDs = {}, -- the current state of the check menu items -- GTK doesn't like to access them in EVT_PAINT -- we don't have to set them since nil == false sudokuTables = {}, -- A table of the sudoku tables for undoing, should always be 1 sudokuTables_pos = 0, -- Current position in the tables sudokuSolnTable = nil, -- solution to the current puzzle nonunique_init_puzzle = nil, -- nil for don't know, is true/false once verified possNakedTable = nil, possHiddenTable = nil, pencilMarks = {}, -- pencilMarks[cell][value] = true/nil pencilMarksNakedTable = nil, pencilMarksHiddenTable = nil, difficulty = 30, -- number of cells to keep for new puzzle sayings_n = 0, -- number of sayings, calculated later sayings = { "Pondering... ", "Musing... ", "Meditating... ", "Contemplating... ", "Reflecting... ", "Mulling... ", "Considering... ", "Deliberating... ", "Thinking... ", "Working... ", "Grinding... ", "Brooding... ", "Languishing? ", "Despairing... ", "To be, or not to be that is the question ", "Whether 'tis nobler in the mind to suffer ", "The slings and arrows of outrageous fortune, ", "Or to take arms against a sea of troubles, ", "And by opposing end them? ", "To die: to sleep; ", "No more; and by a sleep to say we end ", "The heart-ache and the thousand natural shocks", "That flesh is heir to, 'tis a consummation", "Devoutly to be wish'd. To die, to sleep;", "To sleep: perchance to dream:", "ay, there's the rub;", "For in that sleep of death what dreams may come", "When we have shuffled off this mortal coil,", "Must give us pause: there's the respect", "That makes calamity of so long life;", "For who would bear the whips and scorns of time,", "The oppressor's wrong, the proud man's contumely,", "The pangs of despised love, the law's delay,", "The insolence of office and the spurns", "That patient merit of the unworthy takes,", "When he himself might his quietus make", "With a bare bodkin? who would fardels bear,", "To grunt and sweat under a weary life,", "But that the dread of something after death,", "The undiscover'd country from whose bourn", "No traveller returns, puzzles the will", "And makes us rather bear those ills we have", "Than fly to others that we know not of?", "Thus conscience does make cowards of us all;", "And thus the native hue of resolution", "Is sicklied o'er with the pale cast of thought,", "And enterprises of great pitch and moment", "With this regard their currents turn awry,", "And lose the name of action.-- Soft you now!", "The fair Ophelia! Nymph, in thy orisons", "Be all my sins remember'd..." } } sudokuGUI.sayings_n = #sudokuGUI.sayings -- calc number of sayings for cell = 1, 81 do sudokuGUI.pencilMarks[cell] = {} end -- ---------------------------------------------------------------------------- -- Run this function once to find the best font sizes to use and store them -- if height/width = nil then use a default size, -- else try to fit a font size to given height/width function sudokuGUI.GetCellBestSize(cell_width, cell_height) local dc = wx.wxClientDC(sudokuGUI.frame) local size = sudokuGUI.DoGetCellBestSize(dc, cell_width, cell_height, sudokuGUI.valueFont, sudokuGUI.possibleFont, sudokuGUI.valueFont_cache, sudokuGUI.possibleFont_cache) dc:delete() -- ALWAYS delete() any wxDCs created when done return size end function sudokuGUI.DoGetCellBestSize(dc, cell_width, cell_height, valueFont, possibleFont, valueFont_cache, possibleFont_cache) -- cache are optional local function GetBestFontSize(dc, width, height, fontTable, font_cache) local function DoGetBestFontSize(step, largest) for s = fontTable.size, largest, step do fontTable.size = s if font_cache[fontTable.size] then fontTable.width = font_cache[fontTable.size].width fontTable.height = font_cache[fontTable.size].height else fontTable.wxfont:SetPointSize(fontTable.size) dc:SetFont(fontTable.wxfont) fontTable.width, fontTable.height = dc:GetTextExtent("5") font_cache[fontTable.size] = {} font_cache[fontTable.size].width = fontTable.width font_cache[fontTable.size].height = fontTable.height end if (fontTable.height > height) or (fontTable.width > width) then break end end end fontTable.size = 2 if not font_cache then font_cache = {} end -- use local font cache -- skip font sizes by 4 to get a rough estimate of the size DoGetBestFontSize(4, 1000) local largest = fontTable.size fontTable.size = iff(fontTable.size-3 > 1, fontTable.size-3, 2) -- get the best size to use DoGetBestFontSize(1, largest) -- use next smaller value that actually fits fontTable.size = iff(fontTable.size-1 > 1, fontTable.size-1, 2) fontTable.wxfont:SetPointSize(fontTable.size) fontTable.width = font_cache[fontTable.size].width fontTable.height = font_cache[fontTable.size].height return fontTable end if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE) then possibleFont = GetBestFontSize(dc, (cell_width-2)/10, (cell_height-2), possibleFont, possibleFont_cache) else possibleFont = GetBestFontSize(dc, (cell_width-2)/3, (cell_height-2)/3, possibleFont, possibleFont_cache) end valueFont = GetBestFontSize(dc, cell_width-4, cell_height-4, valueFont, valueFont_cache) return wx.wxSize(valueFont.height+4, valueFont.height+4) end -- ---------------------------------------------------------------------------- -- Is this cell in an "odd" block for colouring the blocks function sudokuGUI.IsOddBlockCell(cell) return math.fmod(sudoku.CellToBlock(cell), 2) ~= 0 end -- ---------------------------------------------------------------------------- -- Create one of the 81 cell windows and connect the events to it function sudokuGUI.CreateCellWindow(parent, winID, size) local win = wx.wxWindow(parent, winID, wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxWANTS_CHARS+wx.wxSIMPLE_BORDER) --wxSUNKEN_BORDER) -- set the background colour to reduce flicker if sudokuGUI.IsOddBlockCell(winID) then win:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR]) else win:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR]) end win:Connect(wx.wxEVT_ERASE_BACKGROUND, function(event) end) win:Connect(wx.wxEVT_PAINT, sudokuGUI.OnPaintCellWindow) win:Connect(wx.wxEVT_KEY_DOWN, sudokuGUI.OnKeyDownCellWindow ) win:Connect(wx.wxEVT_KEY_UP, sudokuGUI.OnKeyUpCellWindow ) win:Connect(wx.wxEVT_LEFT_DOWN, sudokuGUI.OnLeftClickCellWindow ) win:Connect(wx.wxEVT_LEFT_DCLICK, sudokuGUI.OnLeftDClickCellWindow ) return win end -- ---------------------------------------------------------------------------- -- wxPaintEvent handler for all of the cell windows function sudokuGUI.OnPaintCellWindow(event) local win = event:GetEventObject():DynamicCast("wxWindow") -- ALWAYS create a wxPaintDC in a wxEVT_PAINT handler, even if unused local dc = wx.wxPaintDC(win) if not sudokuGUI.block_refresh then local cell = win:GetId() local width, height = win:GetClientSizeWH() sudokuGUI.PaintCell(dc, cell, width, height, sudokuGUI.valueFont, sudokuGUI.possibleFont) end dc:delete() -- ALWAYS delete() any wxDCs created when done end function sudokuGUI.PaintCell(dc, cell, width, height, valueFont, possibleFont) -- clear the window before drawing dc:SetPen(wx.wxTRANSPARENT_PEN) local bgColour if sudokuGUI.focused_cell_id ~= cell then if sudokuGUI.IsOddBlockCell(cell) then bgColour = sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR] else bgColour = sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR] end else bgColour = sudokuGUI.Colours[sudokuGUI.FOCUS_CELL_COLOUR] end local brush = wx.wxBrush(bgColour, wx.wxSOLID) dc:SetBrush(brush) dc:DrawRectangle(0, 0, width, height) brush:delete() local sudokuTable = sudokuGUI.GetCurrentTable() local value_str, is_init = sudokuGUI.GetCellValueString(cell) local has_cell_value = string.len(value_str) ~= 0 -- Draw the possible values local show_possible = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE) local show_possible_user = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) local show_possible_line = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE) if (show_possible or show_possible_user) and (not has_cell_value) then local possible = sudoku.GetCellPossible(sudokuTable, cell) if show_possible_user then possible = sudokuGUI.pencilMarks[cell] end dc:SetTextForeground(sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR]) dc:SetFont(possibleFont.wxfont) -- find the positions of each possible value local pos = sudokuGUI.CalcPossiblePositions(width, height, possibleFont) local show_possible_user_all = false if show_possible_user and (cell == sudokuGUI.focused_cell_id) then show_possible_user_all = wx.wxGetKeyState(wx.WXK_SHIFT) end -- draw each one separately, even for line to ensure monospace for i = 1, 9 do if possible and possible[i] then dc:DrawText(tostring(i), pos[i].x, pos[i].y) elseif show_possible_user_all then dc:SetBackgroundMode(wx.wxSOLID) dc:SetTextForeground(bgColour) dc:SetTextBackground(sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR]) dc:DrawText(tostring(i), pos[i].x, pos[i].y) dc:SetTextForeground(sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR]) dc:SetTextBackground(bgColour) dc:SetBackgroundMode(wx.wxTRANSPARENT) end end local nakedTable = sudokuGUI.possNakedTable local hiddenTable = sudokuGUI.possHiddenTable if show_possible_user then nakedTable = sudokuGUI.pencilMarksNakedTable hiddenTable = sudokuGUI.pencilMarksHiddenTable end if nakedTable and hiddenTable then dc:SetBrush(wx.wxTRANSPARENT_BRUSH) local char0 = string.byte("0") local function draw_nakedhidden(colourID, num, key_table, hidden) if (not key_table) or (#key_table < 1) then return end local pen = wx.wxPen(sudokuGUI.Colours[colourID], 1, wx.wxSOLID) dc:SetPen(pen) for k = 1, #key_table do for n = 1, num do local val = string.byte(key_table[k], n)-char0 if hidden ~= true then dc:DrawRectangle(pos[val].x, pos[val].y, possibleFont.width, possibleFont.height) else dc:DrawRoundedRectangle(pos[val].x, pos[val].y, possibleFont.width, possibleFont.height, 20) end end end pen:delete() end -- draw pair marker last so it's on top of the others if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_HIDDENQUADS) then draw_nakedhidden(sudokuGUI.HIDDEN_QUADS_COLOUR, 4, hiddenTable.quads.cells[cell], true) end if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_NAKEDQUADS) then draw_nakedhidden(sudokuGUI.NAKED_QUADS_COLOUR, 4, nakedTable.quads.cells[cell]) end if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_HIDDENTRIPLETS) then draw_nakedhidden(sudokuGUI.HIDDEN_TRIPLETS_COLOUR, 3, hiddenTable.triplets.cells[cell], true) end if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_NAKEDTRIPLETS) then draw_nakedhidden(sudokuGUI.NAKED_TRIPLETS_COLOUR, 3, nakedTable.triplets.cells[cell]) end if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_HIDDENPAIRS) then draw_nakedhidden(sudokuGUI.HIDDEN_PAIRS_COLOUR, 2, hiddenTable.pairs.cells[cell], true) end if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_NAKEDPAIRS) then draw_nakedhidden(sudokuGUI.NAKED_PAIRS_COLOUR, 2, nakedTable.pairs.cells[cell]) end end end -- mark invalid cells, always mark invalid if creating local show_errors = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_ERRORS) if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE) then show_errors = true end if show_errors then show_errors = sudokuGUI.GetCurrentTable().invalid[cell] end local show_mistakes = sudokuGUI.sudokuSolnTable and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) if show_errors then dc:SetPen(wx.wxPen(sudokuGUI.Colours[sudokuGUI.INVALID_VALUE_COLOUR], 1, wx.wxSOLID)) dc:DrawLine(0, 0, width, height) dc:DrawLine(width, 0, 0, height) elseif show_mistakes then if sudoku.HasCellValue(sudokuTable, cell) and (sudoku.GetCellValue(sudokuGUI.sudokuSolnTable, cell) ~= sudoku.GetCellValue(sudokuTable, cell)) then local pen = wx.wxPen(sudokuGUI.Colours[sudokuGUI.INVALID_VALUE_COLOUR], 1, wx.wxSOLID) dc:SetPen(pen) pen:delete() dc:DrawLine(0, 0, width, height) end end -- Draw the set value, if any if has_cell_value then local fgColour = sudokuGUI.Colours[sudokuGUI.VALUE_COLOUR] if is_init then fgColour = sudokuGUI.Colours[sudokuGUI.INIT_VALUE_COLOUR] end dc:SetTextForeground(fgColour) dc:SetFont(valueFont.wxfont) dc:DrawText(value_str, width/2 - valueFont.width/2, height/2 - valueFont.height/2) end end -- ---------------------------------------------------------------------------- function sudokuGUI.CalcPossiblePositions(width, height, possibleFont) local pos = {} local show_possible_line = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE) if (sudokuGUI.possiblePosCache.pos and (sudokuGUI.possiblePosCache.cell_width == width) and (sudokuGUI.possiblePosCache.cell_height == height) and (sudokuGUI.possiblePosCache.width == possibleFont.width) and (sudokuGUI.possiblePosCache.height == possibleFont.height) and (sudokuGUI.possiblePosCache.line == show_possible_line)) then pos = sudokuGUI.possiblePosCache.pos else if show_possible_line then for i = 1, 9 do pos[i] = { x = (1+ (i-1)*possibleFont.width), y = 1 } end else local c = 0 local horiz_space = (width - 3*possibleFont.width)/4 local vert_space = (height - 3*possibleFont.height)/4 local h_space = horiz_space local v_space = vert_space for j = 1, 3 do -- try to center it a little better if (j == 1) and (vert_space - math.floor(vert_space) > .3) then v_space = vert_space+1 else v_space = vert_space end for i = 1, 3 do c = c + 1 if (i == 1) and (horiz_space - math.floor(horiz_space) > .3) then h_space = horiz_space+1 else h_space = horiz_space end pos[c] = { x = i*h_space + (i-1)*possibleFont.width, y = j*v_space + (j-1)*possibleFont.height } end end end -- cache these values for next cell sudokuGUI.possiblePosCache.pos = pos sudokuGUI.possiblePosCache.cell_width = width sudokuGUI.possiblePosCache.cell_height = height sudokuGUI.possiblePosCache.width = possibleFont.width sudokuGUI.possiblePosCache.height = possibleFont.height sudokuGUI.possiblePosCache.line = show_possible_line end return pos end -- ---------------------------------------------------------------------------- function sudokuGUI.ConnectPrintEvents(printOut) printOut:SetPageInfo(1, 1, 1, 1) printOut.HasPage = function(self, pageNum) return pageNum == 1 end --printOut.GetPageInfo = function(self) return 1, 1, 1, 1 end printOut.OnPrintPage = function(self, pageNum) local dc = self:GetDC() local ppiScr_width, ppiScr_height = self:GetPPIScreen() local ppiPrn_width, ppiPrn_height = self:GetPPIPrinter() local ppi_scale_x = ppiPrn_width/ppiScr_width local ppi_scale_y = ppiPrn_height/ppiScr_height -- Get the size of DC in pixels and the number of pixels in the page local dc_width, dc_height = dc:GetSize() local pagepix_width, pagepix_height = self:GetPageSizePixels() local dc_pagepix_scale_x = dc_width/pagepix_width local dc_pagepix_scale_y = dc_height/pagepix_height -- If printer pageWidth == current DC width, then this doesn't -- change. But w might be the preview bitmap width, so scale down. local dc_scale_x = ppi_scale_x * dc_pagepix_scale_x local dc_scale_y = ppi_scale_y * dc_pagepix_scale_y -- calculate the pixels / mm (25.4 mm = 1 inch) local ppmm_x = ppiScr_width / 25.4 local ppmm_y = ppiScr_height / 25.4 -- Adjust the page size for the pixels / mm scaling factor local pageMM_width, pageMM_height = self:GetPageSizeMM() local pagerect_x, pagerect_y = 0, 0 local pagerect_w, pagerect_h = pageMM_width * ppmm_x, pageMM_height * ppmm_y -- get margins informations and convert to printer pixels local topLeft = sudokuGUI.pageSetupData:GetMarginTopLeft() local bottomRight = sudokuGUI.pageSetupData:GetMarginBottomRight() local top = topLeft:GetY() * ppmm_y local bottom = bottomRight:GetY() * ppmm_y local left = topLeft:GetX() * ppmm_x local right = bottomRight:GetX() * ppmm_x local printrect_x, printrect_y = left, top local printrect_w, printrect_h = pagerect_w-(left+right), pagerect_h-(top+bottom) dc:SetUserScale(dc_scale_x, dc_scale_y); local cell_width = (printrect_w)/11 local cell_height = (printrect_h)/11 if cell_width < cell_height then cell_height = cell_width end if cell_width > cell_height then cell_width = cell_height end -- calculate font sizes for the printout, copy font since we'll recalc the size local valueFont = { wxfont = wx.wxFont(sudokuGUI.valueFont.wxfont), size = 8, width = 0, height = 0 } local possibleFont = { wxfont = wx.wxFont(sudokuGUI.possibleFont.wxfont), size = 6, width = 0, height = 0 } sudokuGUI.DoGetCellBestSize(dc, cell_width, cell_height, valueFont, possibleFont) local function RowOrigin(row) return printrect_x + row*cell_height + row end local function ColOrigin(col) return printrect_y + col*cell_width + col end local old_focused_cell_id = sudokuGUI.focused_cell_id -- clear focus sudokuGUI.focused_cell_id = 0 for row = 1, 9 do for col = 1, 9 do local x = ColOrigin(col) local y = RowOrigin(row) dc:SetDeviceOrigin(x*dc_scale_x, y*dc_scale_x) local cell = sudoku.RowColToCell(row, col) sudokuGUI.PaintCell(dc, cell, cell_width, cell_height, valueFont, possibleFont) end end valueFont.wxfont:delete() possibleFont.wxfont:delete() sudokuGUI.focused_cell_id = old_focused_cell_id -- restore focus dc:SetDeviceOrigin(0, 0) local borders = { [1]=true, [4]=true, [7]=true, [10]=true } for i = 1, 10 do local pen = wx.wxPen(wx.wxBLACK, iff(borders[i], 4, 2), wx.wxSOLID) dc:SetPen(pen) pen:delete() dc:DrawLine(ColOrigin(1), RowOrigin(i), ColOrigin(10), RowOrigin(i)) dc:DrawLine(ColOrigin(i), RowOrigin(1), ColOrigin(i), RowOrigin(10)) end return true end end function sudokuGUI.Print() local printDialogData = wx.wxPrintDialogData(sudokuGUI.printData) local printer = wx.wxPrinter(printDialogData) local luaPrintout = wx.wxLuaPrintout("wxLuaSudoku Printout") sudokuGUI.ConnectPrintEvents(luaPrintout) if printer:Print(sudokuGUI.frame, luaPrintout, true) == false then if printer:GetLastError() == wx.wxPRINTER_ERROR then wx.wxMessageBox("There was a problem printing.\n".. "Perhaps your current printer is not setup correctly?", "wxLuaSudoku Printout", wx.wxOK, sudokuGUI.frame) end else sudokuGUI.printData = printer:GetPrintDialogData():GetPrintData():Copy() end end function sudokuGUI.PrintPreview() luaPrintout = wx.wxLuaPrintout("wxLuaSudoku Print Preview") luaPrintPrintout = wx.wxLuaPrintout("wxLuaSudoku Printout") sudokuGUI.ConnectPrintEvents(luaPrintout) sudokuGUI.ConnectPrintEvents(luaPrintPrintout) local printDialogData = wx.wxPrintDialogData(sudokuGUI.printData):GetPrintData() local preview = wx.wxPrintPreview(luaPrintout, luaPrintPrintout, printDialogData) local result = preview:Ok() if result == false then wx.wxMessageBox("There was a problem previewing.\n".. "Perhaps your current printer is not setup correctly?", "wxLuaSudoku print preview error", wx.wxOK, sudokuGUI.frame) else local previewFrame = wx.wxPreviewFrame(preview, sudokuGUI.frame, "wxLuaSudoku print preview", wx.wxDefaultPosition, wx.wxSize(600, 600)) previewFrame:Connect(wx.wxEVT_CLOSE_WINDOW, function (event) previewFrame:Destroy() event:Skip() end ) previewFrame:Centre(wx.wxBOTH) previewFrame:Initialize() previewFrame:Show(true) end end function sudokuGUI.PrintSetup() -- FIXME DEPRICATED IN WXWIDGETS? local printDialogData = wx.wxPrintDialogDataFromPrintData(sudokuGUI.printData) local printerDialog = wx.wxPrintDialog(sudokuGUI.frame, printDialogData) printerDialog:GetPrintDialogData():SetSetupDialog(true) printerDialog:ShowModal() sudokuGUI.printData = printerDialog:GetPrintDialogData():GetPrintData():Copy() --printerDialog:Destroy() end function sudokuGUI.PageSetup() sudokuGUI.printData = sudokuGUI.pageSetupData:GetPrintData():Copy() local pageSetupDialog = wx.wxPageSetupDialog(sudokuGUI.frame, sudokuGUI.pageSetupData) pageSetupDialog:ShowModal() sudokuGUI.printData = pageSetupDialog:GetPageSetupDialogData():GetPrintData():Copy() sudokuGUI.pageSetupData = pageSetupDialog:GetPageSetupDialogData():Copy() --pageSetupDialog:Destroy() end -- ---------------------------------------------------------------------------- -- Set the currently focused window, refresh new and old function sudokuGUI.SetFocusWindow(cell) local last_id = sudokuGUI.focused_cell_id sudokuGUI.focused_cell_id = iff((cell>=1) and (cell<=81), cell, 0) if sudokuGUI.cellWindows[last_id] then sudokuGUI.cellWindows[last_id]:Refresh() end if sudokuGUI.cellWindows[cell] then sudokuGUI.cellWindows[cell]:Refresh() end end -- ---------------------------------------------------------------------------- function sudokuGUI.OnKeyUpCellWindow(event) event:Skip() -- we don't care who actually got this event, just use the "focused cell" if (sudokuGUI.focused_cell_id < 1) then return end local key = event:GetKeyCode() if (sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) == true) and (key == wx.WXK_SHIFT) then sudokuGUI.cellWindows[sudokuGUI.focused_cell_id]:Refresh(false) return end end -- Left down click handler for the cell windows, hide the cell editor function sudokuGUI.OnKeyDownCellWindow(event) event:Skip() -- we don't care who actually got this event, just use the "focused cell" if (sudokuGUI.focused_cell_id < 1) then return end local key = event:GetKeyCode() if (sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) == true) and (key == wx.WXK_SHIFT) then sudokuGUI.cellWindows[sudokuGUI.focused_cell_id]:Refresh(false) end if event:HasModifiers() or event:AltDown() or event:ControlDown() then sudokuGUI.cellWindows[sudokuGUI.focused_cell_id]:Refresh(false) return end -- clear the current focused window if (key == wx.WXK_ESCAPE) then sudokuGUI.SetFocusWindow(0) return end -- the cursor keys move the focus cell local movefocus = { [wx.WXK_LEFT] = -1, [wx.WXK_NUMPAD_LEFT] = -1, [wx.WXK_UP] = -9, [wx.WXK_NUMPAD_UP] = -9, [wx.WXK_RIGHT] = 1, [wx.WXK_NUMPAD_RIGHT] = 1, [wx.WXK_DOWN] = 9, [wx.WXK_NUMPAD_DOWN] = 9, [wx.WXK_PAGEUP] = -9, [wx.WXK_PRIOR] = -9, [wx.WXK_PAGEDOWN] = 9, [wx.WXK_NEXT] = 9, [wx.WXK_NUMPAD_HOME] = -10, [wx.WXK_NUMPAD_PAGEUP] = -8, [wx.WXK_NUMPAD_PRIOR] = -8, [wx.WXK_NUMPAD_END] = 8, [wx.WXK_NUMPAD_PAGEDOWN] = 10, [wx.WXK_NUMPAD_NEXT] = 10, [wx.WXK_TAB] = 1, [wx.WXK_RETURN] = 1, [wx.WXK_NUMPAD_ENTER] = 1 } if (key == wx.WXK_HOME) then sudokuGUI.SetFocusWindow(1) return elseif (key == wx.WXK_END) then sudokuGUI.SetFocusWindow(81) return elseif movefocus[key] then local cell = sudokuGUI.focused_cell_id + movefocus[key] if (cell >= 1) and (cell <= 81) then sudokuGUI.SetFocusWindow(cell) end return end -- translate number pad keys to numbers local numpad = { [wx.WXK_NUMPAD0] = 0, [wx.WXK_NUMPAD1] = 1, [wx.WXK_NUMPAD2] = 2, [wx.WXK_NUMPAD3] = 3, [wx.WXK_NUMPAD4] = 4, [wx.WXK_NUMPAD5] = 5, [wx.WXK_NUMPAD6] = 6, [wx.WXK_NUMPAD7] = 7, [wx.WXK_NUMPAD8] = 8, [wx.WXK_NUMPAD9] = 9, [wx.WXK_DELETE] = 0, [wx.WXK_BACK] = 0, [wx.WXK_SPACE] = 0, [wx.WXK_NUMPAD_INSERT] = 0, [wx.WXK_NUMPAD_DECIMAL] = 0, [wx.WXK_NUMPAD_DELETE] = 0, } local zero = string.byte("0") if numpad[key] then key = zero + numpad[key] end if (key < 32) or (key > 127) then return end -- normal ASCII key local one = string.byte("1") local nine = string.byte("9") if (key >= one) and (key <= nine) then key = key - one + 1 else key = 0 end if event:ShiftDown() and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) then if (key >= 1) and (key <= 9) then sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][key] = iff(sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][key], nil, key) sudokuGUI.UpdateTable() end else sudokuGUI.SetCellValue(sudokuGUI.focused_cell_id, key) end end -- ---------------------------------------------------------------------------- -- Test to see if one of the possible values has been clicked on function sudokuGUI.HitTestPossibleValue(mx, my) local cell = sudokuGUI.focused_cell_id if (cell < 1) or (cell > 81) then return nil end local w = sudokuGUI.possiblePosCache.width local h = sudokuGUI.possiblePosCache.height local rect = wx.wxRect(0, 0, w, h) for n = 1, 9 do rect.X = sudokuGUI.possiblePosCache.pos[n].x rect.Y = sudokuGUI.possiblePosCache.pos[n].y if rect:Inside(mx, my) then return n end end return nil end -- ---------------------------------------------------------------------------- -- Left down click handler for the cell windows, hide the cell editor function sudokuGUI.OnLeftClickCellWindow(event) event:Skip() local win = event:GetEventObject():DynamicCast("wxWindow") local winId = win:GetId() if sudokuGUI.cellTextCtrl then sudokuGUI.SaveCellTextCtrlValue() sudokuGUI.DestroyCellTextCtrl() end sudokuGUI.SetFocusWindow(winId) if event:ShiftDown() and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) then local p = sudokuGUI.HitTestPossibleValue(event:GetX(), event:GetY()) if p then sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][p] = iff(sudokuGUI.pencilMarks[sudokuGUI.focused_cell_id][p], nil, p) sudokuGUI.cellWindows[winId]:Refresh(false) end end end -- Left double click handler for the cell windows, hide old, show new cell editor function sudokuGUI.OnLeftDClickCellWindow(event) event:Skip() local win = event:GetEventObject():DynamicCast("wxWindow") local winId = win:GetId() local winWidth, winHeight = win:GetSizeWH() if event:ShiftDown() then return end if sudokuGUI.cellTextCtrl then if sudokuGUI.cellTextCtrl:GetId() == winId then sudokuGUI.cellTextCtrl:Show(true) return end sudokuGUI.SaveCellTextCtrlValue() sudokuGUI.DestroyCellTextCtrl() end local is_creating = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE) local value, is_init = sudokuGUI.GetCellValueString(winId) if is_init and (not is_creating) then return end sudokuGUI.cellTextCtrl = wx.wxTextCtrl(win, winId, value, wx.wxPoint(0, 0), wx.wxSize(winWidth, winHeight), wx.wxTE_PROCESS_ENTER+wx.wxTE_CENTRE) sudokuGUI.cellTextCtrl:SetFont(sudokuGUI.valueFont.wxfont) sudokuGUI.cellTextCtrl:SetMaxLength(1) --local valid = wx.wxTextValidator(wx.wxFILTER_INCLUDE_LIST) --valid:SetIncludeList( --cellTextCtrl:SetValidator(valid) sudokuGUI.cellTextCtrl:Connect(winId, wx.wxEVT_COMMAND_TEXT_ENTER, function (event) local win = event:GetEventObject():DynamicCast("wxWindow") sudokuGUI.SaveCellTextCtrlValue() win:Show(false) -- just hide it, we'll destroy it later end) sudokuGUI.cellTextCtrl:Connect(wx.wxEVT_CHAR, function (event) if (event:GetKeyCode() == wx.WXK_ESCAPE) then sudokuGUI.cellTextCtrl:Show(false) sudokuGUI.cellTextCtrl:SetValue("") end event:Skip() end) end function sudokuGUI.DestroyCellTextCtrl() if sudokuGUI.cellTextCtrl then sudokuGUI.cellTextCtrl:Show(false) sudokuGUI.cellTextCtrl:Destroy() sudokuGUI.cellTextCtrl = nil end end -- Save the value of the text editor back to the grid function sudokuGUI.SaveCellTextCtrlValue() if not sudokuGUI.cellTextCtrl then return end local value = sudokuGUI.cellTextCtrl:GetValue() local cell = sudokuGUI.cellTextCtrl:GetId() sudokuGUI.SetCellValue(cell, value) end -- ---------------------------------------------------------------------------- -- Set the value of the cell from the value string function sudokuGUI.SetCellValue(cell, value) -- fix the value to something reasonable if type(value) == "string" then if (value == "") or (value == " ") or (value == "0") then value = 0 elseif (string.len(value) ~= 1) or (not string.find("123456789", value)) then return else value = tonumber(value) end end -- if value is still bad, just exit if not ((value == 0) or sudoku.IsValidValueN(value)) then return end local is_creating = sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE) local row, col = sudoku.CellToRowCol(cell) local is_init = sudoku.HasCellValue(sudokuGUI.sudokuTables[1], cell) if is_creating then if sudoku.GetCellValue(sudokuGUI.sudokuTables[1], cell) ~= value then -- add the value to all the tables since it's an init value for n = 1, TableCount(sudokuGUI.sudokuTables) do sudoku.SetValue(sudokuGUI.sudokuTables[n], row, col, value) sudoku.UpdateTable(sudokuGUI.sudokuTables[n]) end -- refresh all in case it's invalid also if not at 1st table update possible sudokuGUI.UpdateTable(true) sudokuGUI.sudokuSolnTable = nil -- don't know anymore end else local s = sudokuGUI.GetCurrentTable() if (not is_init) and (sudoku.GetCellValue(s, cell) ~= value) then local s = TableCopy(s) sudoku.SetValue(s, row, col, value) sudokuGUI.AddTable(s) end end sudokuGUI.UpdateGUI() end -- ---------------------------------------------------------------------------- -- Get the initial sudoku table function sudokuGUI.GetInitTable() return sudokuGUI.sudokuTables[1] end -- Set the initial sudoku table, clearing all others function sudokuGUI.SetInitTable(sudokuTable, solnTable) sudokuGUI.sudokuSolnTable = solnTable sudokuGUI.sudokuTables_pos = 1 sudokuGUI.sudokuTables = {} table.insert(sudokuGUI.sudokuTables, sudokuTable) sudokuGUI.UpdateTable() -- resets possible and refreshes too end -- ---------------------------------------------------------------------------- -- Get the current sudoku table to use function sudokuGUI.GetCurrentTable() return sudokuGUI.sudokuTables[sudokuGUI.sudokuTables_pos] end -- Set the current sudoku table to this table function sudokuGUI.SetCurrentTable(sudokuTable) sudokuGUI.sudokuTables[sudokuGUI.sudokuTables_pos] = sudokuTable end -- ---------------------------------------------------------------------------- -- Add a sudoku table to the list of tables, removing any past the current position, -- find possible, and refresh function sudokuGUI.AddTable(sudokuTable) while TableCount(sudokuGUI.sudokuTables) > sudokuGUI.sudokuTables_pos do table.remove(sudokuGUI.sudokuTables) end -- clear calculated values to save memory for n = 2, sudokuGUI.sudokuTables_pos do sudokuGUI.sudokuTables[n].row_values = {} sudokuGUI.sudokuTables[n].col_values = {} sudokuGUI.sudokuTables[n].block_values = {} sudokuGUI.sudokuTables[n].possible = {} sudokuGUI.sudokuTables[n].invalid = {} end table.insert(sudokuGUI.sudokuTables, sudokuTable) sudokuGUI.sudokuTables_pos = sudokuGUI.sudokuTables_pos + 1 sudokuGUI.UpdateTable() end -- ---------------------------------------------------------------------------- -- Get the value of the cell as a printable string function sudokuGUI.GetCellValueString(cell) local value = "" if sudoku.HasCellValue(sudokuGUI.sudokuTables[1], cell) then return tostring(sudoku.GetCellValue(sudokuGUI.sudokuTables[1], cell)), true elseif sudoku.HasCellValue(sudokuGUI.GetCurrentTable(), cell) then value = tostring(sudoku.GetCellValue(sudokuGUI.GetCurrentTable(), cell)) end return value, false end -- ---------------------------------------------------------------------------- -- refresh all the grid cells function sudokuGUI.Refresh() for i = 1, 81 do if sudokuGUI.cellWindows[i] then sudokuGUI.cellWindows[i]:Refresh(false) end end sudokuGUI.UpdateGUI() end -- ---------------------------------------------------------------------------- -- Create a new empty puzzle function sudokuGUI.NewPuzzle() local ret = wx.wxMessageBox("Clear all the values in the current puzzle and start anew?\n".. "Use 'Create' to enter the initial values.", "wxLuaSudoku - New puzzle?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxOK then sudokuGUI.SetInitTable(sudoku.CreateTable(), nil) end end -- ---------------------------------------------------------------------------- -- Create a puzzle by hand function sudokuGUI.CreatePuzzle(init) local enableIds = { sudokuGUI.ID_GENERATE, sudokuGUI.ID_OPEN, sudokuGUI.ID_SAVEAS, sudokuGUI.ID_RESET, sudokuGUI.ID_UNDO, sudokuGUI.ID_REDO, sudokuGUI.ID_SOLVE_SCANSINGLES, sudokuGUI.ID_SOLVE_SCANROWS, sudokuGUI.ID_SOLVE_SCANCOLS, sudokuGUI.ID_SOLVE_SCANBLOCKS, sudokuGUI.ID_SOLVE_SCANNING, sudokuGUI.ID_SOLVE_BRUTEFORCE } sudokuGUI.CheckMenuItem(sudokuGUI.ID_CREATE, init) if init then local ret = wx.wxMessageBox( "Enter values in the cells to initialize the puzzle with.\n".. "Previous cell values will be overwritten.\n".. "Don't forget to uncheck 'Create' before playing.", "wxLuaSudoku - Initialize puzzle?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxCANCEL then sudokuGUI.CheckMenuItem(sudokuGUI.ID_CREATE, false) return end else sudokuGUI.sudokuSolnTable = nil -- reset to unknown if not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid) then -- try to make them correct the puzzle local ret = wx.wxMessageBox( "The initial puzzle you've created has invalid values.\n".. "Press 'Ok' to correct them before continuing.\n".. "If you press 'Cancel' showing mistakes will be disabled and ".. "don't blame me if things don't work out for you.", "wxLuaSudoku - Invalid initial puzzle!", wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR, sudokuGUI.frame ) if ret == wx.wxOK then sudokuGUI.CheckMenuItem(sudokuGUI.ID_CREATE, true) init = true end else --if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) then sudokuGUI.sudokuSolnTable = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable()) end if (not sudokuGUI.sudokuSolnTable) and sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) then sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_MISTAKES, false) end end for n, id in pairs(enableIds) do sudokuGUI.frame:GetMenuBar():Enable(id, not init) sudokuGUI.frame:GetToolBar():EnableTool(id, not init) end sudokuGUI.UpdateTable() end -- ---------------------------------------------------------------------------- -- Generate a new puzzle automatically function sudokuGUI.GeneratePuzzle() local keep = wx.wxGetNumberFromUser("Set the difficulty of the new puzzle by clearing cells.\n".. "Note: The minimum number of cells to show for a unique puzzle is 17.", "Number of cell values to show", "wxLuaSudoku - Generate puzzle?", sudokuGUI.difficulty, 1, 81, sudokuGUI.frame) if keep < 1 then return end -- canceled sudokuGUI.difficulty = keep local solve_progress = 0 local start_time = os.time() local last_time = start_time local solve_ok = true local msg_idx = 1 local progressDialog = wx.wxProgressDialog("wxLuaSudoku - Generating...", string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[1], 0, 0), 1000, sudokuGUI.frame, wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME) -- define handler function here so it'll work w/o gui function sudoku.GeneratePuzzleHook(count, cell) if solve_ok == false then return false end -- canceled solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1) if solve_progress%10 ~= 0 then return true end if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then msg_idx = msg_idx + 1 last_time = os.time() end local msg = string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[msg_idx], count, cell) solve_ok = progressDialog:Update(solve_progress, msg) return solve_ok end local s, count = sudoku.GeneratePuzzle() progressDialog:Destroy() if not s then return end -- have complete puzzle, now remove cells local diff_count = 0 local diff_i = 0 local diff_cell = 0 local diff_open = 0 local diff_trial = 0 -- define handler function here so it'll work w/o gui function sudoku.GeneratePuzzleDifficultyHook(count, i, cell, open_cells, trial) diff_count = count diff_i = i diff_cell = cell diff_open = open_cells diff_trial = trial if solve_ok == false then return false end -- canceled if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then msg_idx = msg_idx + 1 last_time = os.time() end local msg = string.format("%s\nTrial %d, Iteration # %d, current cell %d, cells to go %d, available cells %d ", sudokuGUI.sayings[msg_idx], trial, count, cell, 81-keep-i, open_cells) solve_ok = progressDialog:Update(i, msg) return solve_ok end -- hook into brute force solver to update the generate puzzle progress dialog function sudoku.SolveBruteForceHook(guesses, cell) solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1) if solve_progress%10 ~= 0 then return true end return sudoku.GeneratePuzzleDifficultyHook(diff_count, diff_i, diff_cell, diff_open, diff_trial) end local ensure_unique = true while 1 do diff_count = 0 diff_i = 0 diff_cell = 0 diff_open = 0 diff_trial = 0 solve_progress = 0 start_time = os.time() last_time = start_time solve_ok = true msg_idx = 1 local caption = "wxLuaSudoku - Ensuring unique solution..." if ensure_unique == false then caption = "wxLuaSudoku - Removing values randomly..." end progressDialog = wx.wxProgressDialog(caption, string.format("%s\nTrial %d, Iteration # %d, current cell %d, cells to go %d, available cells %d ", sudokuGUI.sayings[msg_idx], 0, count, 0, 81, 0), 81 - sudokuGUI.difficulty + 1, sudokuGUI.frame, wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME) local s1 = sudoku.GeneratePuzzleDifficulty(TableCopy(s), sudokuGUI.difficulty, ensure_unique) progressDialog:Destroy() if s1 then if ensure_unique then sudokuGUI.SetInitTable(s1, TableCopy(s)) else -- verify the puzzle anyway to let them know the status local s2 = sudokuGUI.VerifyUniquePuzzle(s1) sudokuGUI.SetInitTable(s1, s2) end break else local ret = wx.wxMessageBox("The puzzle was not fully generated. ".. "Press 'Ok' to randomly remove cell values which may or may not ".. "yield a unique puzzle or 'Cancel' to abort", "wxLuaSudoku - Unfinished generation", wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR, sudokuGUI.frame) if ret == wx.wxOK then ensure_unique = false else break end end end end -- ---------------------------------------------------------------------------- -- Open a puzzle from a file function sudokuGUI.OpenPuzzle() local fileDialog = wx.wxFileDialog(sudokuGUI.frame, "Open file", sudokuGUI.filePath, sudokuGUI.fileName, "wxLuaSudoku files (*.sudoku)|*.sudoku|All files (*)|*", wx.wxOPEN + wx.wxFILE_MUST_EXIST) if fileDialog:ShowModal() == wx.wxID_OK then local fileName = fileDialog:GetPath() local fn = wx.wxFileName(fileName) sudokuGUI.filePath = fn:GetPath() sudokuGUI.fileName = fn:GetFullName() local s, msg = sudoku.Open(fileName) if s then sudokuGUI.frame:SetTitle("wxLuaSudoku - "..sudokuGUI.fileName) sudokuGUI.SetInitTable(s, nil) if not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid) then -- make them correct the puzzle local ret = wx.wxMessageBox( "The puzzle you've opened has invalid values.\n".. "Press 'Ok' to correct them using 'Create' before continuing ".. "otherwise 'Cancel' to ignore them.", "wxLuaSudoku - Invalid puzzle", wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR, sudokuGUI.frame ) if ret == wx.wxOK then sudokuGUI.CreatePuzzle(true) end else --if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_MISTAKES) then sudokuGUI.sudokuSolnTable = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable()) end else wx.wxMessageBox( msg, "wxLuaSudoku - Open file error", wx.wxOK + wx.wxICON_ERROR, sudokuGUI.frame ) end end fileDialog:Destroy() end -- ---------------------------------------------------------------------------- -- Save the puzzle to a file function sudokuGUI.SaveAsPuzzle() local fileDialog = wx.wxFileDialog(sudokuGUI.frame, "Save puzzle", sudokuGUI.filePath, sudokuGUI.fileName, "wxLuaSudoku files (*.sudoku)|*.sudoku|All files (*)|*", wx.wxSAVE + wx.wxOVERWRITE_PROMPT) local result = false if fileDialog:ShowModal() == wx.wxID_OK then local fileName = fileDialog:GetPath() local fn = wx.wxFileName(fileName) sudokuGUI.filePath = fn:GetPath() sudokuGUI.fileName = fn:GetFullName() result = sudoku.Save(sudokuGUI.GetCurrentTable(), fileName) if result then sudokuGUI.frame:SetTitle("wxLuaSudoku - "..sudokuGUI.fileName) else wx.wxMessageBox( "Unable to save file\n'"..fileName.."'", "wxLuaSudoku - Save file error", wx.wxOK + wx.wxICON_ERROR, sudokuGUI.frame ) end end fileDialog:Destroy() return result end -- ---------------------------------------------------------------------------- function sudokuGUI.Undo() if sudokuGUI.sudokuTables_pos > 1 then sudokuGUI.sudokuTables_pos = sudokuGUI.sudokuTables_pos - 1 sudokuGUI.UpdateTable(true) end end function sudokuGUI.Redo() if sudokuGUI.sudokuTables_pos < TableCount(sudokuGUI.sudokuTables) then sudokuGUI.sudokuTables_pos = sudokuGUI.sudokuTables_pos + 1 sudokuGUI.UpdateTable(true) end end -- ---------------------------------------------------------------------------- -- Try to fix the invalid cells if possible function sudokuGUI.FixInvalid(sudokuTable, show_dialog) local s = TableCopy(sudokuTable) local solnTable = sudokuGUI.sudokuSolnTable if solnTable and TableIsEmpty(solnTable.invalid) then for cell = 1, 81 do if sudoku.HasCellValue(s, cell) then local current_value = sudoku.GetCellValue(s, cell) local correct_value = sudoku.GetCellValue(solnTable, cell) if current_value ~= correct_value then sudoku.SetCellValue(s, cell, correct_value) end end end sudoku.UpdateTable(s) return s else if show_dialog then local msg = "The initial puzzle must be solved first.\n".. "Would you like me to try to solve it?" local flags = wx.wxYES_NO local invalid = true if solnTable and not TableIsEmpty(solnTable.invalid) then invalid = true end if invalid then msg = "The initial puzzle has invalid values.\n".. "Please correct them first using Create." flags = wx.wxOK end local ret = wx.wxMessageBox(msg, "wxLuaSudoku - Invalid puzzle", flags + wx.wxICON_INFORMATION, sudokuGUI.frame ) if not invalid and (ret == wx.wxOK) then s = sudokuGUI.SolveBruteForce(sudokuGUI.sudokuTables[1]) if not s then wx.wxMessageBox("Unable to solve or or solving was aborted, giving up.", "wxLuaSudoku - Invalid puzzle", wx.wxOK + wx.wxICON_INFORMATION, sudokuGUI.frame ) return nil end sudokuGUI.sudokuSolnTable = s sudoku.UpdateTable(sudokuGUI.sudokuSolnTable) return sudokuGUI.FixInvalid(sudokuTable, show_dialog) else return nil end else return nil end end end -- ---------------------------------------------------------------------------- function sudokuGUI.VerifyUniquePuzzle(sudokuTable) sudoku.CalcInvalidCells(sudokuTable) local invalid_count = TableCount(sudokuTable.invalid) if invalid_count > 0 then local ret = wx.wxMessageBox( string.format("The initial values of the puzzle are invalid.\n".. "There are %d cells with duplicate values.\n".. "Please select Create and fix them before trying to solve.\n", invalid_count), "wxLuaSudoku - Invalid puzzle", wx.wxOK + wx.wxICON_ERROR, sudokuGUI.frame ) return end local solve_progress = 0 local start_time = os.time() local last_time = start_time local solve_ok = true local msg_idx = 1 -- define handler function here so it'll work w/o gui function sudoku.SolveBruteForceHook(guesses, cell) if solve_ok == false then return false end -- canceled solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1) if (solve_progress-1)%10 ~= 0 then return true end if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then msg_idx = msg_idx + 1 last_time = os.time() end local msg = string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[msg_idx], guesses.current, cell) solve_ok = progressDialog:Update(solve_progress, msg) return solve_ok end local ret = wx.wxOK while ret == wx.wxOK do solve_progress = 0 start_time = os.time() last_time = start_time solve_ok = true msg_idx = 1 progressDialog = wx.wxProgressDialog("wxLuaSudoku - Verifying puzzle...", string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[1], 0, 0), 1000, sudokuGUI.frame, wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME) local s1, s2 = sudoku.IsUniquePuzzle(sudokuTable) progressDialog:Destroy() if s1 and (s2 == nil) then return s1 elseif solve_ok == false then ret = wx.wxMessageBox("The puzzle was not fully verified and therefore may not have a unique solution or a solution at all.\n".. "Press 'Ok' to restart checking or 'Cancel' to quit.", "wxLuaSudoku - Unfinished check", wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR, sudokuGUI.frame) elseif s1 and s2 then wx.wxMessageBox("The puzzle does not have a unique solution.\n".. "Use 'Create' to fix the problem, showing mistakes will be disabled.", "wxLuaSudoku - Nonunique puzzle", wx.wxOK + wx.wxICON_ERROR, sudokuGUI.frame) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_MISTAKES, false) return nil else wx.wxMessageBox("The puzzle does not have a solution.\n".. "Use 'Create' to fix the problem, showing mistakes will be disabled.", "wxLuaSudoku - Unsolvable puzzle", wx.wxOK + wx.wxICON_ERROR, sudokuGUI.frame) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_MISTAKES, false) return nil end end return nil end -- ---------------------------------------------------------------------------- -- Use the scanning method to solve it function sudokuGUI.SolveScanning() local s = TableCopy(sudokuGUI.GetCurrentTable()) --[[ local invalid = not TableIsEmpty(s.invalid) if invalid then if not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid) then local ret = wx.wxMessageBox("The initial values in the puzzle are invalid.\n".. "Please select Create and fix them before trying to solve.\n".. "Press 'Cancel' to try to solve it anyway.", "wxLuaSudoku - Invalid puzzle", wx.wxOK + wx.wxCANCEL + wx.wxICON_ERROR, sudokuGUI.frame ) if ret == wx.wxOK then return end else local ret = wx.wxMessageBox("The current puzzle has invalid cell values.\n".. "Would you like me to try to fix those cells or press cancel to abort solving.", "wxLuaSudoku - Invalid puzzle", wx.wxYES_NO + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxCANCEL then return elseif ret == wx.wxYES then local fixedS = sudokuGUI.FixInvalid(s, true) if fixedS then sudokuGUI.AddTable(fixedS) s = TableCopy(sudokuGUI.GetCurrentTable()) else return end end end end ]] local count, changed_cells = sudoku.SolveScan(s) local changed_count = 0 if changed_cells then sudokuGUI.AddTable(s) changed_count = TableCount(changed_cells) end local msg = string.format("Scanned rows, cols, and blocks %d times.\n".. "Found %d new values.\n".. "You may be able to do better using 'Eliminate groups'", count, changed_count) wx.wxMessageBox( msg, "wxLuaSudoku - Finished scanning", wx.wxOK + wx.wxICON_INFORMATION, sudokuGUI.frame ) end -- ---------------------------------------------------------------------------- -- Use the brute force method to solve it function sudokuGUI.SolveBruteForce(sudokuTable) local s if sudokuTable then s = TableCopy(sudokuTable) else s = TableCopy(sudokuGUI.GetCurrentTable()) end --[[ local invalid = not TableIsEmpty(s.invalid) if invalid then if (sudokuTable == nil) and (not TableIsEmpty(sudokuGUI.sudokuTables[1].invalid)) then wx.wxMessageBox("The initial values in the puzzle are invalid.\n".. "Please select Create and fix them before trying to solve.", "wxLuaSudoku - Invalid puzzle", wx.wxOK + wx.wxICON_ERROR, sudokuGUI.frame ) return end local ret = wx.wxMessageBox("The current puzzle has invalid cell values.\n".. "Would you like me to try to fix those cells or press cancel to abort solving.", "wxLuaSudoku - Invalid puzzle", wx.wxYES_NO + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxCANCEL then return elseif ret == wx.wxYES then local fixedS = sudokuGUI.FixInvalid(s, true) if fixedS then sudokuGUI.AddTable(fixedS) s = TableCopy(sudokuGUI.GetCurrentTable()) else return end end end ]] local progressDialog = wx.wxProgressDialog("wxLuaSudoku - Solving...", string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[1], 0, 0), 1000, sudokuGUI.frame, wx.wxPD_AUTO_HIDE+wx.wxPD_CAN_ABORT+wx.wxPD_ELAPSED_TIME) local solve_progress = 0 local start_time = os.time() local last_time = start_time local solve_ok = true local msg_idx = 1 -- define handler function here so it'll work w/o gui function sudoku.SolveBruteForceHook(guesses, cell) if solve_ok == false then return false end -- canceled solve_progress = iff(solve_progress+1 >= 1000, 0, solve_progress + 1) if (solve_progress-1)%10 ~= 0 then return true end if (msg_idx < sudokuGUI.sayings_n) and (os.time() - last_time > 4) then msg_idx = msg_idx + 1 last_time = os.time() end local msg = string.format("%s\nIteration # %d, current cell %d ", sudokuGUI.sayings[msg_idx], guesses.current, cell) solve_ok = progressDialog:Update(solve_progress, msg) return solve_ok end -- "cheat" a little by using SolveScan to get easy to find values --local flags = TableCopy(s.flags) --for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do -- s.flags[n] = true --end --local count, changed_cells = sudoku.SolveScan(s) local s, g = sudoku.SolveBruteForce(s) progressDialog:Destroy() if not s then if solve_ok then wx.wxMessageBox("Sorry, no solutions found!", "wxLuaSudoku - error", wx.wxOK + wx.wxICON_INFORMATION, sudokuGUI.frame ) end elseif not sudokuTable then --s.flags = flags -- restore flags sudokuGUI.AddTable(s) -- we solved the current grid else --s.flags = flags -- restore flags return s -- we solved the input grid end end -- ---------------------------------------------------------------------------- -- Reset the grid to the original values function sudokuGUI.ResetPuzzle(dont_query_user) dont_query_user = dont_query_user or false local ret = wx.wxOK if not dont_query_user then ret = wx.wxMessageBox("Reset the puzzle to the initial state?", "wxLuaSudoku - reset puzzle?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) end if ret == wx.wxCANCEL then return else sudokuGUI.sudokuTables_pos = 1 while TableCount(sudokuGUI.sudokuTables) > 1 do table.remove(sudokuGUI.sudokuTables, 2) end end sudokuGUI.UpdateTable() -- redo it anyway end -- ---------------------------------------------------------------------------- function sudokuGUI.UpdateTable(refresh) if refresh == nil then refresh = true end local sudokuTable = sudokuGUI.GetCurrentTable() sudokuGUI.block_refresh = true local has_show_flag = false for n = sudoku.ELIMINATE_FLAG_MIN, sudoku.ELIMINATE_FLAG_MAX do local id = n + sudokuGUI.ID_ELIMINATE_NAKEDPAIRS - sudoku.ELIMINATE_FLAG_MIN sudokuTable.flags[n] = sudokuGUI.IsCheckedMenuItem(id) local show_id = n + sudokuGUI.ID_SHOW_NAKEDPAIRS - sudoku.ELIMINATE_FLAG_MIN if (not has_show_flag) and (sudokuGUI.IsCheckedMenuItem(show_id) == true) then has_show_flag = true end end sudoku.UpdateTable(sudokuTable) if has_show_flag == true then -- swap out the possible table temporarily to calc pencil marks if sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE) then local p = sudokuTable.possible sudokuTable.possible = sudokuGUI.pencilMarks sudokuGUI.pencilMarksNakedTable, sudokuGUI.pencilMarksHiddenTable = sudoku.FindAllNakedHiddenGroups(sudokuTable, true) sudokuTable.possible = p else sudokuGUI.possNakedTable, sudokuGUI.possHiddenTable = sudoku.FindAllNakedHiddenGroups(sudokuTable, true) end end sudokuGUI.SetCurrentTable(sudokuTable) sudokuGUI.block_refresh = false if (refresh == true) then sudokuGUI.Refresh() end sudokuGUI.UpdateGUI() end -- ---------------------------------------------------------------------------- function sudokuGUI.UpdateGUI() local table_count = #sudokuGUI.sudokuTables local table_pos = sudokuGUI.sudokuTables_pos sudokuGUI.frame:GetMenuBar():Enable(sudokuGUI.ID_UNDO, table_pos > 1) sudokuGUI.frame:GetMenuBar():Enable(sudokuGUI.ID_REDO, table_pos < table_count) sudokuGUI.frame:GetToolBar():EnableTool(sudokuGUI.ID_UNDO, table_pos > 1) sudokuGUI.frame:GetToolBar():EnableTool(sudokuGUI.ID_REDO, table_pos < table_count) sudokuGUI.frame:SetStatusText(string.format("Step : %d", table_pos), 1) end -- ---------------------------------------------------------------------------- -- The preference pages are in a table so they can be accessed easily sudokuGUI.PreferencesDialogPageUI = {} function sudokuGUI.PreferencesDialogPageUI.Create(parent) local panel = wx.wxPanel(parent, wx.wxID_ANY) local ID_LISTBOX = 10 local ID_SAMPLE_TEXT = 11 local ID_FONT_BUTTON = 12 local ID_COLOUR_BUTTON = 13 local ID_RESET_BUTTON = 14 local listStrings = -- in same order as the colours { "Values", "Initial values", "Possible values", "Invalid values", "Background", "Odd background", "Focused cell", "Naked pairs", "Naked triplets", "Naked quads", "Hidden pairs", "Hidden triplets", "Hidden quads" } local listBoxValues = {} for n = 1, sudokuGUI.COLOUR_MAX do table.insert(listBoxValues, {colour = wx.wxColour(sudokuGUI.Colours[n])}) end listBoxValues[sudokuGUI.VALUE_COLOUR].font = wx.wxFont(sudokuGUI.valueFont.wxfont) listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font = wx.wxFont(sudokuGUI.possibleFont.wxfont) local reset_fonts = true -- Create the dialog ------------------------------------------------------ local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL ) local fcFlexSizer = wx.wxFlexGridSizer( 1, 2, 0, 0 ) fcFlexSizer:AddGrowableCol( 0 ) fcFlexSizer:AddGrowableRow( 0 ) local fcListBox = wx.wxListBox( panel, ID_LISTBOX, wx.wxDefaultPosition, wx.wxSize(80,100), listStrings, wx.wxLB_SINGLE ) fcListBox:SetSelection(0) fcFlexSizer:Add( fcListBox, 0, wx.wxGROW+wx.wxALIGN_CENTER_HORIZONTAL+wx.wxALL, 5 ) local fcBoxSizer = wx.wxBoxSizer( wx.wxVERTICAL ) local sampleWin = wx.wxWindow(panel, ID_SAMPLE_TEXT, wx.wxDefaultPosition, wx.wxSize(140,140)) fcBoxSizer:Add( sampleWin, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 ); local fontButton = wx.wxButton( panel, ID_FONT_BUTTON, "Choose Font", wx.wxDefaultPosition, wx.wxDefaultSize, 0 ) fcBoxSizer:Add( fontButton, 0, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL+wx.wxALL, 5 ) local colourButton = wx.wxButton( panel, ID_COLOUR_BUTTON, "Choose Color", wx.wxDefaultPosition, wx.wxDefaultSize, 0 ) fcBoxSizer:Add( colourButton, 0, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL+wx.wxALL, 5 ) local resetButton = wx.wxButton( panel, ID_RESET_BUTTON, "Reset Value...", wx.wxDefaultPosition, wx.wxDefaultSize, 0 ) fcBoxSizer:Add( resetButton, 0, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL+wx.wxALL, 5 ) fcFlexSizer:Add( fcBoxSizer, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 ) mainSizer:Add( fcFlexSizer, 1, wx.wxGROW+wx.wxALIGN_CENTER_VERTICAL, 5 ) panel:SetSizer( mainSizer ) sampleWin:Connect(wx.wxEVT_PAINT, function (event) local win = event:GetEventObject():DynamicCast("wxWindow") local sel = fcListBox:GetSelection() + 1 local width, height = win:GetClientSizeWH() local dc = wx.wxPaintDC(win) local function SetFontSize(size, width, height, font) -- alternate way, but it fails for fonts that can't scale large enough --local f = wx.wxNullFont:NewSize(wx.wxSize(width, height), font:GetFamily(), font:GetStyle(), font:GetWeight(), font:GetUnderlined(), font:GetFaceName()) --font:SetPointSize(f:GetPointSize()) local font_width = 0 local font_height = 0 while (font_width < width) and (font_height < height) do font:SetPointSize(size) dc:SetFont(font) font_width, font_height = dc:GetTextExtent("5") size = size + 2 if size > 200 then break end -- oops bad font? end font:SetPointSize(size-1) end -- clear background local c = listBoxValues[sudokuGUI.BACKGROUND_COLOUR].colour if (sel == sudokuGUI.ODD_BACKGROUND_COLOUR) or (sel == sudokuGUI.FOCUS_CELL_COLOUR) then c = listBoxValues[sel].colour end local brush = wx.wxBrush(c, wx.wxSOLID) dc:SetBrush(brush) brush:delete() dc:DrawRectangle(0, 0, width, height) -- draw possible values dc:SetTextForeground(listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].colour) local font = listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font if reset_fonts then SetFontSize(4, width/4, height/4, font) end dc:SetFont(font) local font_width, font_height = dc:GetTextExtent("5") local pos = { [1] = { x = 2, y = 2 }, [3] = { x = width-font_width-2, y = 2 }, [4] = { x = 2, y = (height-font_height)/2-2 }, [6] = { x = width-font_width-2, y = (height-font_height)/2-2 }, [7] = { x = 2, y = height-font_height-2 }, [9] = { x = width-font_width-2, y = height-font_height-2 } } dc:SetBrush(wx.wxTRANSPARENT_BRUSH) local function DrawPossible(idx, n, value, hidden) dc:DrawText(value, pos[n].x, pos[n].y) local pen = wx.wxPen(listBoxValues[idx].colour, 1, wx.wxSOLID) dc:SetPen(pen); pen:delete() if hidden ~= true then dc:DrawRectangle(pos[n].x, pos[n].y, font_width, font_height) else dc:DrawRoundedRectangle(pos[n].x, pos[n].y, font_width, font_height, 20) end end DrawPossible(sudokuGUI.NAKED_PAIRS_COLOUR, 1, "2") DrawPossible(sudokuGUI.NAKED_TRIPLETS_COLOUR, 4, "3") DrawPossible(sudokuGUI.NAKED_QUADS_COLOUR, 7, "4") DrawPossible(sudokuGUI.HIDDEN_PAIRS_COLOUR, 3, "2", true) DrawPossible(sudokuGUI.HIDDEN_TRIPLETS_COLOUR, 6, "3", true) DrawPossible(sudokuGUI.HIDDEN_QUADS_COLOUR, 9, "4", true) -- draw invalid marker local pen = wx.wxPen(listBoxValues[sudokuGUI.INVALID_VALUE_COLOUR].colour, 1, wx.wxSOLID) dc:SetPen(pen); pen:delete() dc:DrawLine(0, 0, width, height) -- draw value if (sel == sudokuGUI.INIT_VALUE_COLOUR) then dc:SetTextForeground(listBoxValues[sudokuGUI.INIT_VALUE_COLOUR].colour) else dc:SetTextForeground(listBoxValues[sudokuGUI.VALUE_COLOUR].colour) end local old_font = font local font = listBoxValues[sudokuGUI.VALUE_COLOUR].font if reset_fonts then SetFontSize(old_font:GetPointSize(), width-2, height-2, font) end dc:SetFont(font) local font_width, font_height = dc:GetTextExtent("9") dc:DrawText("9", (width-font_width)/2, (height-font_height)/2) reset_fonts = false dc:delete() end) panel:Connect(ID_LISTBOX, wx.wxEVT_COMMAND_LISTBOX_SELECTED, function (event) local sel = event:GetSelection() + 1 panel:FindWindow(ID_FONT_BUTTON):Enable(listBoxValues[sel].font ~= nil) colourButton:SetForegroundColour(listBoxValues[sel].colour) sampleWin:Refresh(false) end) panel:Connect(ID_FONT_BUTTON, wx.wxEVT_COMMAND_BUTTON_CLICKED, function (event) local sel = fcListBox:GetSelection() + 1 local f = listBoxValues[sel].font f = wx.wxGetFontFromUser(panel, f) if f:Ok() then listBoxValues[sel].font:delete() listBoxValues[sel].font = f reset_fonts = true else f:delete() end sampleWin:Refresh(false) end) panel:Connect(ID_COLOUR_BUTTON, wx.wxEVT_COMMAND_BUTTON_CLICKED, function (event) local sel = fcListBox:GetSelection() + 1 local c = listBoxValues[sel].colour c = wx.wxGetColourFromUser(panel, c) if c:Ok() then listBoxValues[sel].colour:delete() listBoxValues[sel].colour = c colourButton:SetForegroundColour(c) else c:delete() end sampleWin:Refresh(false) end) panel:Connect(ID_RESET_BUTTON, wx.wxEVT_COMMAND_BUTTON_CLICKED, function (event) local sel = fcListBox:GetSelection() + 1 local ret = wx.wxMessageBox( "Press 'Yes' to reset all the colors and fonts or 'No' to reset only just the selected item.", "wxLuaSudoku - Reset colors or fonts?", wx.wxYES_NO + wx.wxCANCEL + wx.wxICON_INFORMATION, panel ) if ret == wx.wxYES then for n = 1, sudokuGUI.COLOUR_MAX do listBoxValues[n].colour:delete() listBoxValues[n].colour = wx.wxColour(sudokuGUI.Colours_[n]) end listBoxValues[sudokuGUI.VALUE_COLOUR].font:delete() listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font:delete() listBoxValues[sudokuGUI.VALUE_COLOUR].font = wx.wxFont(sudokuGUI.valueFont_wxfont_) listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font = wx.wxFont(sudokuGUI.possibleFont_wxfont_) elseif ret == wx.wxNO then listBoxValues[sel].colour:delete() listBoxValues[sel].colour = wx.wxColour(sudokuGUI.Colours_[sel]) if (sel == sudokuGUI.VALUE_COLOUR) then listBoxValues[sel].font:delete() listBoxValues[sel].font = wx.wxFont(sudokuGUI.valueFont_wxfont_) elseif (sel == sudokuGUI.POSS_VALUE_COLOUR) then listBoxValues[sel].font:delete() listBoxValues[sel].font = wx.wxFont(sudokuGUI.possibleFont_wxfont_) end end colourButton:SetForegroundColour(listBoxValues[sel].colour) reset_fonts = true sampleWin:Refresh(false) end) function sudokuGUI.PreferencesDialogPageUI.Apply() for n = 1, sudokuGUI.COLOUR_MAX do sudokuGUI.Colours[n]:delete() sudokuGUI.Colours[n] = wx.wxColour(listBoxValues[n].colour) end -- copy the fonts since when applied their size will change sudokuGUI.valueFont.wxfont:delete() sudokuGUI.valueFont.wxfont = wx.wxFont(listBoxValues[sudokuGUI.VALUE_COLOUR].font) sudokuGUI.possibleFont.wxfont:delete() sudokuGUI.possibleFont.wxfont = wx.wxFont(listBoxValues[sudokuGUI.POSS_VALUE_COLOUR].font) sudokuGUI.valueFont_cache = {} -- clear cache so GetCellBestSize recreates it sudokuGUI.possibleFont_cache = {} for winID = 1, 81 do if sudokuGUI.IsOddBlockCell(winID) then sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR]) else sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR]) end end local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH() sudokuGUI.GetCellBestSize(width, height) sudokuGUI.Refresh() end function sudokuGUI.PreferencesDialogPageUI.Destroy() for n = 1, sudokuGUI.COLOUR_MAX do listBoxValues[n].colour:delete() if listBoxValues[n].font then listBoxValues[n].font:delete() end end end return panel end function sudokuGUI.CheckListBoxCheck(clBox, n_start, n_end, check) for n = n_start, n_end do clBox:Check(n, check) end end function sudokuGUI.CheckListBoxIsChecked(clBox, n_start, n_end) for n = n_start, n_end do if not clBox:IsChecked(n) then return false end end return true end sudokuGUI.PreferencesDialogPageShow = {} function sudokuGUI.PreferencesDialogPageShow.Create(parent) local panel = wx.wxPanel(parent, wx.wxID_ANY) local ID_LISTBOX = 10 local listStrings = { "All naked groups", "All hidden groups", "Naked pairs", "Naked triplets", "Naked quads", "Hidden pairs", "Hidden triplets", "Hidden quads" } local listBoxValues = { sudokuGUI.ID_SHOW_NAKED, sudokuGUI.ID_SHOW_HIDDEN, sudokuGUI.ID_SHOW_NAKEDPAIRS, sudokuGUI.ID_SHOW_NAKEDTRIPLETS, sudokuGUI.ID_SHOW_NAKEDQUADS, sudokuGUI.ID_SHOW_HIDDENPAIRS, sudokuGUI.ID_SHOW_HIDDENTRIPLETS, sudokuGUI.ID_SHOW_HIDDENQUADS } -- Create the dialog ------------------------------------------------------ local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL ) local showListBox = wx.wxCheckListBox( panel, ID_LISTBOX, wx.wxDefaultPosition, wx.wxSize(80,100), listStrings, wx.wxLB_SINGLE ) mainSizer:Add( showListBox, 1, wx.wxGROW+wx.wxALIGN_CENTER_HORIZONTAL+wx.wxALL, 5 ) panel:SetSizer( mainSizer ) for n = 1, showListBox:GetCount() do showListBox:Check(n-1, sudokuGUI.IsCheckedMenuItem(listBoxValues[n])) end panel:Connect(ID_LISTBOX, wx.wxEVT_COMMAND_CHECKLISTBOX_TOGGLED, function (event) local sel = event:GetSelection() local checked = showListBox:IsChecked(sel) local id = listBoxValues[sel+1] if id == sudokuGUI.ID_SHOW_NAKED then sudokuGUI.CheckListBoxCheck(showListBox, 2, 4, checked) elseif id == sudokuGUI.ID_SHOW_HIDDEN then sudokuGUI.CheckListBoxCheck(showListBox, 5, 7, checked) else showListBox:Check(0, sudokuGUI.CheckListBoxIsChecked(showListBox, 2, 4)) showListBox:Check(1, sudokuGUI.CheckListBoxIsChecked(showListBox, 5, 7)) end end) function sudokuGUI.PreferencesDialogPageShow.Apply() for n = 1, showListBox:GetCount() do sudokuGUI.CheckMenuItem(listBoxValues[n], showListBox:IsChecked(n-1)) end sudokuGUI.UpdateTable() end function sudokuGUI.PreferencesDialogPageShow.Destroy() end return panel end sudokuGUI.PreferencesDialogPageSolve = {} function sudokuGUI.PreferencesDialogPageSolve.Create(parent) local panel = wx.wxPanel(parent, wx.wxID_ANY) local ID_LISTBOX = 10 local listStrings = { "All naked groups", "All hidden groups", "Naked pairs", "Naked triplets", "Naked quads", "Hidden pairs", "Hidden triplets", "Hidden quads" } local listBoxValues = { sudokuGUI.ID_ELIMINATE_NAKED, sudokuGUI.ID_ELIMINATE_HIDDEN, sudokuGUI.ID_ELIMINATE_NAKEDPAIRS, sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS, sudokuGUI.ID_ELIMINATE_NAKEDQUADS, sudokuGUI.ID_ELIMINATE_HIDDENPAIRS, sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS, sudokuGUI.ID_ELIMINATE_HIDDENQUADS } -- Create the dialog ------------------------------------------------------ local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL ) local showListBox = wx.wxCheckListBox( panel, ID_LISTBOX, wx.wxDefaultPosition, wx.wxSize(80,100), listStrings, wx.wxLB_SINGLE ) mainSizer:Add( showListBox, 1, wx.wxGROW+wx.wxALIGN_CENTER_HORIZONTAL+wx.wxALL, 5 ) panel:SetSizer( mainSizer ) for n = 1, showListBox:GetCount() do showListBox:Check(n-1, sudokuGUI.IsCheckedMenuItem(listBoxValues[n])) end panel:Connect(ID_LISTBOX, wx.wxEVT_COMMAND_CHECKLISTBOX_TOGGLED, function (event) local sel = event:GetSelection() local checked = showListBox:IsChecked(sel) local id = listBoxValues[sel+1] if id == sudokuGUI.ID_ELIMINATE_NAKED then sudokuGUI.CheckListBoxCheck(showListBox, 2, 4, checked) elseif id == sudokuGUI.ID_ELIMINATE_HIDDEN then sudokuGUI.CheckListBoxCheck(showListBox, 5, 7, checked) else showListBox:Check(0, sudokuGUI.CheckListBoxIsChecked(showListBox, 2, 4)) showListBox:Check(1, sudokuGUI.CheckListBoxIsChecked(showListBox, 5, 7)) end end) function sudokuGUI.PreferencesDialogPageSolve.Apply() for n = 1, showListBox:GetCount() do sudokuGUI.CheckMenuItem(listBoxValues[n], showListBox:IsChecked(n-1)) end sudokuGUI.UpdateTable() end function sudokuGUI.PreferencesDialogPageSolve.Destroy() end return panel end function sudokuGUI.PreferencesDialog() local dialog = wx.wxDialog(sudokuGUI.frame, wx.wxID_ANY, "wxLuaSudoku - Preferences", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxDEFAULT_DIALOG_STYLE+wx.wxRESIZE_BORDER) local panel = wx.wxPanel(dialog, wx.wxID_ANY) local notebook = wx.wxNotebook(panel, wx.wxID_ANY) local notebookPages = {} local page1 = sudokuGUI.PreferencesDialogPageUI.Create(notebook) notebook:AddPage(page1, "Fonts and Colors", true) table.insert(notebookPages, sudokuGUI.PreferencesDialogPageUI) local page2 = sudokuGUI.PreferencesDialogPageShow.Create(notebook) notebook:AddPage(page2, "Mark groups", false) table.insert(notebookPages, sudokuGUI.PreferencesDialogPageShow) local page3 = sudokuGUI.PreferencesDialogPageSolve.Create(notebook) notebook:AddPage(page3, "Eliminate groups", false) table.insert(notebookPages, sudokuGUI.PreferencesDialogPageSolve) local mainSizer = wx.wxBoxSizer( wx.wxVERTICAL ) local buttonSizer = wx.wxBoxSizer( wx.wxHORIZONTAL ) local okButton = wx.wxButton( panel, wx.wxID_OK, "&OK", wx.wxDefaultPosition, wx.wxDefaultSize, 0 ) buttonSizer:Add( okButton, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 ) local cancelButton = wx.wxButton( panel, wx.wxID_CANCEL, "&Cancel", wx.wxDefaultPosition, wx.wxDefaultSize, 0 ) buttonSizer:Add( cancelButton, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 ) local applyButton = wx.wxButton( panel, wx.wxID_APPLY, "&Apply", wx.wxDefaultPosition, wx.wxDefaultSize, 0 ) buttonSizer:Add( applyButton, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 ) mainSizer:Add( notebook, 1, wx.wxGROW+wx.wxALIGN_CENTER, 0 ) mainSizer:Add( buttonSizer, 0, wx.wxALIGN_CENTER+wx.wxALL, 5 ) panel:SetSizer( mainSizer ) mainSizer:SetSizeHints( dialog ) dialog:Connect(wx.wxID_APPLY, wx.wxEVT_COMMAND_BUTTON_CLICKED, function (event) --local sel = notebook:GetSelection() --if sel >= 0 then notebookPages[sel+1].Apply() end for n = 1, #notebookPages do notebookPages[n].Apply() end end) dialog:Connect(wx.wxID_OK, wx.wxEVT_COMMAND_BUTTON_CLICKED, function (event) for n = 1, #notebookPages do notebookPages[n].Apply() notebookPages[n].Destroy() end event:Skip() -- wxDialog will cancel automatically end) dialog:Connect(wx.wxID_CANCEL, wx.wxEVT_COMMAND_BUTTON_CLICKED, function (event) for n = 1, #notebookPages do notebookPages[n].Destroy() end event:Skip() -- wxDialog will cancel automatically end) dialog:ShowModal() end -- ---------------------------------------------------------------------------- function sudokuGUI.ConfigSave(save_prefs) if not sudokuGUI.config then sudokuGUI.config = wx.wxFileConfig("wxLuaSudoku", "wxLua") end if not sudokuGUI.config then return end -- write the frame position so we can restore it local x, y = sudokuGUI.frame:GetPositionXY() local w, h = sudokuGUI.frame:GetClientSizeWH() local max = booltoint(sudokuGUI.frame:IsMaximized()) sudokuGUI.config:Write("wxLuaSudoku/Frame", string.format("x:%d y:%d w:%d h:%d maximized:%d", x, y, w, h, max)) if not save_prefs then return end if sudokuGUI.query_save_prefs then local ret = wx.wxMessageBox( "Preferences are stored in an ini file which you may delete:\n".. "MSW : Documents and Settings\\user\\wxLuaSudoku.ini\n".. "Unix : /home/user/.wxLuaSudoku", "wxLuaSudoku - Save preferences?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxCANCEL then return end sudokuGUI.query_save_prefs = false end if sudokuGUI.config then sudokuGUI.ConfigReadWrite(false, sudokuGUI.config) sudokuGUI.config:Flush(true) end end function sudokuGUI.ConfigLoad() if not sudokuGUI.config then sudokuGUI.config = wx.wxFileConfig("wxLuaSudoku", "wxLua") end if sudokuGUI.config then local dispX, dispY, dispW, dispH = wx.wxClientDisplayRect() local _, str = sudokuGUI.config:Read("wxLuaSudoku/Frame") local x, y, w, h, max = string.match(str, "x:(%d+) y:(%d+) w:(%d+) h:(%d+) maximized:(%d+)") if (x ~= nil) and (y ~= nil) and (w ~= nil) and (h ~= nil) and (max ~= nil) then x = tonumber(x); y = tonumber(y); w = tonumber(w); h = tonumber(h) max = inttobool(tonumber(max)) if max then sudokuGUI.frame:Maximize(true) else if x < dispX - 5 then x = 0 end if y < dispY - 5 then y = 0 end if w > dispW then w = dispW end if h > dispH then h = dispH end sudokuGUI.frame:Move(x, y) sudokuGUI.frame:SetClientSize(w, h) end end sudokuGUI.ConfigReadWrite(true, sudokuGUI.config) end for winID = 1, 81 do if sudokuGUI.IsOddBlockCell(winID) then sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR]) else sudokuGUI.cellWindows[winID]:SetBackgroundColour(sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR]) end end local show_toolbar = sudokuGUI.frame:GetMenuBar():IsChecked(sudokuGUI.ID_SHOW_TOOLBAR) if sudokuGUI.frame:GetToolBar():IsShown() ~= show_toolbar then -- generate fake event to simplify processing local evt = wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.ID_SHOW_TOOLBAR) evt:SetInt(booltoint(show_toolbar)) sudokuGUI.OnMenuEvent(evt) end local show_toolbar_labels = sudokuGUI.frame:GetMenuBar():IsChecked(sudokuGUI.ID_SHOW_TOOLBAR_LABELS) if (bit.band(sudokuGUI.frame:GetToolBar():GetWindowStyleFlag(), wx.wxTB_TEXT) ~= 0) ~= show_toolbar_labels then -- generate fake event to simplify processing local evt = wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.ID_SHOW_TOOLBAR_LABELS) evt:SetInt(booltoint(show_toolbar_labels)) sudokuGUI.OnMenuEvent(evt) end local show_statusbar = sudokuGUI.frame:GetMenuBar():IsChecked(sudokuGUI.ID_SHOW_STATUSBAR) if sudokuGUI.frame:GetStatusBar():IsShown() ~= show_statusbar then -- generate fake event to simplify processing local evt = wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.ID_SHOW_STATUSBAR) evt:SetInt(booltoint(show_statusbar)) sudokuGUI.OnMenuEvent(evt) end sudokuGUI.valueFont_cache = {} -- clear cache in case the font has changed sudokuGUI.possibleFont_cache = {} -- update font size local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH() sudokuGUI.GetCellBestSize(width, height) -- update for preferences sudokuGUI.UpdateTable() sudokuGUI.Refresh() end function sudokuGUI.ConfigReadWrite(read, config) local path = "wxLuaSudoku" local function ReadWriteColour(key, c) if read then if config:HasEntry(key) then local _, str = config:Read(key) local r, g, b = string.match(str, "r:(%d+) g:(%d+) b:(%d+)") if (r == nil) or (g == nil) or (b == nil) then return end r = tonumber(r); g = tonumber(g); b = tonumber(b) if (r < 0) or (r > 255) then return end -- sanity check if (g < 0) or (g > 255) then return end if (b < 0) or (b > 255) then return end c:Set(r, g, b) end else config:Write(key, string.format("r:%d g:%d b:%d", c:Red(), c:Green(), c:Blue())) end end local function ReadWriteFont(key, f) if read then if config:HasEntry(key) then local _, str = config:Read(key) local face, family, style, underlined, weight = string.match(str, "face:(\"[%w ]+\") family:(%d+) style:(%d+) underlined:(%d+) weight:(%d+)") if (face == nil) or (family == nil) or (style == nil) or (underlined == nil) or (weight == nil) then return end family = tonumber(family); style = tonumber(style); underlined = inttobool(tonumber(underlined)); weight = tonumber(weight) -- remove quotes if string.len(face) > 2 then face = string.sub(face, 2, -2) end -- test so see if the values are any good local ff = wx.wxFont(12, family, style, weight, underlined, face) if not ff:Ok() then return end local tempF = wx.wxFont(f) f:SetFaceName(face) f:SetFamily(family) f:SetStyle(style) f:SetUnderlined(underlined) f:SetWeight(weight) -- shouldn't happen but we always want a usable font if not f:Ok() then f:SetFaceName(tempF:GetFaceName()) f:SetFamily(tempF:GetFamily()) f:SetStyle(tempF:GetStyle()) f:SetUnderlined(tempF:GetUnderlined()) f:SetWeight(tempF:GetWeight()) end end else config:Write(key, string.format("face:\"%s\" family:%d style:%d underlined:%d weight:%d", f:GetFaceName(), f:GetFamily(), f:GetStyle(), booltoint(f:GetUnderlined()), f:GetWeight())) end end if read then local _ if config:HasEntry(path.."/LastOpenedFilepath") then _, sudokuGUI.filePath = config:Read(path.."/LastOpenedFilepath", "") end if config:HasEntry(path.."/LastOpenedFilename") then _, sudokuGUI.fileName = config:Read(path.."/LastOpenedFilename", "") end if config:HasEntry(path.."/GenerateDifficulty") then _, sudokuGUI.difficulty = config:Read(path.."/GenerateDifficulty", 0) end else config:Write(path.."/LastOpenedFilepath", sudokuGUI.filePath) config:Write(path.."/LastOpenedFilename", sudokuGUI.fileName) config:Write(path.."/GenerateDifficulty", sudokuGUI.difficulty) end ReadWriteColour(path.."/Colours/Value", sudokuGUI.Colours[sudokuGUI.VALUE_COLOUR]) ReadWriteColour(path.."/Colours/ValueInit", sudokuGUI.Colours[sudokuGUI.INIT_VALUE_COLOUR]) ReadWriteColour(path.."/Colours/ValuePossible", sudokuGUI.Colours[sudokuGUI.POSS_VALUE_COLOUR]) ReadWriteColour(path.."/Colours/ValueInvalid", sudokuGUI.Colours[sudokuGUI.INVALID_VALUE_COLOUR]) ReadWriteColour(path.."/Colours/CellBackground", sudokuGUI.Colours[sudokuGUI.BACKGROUND_COLOUR]) ReadWriteColour(path.."/Colours/CellOddBackground", sudokuGUI.Colours[sudokuGUI.ODD_BACKGROUND_COLOUR]) ReadWriteColour(path.."/Colours/CellFocus", sudokuGUI.Colours[sudokuGUI.FOCUS_CELL_COLOUR]) ReadWriteColour(path.."/Colours/NakedPairs", sudokuGUI.Colours[sudokuGUI.NAKED_PAIRS_COLOUR]) ReadWriteColour(path.."/Colours/NakedTriplets", sudokuGUI.Colours[sudokuGUI.NAKED_TRIPLETS_COLOUR]) ReadWriteColour(path.."/Colours/NakedQuads", sudokuGUI.Colours[sudokuGUI.NAKED_QUADS_COLOUR]) ReadWriteColour(path.."/Colours/HiddenPairs", sudokuGUI.Colours[sudokuGUI.HIDDEN_PAIRS_COLOUR]) ReadWriteColour(path.."/Colours/HiddenTriplets", sudokuGUI.Colours[sudokuGUI.HIDDEN_TRIPLETS_COLOUR]) ReadWriteColour(path.."/Colours/HiddenQuads", sudokuGUI.Colours[sudokuGUI.HIDDEN_QUADS_COLOUR]) ReadWriteFont(path.."/Fonts/Value", sudokuGUI.valueFont.wxfont) ReadWriteFont(path.."/Fonts/ValuePossible", sudokuGUI.possibleFont.wxfont) local function ReadWritePref(key, pref) if read then if config:HasEntry(key) then local _, v = config:Read(key, 0) sudokuGUI.CheckMenuItem(pref, inttobool(v)) end else config:Write(key, booltoint(sudokuGUI.IsCheckedMenuItem(pref))) end end ReadWritePref(path.."/Preferences/SHOW_ERRORS", sudokuGUI.ID_SHOW_ERRORS) ReadWritePref(path.."/Preferences/SHOW_MISTAKES", sudokuGUI.ID_SHOW_MISTAKES) ReadWritePref(path.."/Preferences/SHOW_TOOLBAR", sudokuGUI.ID_SHOW_TOOLBAR) ReadWritePref(path.."/Preferences/SHOW_TOOLBAR_LABELS", sudokuGUI.ID_SHOW_TOOLBAR_LABELS) ReadWritePref(path.."/Preferences/SHOW_STATUSBAR", sudokuGUI.ID_SHOW_STATUSBAR) ReadWritePref(path.."/Preferences/SHOW_POSSIBLE", sudokuGUI.ID_SHOW_POSSIBLE) ReadWritePref(path.."/Preferences/SHOW_USER_POSSIBLE", sudokuGUI.ID_SHOW_USER_POSSIBLE) ReadWritePref(path.."/Preferences/SHOW_POSSIBLE_LINE", sudokuGUI.ID_SHOW_POSSIBLE_LINE) ReadWritePref(path.."/Preferences/SHOW_NAKED", sudokuGUI.ID_SHOW_NAKED) ReadWritePref(path.."/Preferences/SHOW_HIDDEN", sudokuGUI.ID_SHOW_HIDDEN) ReadWritePref(path.."/Preferences/SHOW_NAKEDPAIRS", sudokuGUI.ID_SHOW_NAKEDPAIRS) ReadWritePref(path.."/Preferences/SHOW_HIDDENPAIRS", sudokuGUI.ID_SHOW_HIDDENPAIRS) ReadWritePref(path.."/Preferences/SHOW_NAKEDTRIPLETS", sudokuGUI.ID_SHOW_NAKEDTRIPLETS) ReadWritePref(path.."/Preferences/SHOW_HIDDENTRIPLETS", sudokuGUI.ID_SHOW_HIDDENTRIPLETS) ReadWritePref(path.."/Preferences/SHOW_NAKEDQUADS", sudokuGUI.ID_SHOW_NAKEDQUADS) ReadWritePref(path.."/Preferences/SHOW_HIDDENQUADS", sudokuGUI.ID_SHOW_HIDDENQUADS) ReadWritePref(path.."/Preferences/ELIMINATE_NAKED", sudokuGUI.ID_ELIMINATE_NAKED) ReadWritePref(path.."/Preferences/ELIMINATE_HIDDEN", sudokuGUI.ID_ELIMINATE_HIDDEN) ReadWritePref(path.."/Preferences/ELIMINATE_NAKEDPAIRS", sudokuGUI.ID_ELIMINATE_NAKEDPAIRS) ReadWritePref(path.."/Preferences/ELIMINATE_HIDDENPAIRS", sudokuGUI.ID_ELIMINATE_HIDDENPAIRS) ReadWritePref(path.."/Preferences/ELIMINATE_NAKEDTRIPLETS", sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS) ReadWritePref(path.."/Preferences/ELIMINATE_HIDDENTRIPLETS", sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS) ReadWritePref(path.."/Preferences/ELIMINATE_NAKEDQUADS", sudokuGUI.ID_ELIMINATE_NAKEDQUADS) ReadWritePref(path.."/Preferences/ELIMINATE_HIDDENQUADS", sudokuGUI.ID_ELIMINATE_HIDDENQUADS) end -- ---------------------------------------------------------------------------- function sudokuGUI.InitFontsAndColours() sudokuGUI.Colours = { [sudokuGUI.VALUE_COLOUR] = wx.wxColour(0, 0, 230), [sudokuGUI.INIT_VALUE_COLOUR] = wx.wxColour(0, 0, 0), [sudokuGUI.POSS_VALUE_COLOUR] = wx.wxColour(0, 0, 0), [sudokuGUI.INVALID_VALUE_COLOUR] = wx.wxColour(255, 0, 0), [sudokuGUI.BACKGROUND_COLOUR] = wx.wxColour(255, 255, 255), [sudokuGUI.ODD_BACKGROUND_COLOUR] = wx.wxColour(250, 250, 210), [sudokuGUI.FOCUS_CELL_COLOUR] = wx.wxColour(200, 220, 250), [sudokuGUI.NAKED_PAIRS_COLOUR] = wx.wxColour(255, 0, 0), [sudokuGUI.NAKED_TRIPLETS_COLOUR] = wx.wxColour(255, 180, 0), [sudokuGUI.NAKED_QUADS_COLOUR] = wx.wxColour(255, 255, 0), [sudokuGUI.HIDDEN_PAIRS_COLOUR] = wx.wxColour(0, 220, 0), [sudokuGUI.HIDDEN_TRIPLETS_COLOUR] = wx.wxColour(0, 240, 160), [sudokuGUI.HIDDEN_QUADS_COLOUR] = wx.wxColour(0, 220, 220) } sudokuGUI.Colours_ = {} for n = 1, sudokuGUI.COLOUR_MAX do sudokuGUI.Colours_[n] = wx.wxColour(sudokuGUI.Colours[n]) end -- just use defaults since some XP systems may not even have wxMODERN sudokuGUI.possibleFont_wxfont_ = wx.wxFont(wx.wxNORMAL_FONT) sudokuGUI.valueFont_wxfont_ = wx.wxFont(wx.wxNORMAL_FONT) sudokuGUI.valueFont_wxfont_:SetWeight(wx.wxFONTWEIGHT_BOLD) if not sudokuGUI.valueFont_wxfont_:Ok() then sudokuGUI.valueFont_wxfont_:Destroy() sudokuGUI.valueFont_wxfont_ = wx.wxFont(wx.wxNORMAL_FONT) end sudokuGUI.possibleFont.wxfont = wx.wxFont(sudokuGUI.possibleFont_wxfont_) sudokuGUI.valueFont.wxfont = wx.wxFont(sudokuGUI.valueFont_wxfont_) end -- ---------------------------------------------------------------------------- -- Create a table of the menu IDs to use as a "case" type statement sudokuGUI.MenuId = {} -- ---------------------------------------------------------------------------- sudokuGUI.MenuId[sudokuGUI.ID_NEW] = function() sudokuGUI.NewPuzzle(true) end sudokuGUI.MenuId[sudokuGUI.ID_CREATE] = function(event) sudokuGUI.CreatePuzzle(event:IsChecked()) end sudokuGUI.MenuId[sudokuGUI.ID_GENERATE] = function() sudokuGUI.GeneratePuzzle() end sudokuGUI.MenuId[sudokuGUI.ID_OPEN] = function() sudokuGUI.OpenPuzzle() end sudokuGUI.MenuId[sudokuGUI.ID_SAVEAS] = function() sudokuGUI.SaveAsPuzzle() end sudokuGUI.MenuId[sudokuGUI.ID_PAGESETUP] = function() sudokuGUI.PageSetup() end sudokuGUI.MenuId[sudokuGUI.ID_PRINTSETUP] = function() sudokuGUI.PrintSetup() end sudokuGUI.MenuId[sudokuGUI.ID_PRINTPREVIEW] = function() sudokuGUI.PrintPreview() end sudokuGUI.MenuId[sudokuGUI.ID_PRINT] = function() sudokuGUI.Print() end sudokuGUI.MenuId[sudokuGUI.ID_EXIT] = function() sudokuGUI.frame:Close() end -- ---------------------------------------------------------------------------- sudokuGUI.MenuId[sudokuGUI.ID_COPY_PUZZLE] = function (event) local str = sudoku.ToString(sudokuGUI.GetCurrentTable()) if wx.wxClipboard.Get():Open() then wx.wxClipboard.Get():SetData(wx.wxTextDataObject(str)) wx.wxClipboard.Get():Close() end end sudokuGUI.MenuId[sudokuGUI.ID_RESET] = function() sudokuGUI.ResetPuzzle() end sudokuGUI.MenuId[sudokuGUI.ID_UNDO] = function() sudokuGUI.Undo() end sudokuGUI.MenuId[sudokuGUI.ID_REDO] = function() sudokuGUI.Redo() end sudokuGUI.MenuId[sudokuGUI.ID_PREFERENCES] = function() sudokuGUI.PreferencesDialog() end sudokuGUI.MenuId[sudokuGUI.ID_SAVE_PREFERENCES] = function() sudokuGUI.ConfigSave(true) end -- ---------------------------------------------------------------------------- -- Makes sure that menu and tool items are in sync and updates the table function sudokuGUI.MenuCheckUpdate(event) sudokuGUI.CheckMenuItem(event:GetId(), event:IsChecked()) sudokuGUI.UpdateTable() end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_ERRORS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SHOW_MISTAKES] = function (event) -- need to solve it ourselves first if (not sudokuGUI.IsCheckedMenuItem(sudokuGUI.ID_CREATE)) and (event:IsChecked()) and (not sudokuGUI.sudokuSolnTable) then sudokuGUI.sudokuSolnTable = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable()) if not sudokuGUI.sudokuSolnTable then event:SetInt(0) -- uncheck for MenuCheckUpdate function sudokuGUI.frame:GetMenuBar():Check(sudokuGUI.ID_SHOW_MISTAKES, false) end end sudokuGUI.MenuCheckUpdate(event) end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_TOOLBAR] = function(event) sudokuGUI.frame:GetToolBar():Show(event:IsChecked()) -- hack to make the wxFrame layout the child panel local w, h = sudokuGUI.frame:GetSizeWH() sudokuGUI.frame:SetSize(wx.wxSize(w, h+1)) sudokuGUI.frame:SetSize(wx.wxSize(w, h)) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_TOOLBAR, event:IsChecked()) end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_TOOLBAR_LABELS] = function(event) local style = wx.wxNO_BORDER if event:IsChecked() then style = style + wx.wxTB_TEXT end sudokuGUI.frame:GetToolBar():SetWindowStyle(style) sudokuGUI.frame:GetToolBar():Realize() -- hack to make the wxFrame layout the child panel local w, h = sudokuGUI.frame:GetSizeWH() sudokuGUI.frame:SetSize(wx.wxSize(w, h+1)) sudokuGUI.frame:SetSize(wx.wxSize(w, h)) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_TOOLBAR_LABELS, event:IsChecked()) end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_STATUSBAR] = function(event) sudokuGUI.frame:GetStatusBar():Show(event:IsChecked()) -- hack to make the wxFrame layout the child panel local w, h = sudokuGUI.frame:GetSizeWH() sudokuGUI.frame:SetSize(wx.wxSize(w, h+1)) sudokuGUI.frame:SetSize(wx.wxSize(w, h)) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_STATUSBAR, event:IsChecked()) end -- ---------------------------------------------------------------------------- sudokuGUI.MenuId[sudokuGUI.ID_SHOW_POSSIBLE] = function (event) if event:IsChecked() then -- make this act like a radio item that can be unchecked sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_USER_POSSIBLE, false) end sudokuGUI.MenuCheckUpdate(event) end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_USER_POSSIBLE] = function (event) if event:IsChecked() then -- make this act like a radio item that can be unchecked sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_POSSIBLE, false) end sudokuGUI.MenuCheckUpdate(event) end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_POSSIBLE_LINE] = function (event) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_POSSIBLE_LINE, event:IsChecked()) local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH() sudokuGUI.GetCellBestSize(width-1, height-1) sudokuGUI.Refresh() end sudokuGUI.MenuId[sudokuGUI.ID_USER_POSSIBLE_CLEAR] = function (event) local ret = wx.wxMessageBox("Clear all of your pencil marks?", "wxLuaSudoku - clear pencil marks?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxOK then sudokuGUI.pencilMarks = {} for cell = 1, 81 do sudokuGUI.pencilMarks[cell] = {} end sudokuGUI.UpdateTable() end end sudokuGUI.MenuId[sudokuGUI.ID_USER_POSSIBLE_SETALL] = function (event) local ret = wx.wxMessageBox("Set all values as possible in the pencil marks?", "wxLuaSudoku - set all pencil marks?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxOK then sudokuGUI.pencilMarks = {} for cell = 1, 81 do sudokuGUI.pencilMarks[cell] = {} for v = 1, 9 do sudokuGUI.pencilMarks[cell][v] = v end end sudokuGUI.UpdateTable() end end sudokuGUI.MenuId[sudokuGUI.ID_USER_POSSIBLE_INIT] = function (event) local ret = wx.wxMessageBox("Initialize the pencil marks to the calculated possible values?", "wxLuaSudoku - initialize pencil marks?", wx.wxOK + wx.wxCANCEL + wx.wxICON_INFORMATION, sudokuGUI.frame ) if ret == wx.wxOK then local s = sudokuGUI.GetCurrentTable() for cell = 1, 81 do sudokuGUI.pencilMarks[cell] = {} for v = 1, 9 do sudokuGUI.pencilMarks[cell][v] = s.possible[cell][v] end end sudokuGUI.UpdateTable() end end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKED] = function (event) local checked = event:IsChecked() sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_NAKEDPAIRS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_NAKEDTRIPLETS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_NAKEDQUADS, checked) sudokuGUI.UpdateTable() end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDEN] = function (event) local checked = event:IsChecked() sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_HIDDENPAIRS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_HIDDENTRIPLETS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_SHOW_HIDDENQUADS, checked) sudokuGUI.UpdateTable() end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKEDPAIRS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDENPAIRS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKEDTRIPLETS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDENTRIPLETS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SHOW_NAKEDQUADS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SHOW_HIDDENQUADS] = sudokuGUI.MenuCheckUpdate -- ---------------------------------------------------------------------------- sudokuGUI.MenuId[sudokuGUI.ID_VERIFY_PUZZLE] = function (event) local s = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable()) if s then sudokuGUI.sudokuSolnTable = s end end sudokuGUI.MenuId[sudokuGUI.ID_SHOW_SOLUTION] = function (event) if not sudokuGUI.sudokuSolnTable then local s = sudokuGUI.VerifyUniquePuzzle(sudokuGUI.GetInitTable()) if s then sudokuGUI.sudokuSolnTable = s end end if sudokuGUI.sudokuSolnTable then sudokuGUI.AddTable(sudokuGUI.sudokuSolnTable) end end sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKED] = function (event) local checked = event:IsChecked() sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_NAKEDPAIRS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_NAKEDQUADS, checked) sudokuGUI.UpdateTable() end sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDEN] = function (event) local checked = event:IsChecked() sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_HIDDENPAIRS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS, checked) sudokuGUI.CheckMenuItem(sudokuGUI.ID_ELIMINATE_HIDDENQUADS, checked) sudokuGUI.UpdateTable() end sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKEDPAIRS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDENPAIRS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_NAKEDQUADS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_ELIMINATE_HIDDENQUADS] = sudokuGUI.MenuCheckUpdate sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANSINGLES] = function (event) local s = TableCopy(sudokuGUI.GetCurrentTable()) local changed_cells = sudoku.SolveScanSingles(s) if changed_cells then sudokuGUI.AddTable(s) end end sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANROWS] = function (event) local s = TableCopy(sudokuGUI.GetCurrentTable()) local changed_cells = sudoku.SolveScanRows(s) if changed_cells then sudokuGUI.AddTable(s) end end sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANCOLS] = function (event) local s = TableCopy(sudokuGUI.GetCurrentTable()) local changed_cells = sudoku.SolveScanCols(s) if changed_cells then sudokuGUI.AddTable(s) end end sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANBLOCKS] = function (event) local s = TableCopy(sudokuGUI.GetCurrentTable()) local changed_cells = sudoku.SolveScanBlocks(s) if changed_cells then sudokuGUI.AddTable(s) end end sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_SCANNING] = function (event) sudokuGUI.SolveScanning() end sudokuGUI.MenuId[sudokuGUI.ID_SOLVE_BRUTEFORCE] = function (event) sudokuGUI.SolveBruteForce() end -- ---------------------------------------------------------------------------- sudokuGUI.MenuId[sudokuGUI.ID_ABOUT] = function (event) wx.wxMessageBox("Welcome to wxLuaSudoku!\nWritten by John Labenski\nCopyright 2006.\n".. wxlua.wxLUA_VERSION_STRING.." built with "..wx.wxVERSION_STRING, "About wxLuaSudoku", wx.wxOK + wx.wxICON_INFORMATION, sudokuGUI.frame ) end sudokuGUI.MenuId[sudokuGUI.ID_HELP] = function (event) local helpFrame = wx.wxFrame(sudokuGUI.frame, wx.wxID_ANY, "Help on wxLuaSudoku", wx.wxDefaultPosition, wx.wxSize(600,400)) local htmlWin = wx.wxHtmlWindow(helpFrame) if (htmlWin:SetPage(sudokuGUIhelp)) then helpFrame:Centre() helpFrame:Show(true) else helpFrame:Destroy() end end -- ---------------------------------------------------------------------------- function sudokuGUI.OnMenuEvent(event) local id = event:GetId() if sudokuGUI.MenuId[id] then sudokuGUI.MenuId[id](event) return end end -- ---------------------------------------------------------------------------- -- Unify all checking and unchecking of the menu items and -- make sure menu/toolbar are in sync function sudokuGUI.CheckMenuItem(id, check) sudokuGUI.frame:GetMenuBar():Check(id, check) sudokuGUI.frame:GetToolBar():ToggleTool(id, check) -- doesn't care if id doesn't exist sudokuGUI.menuCheckIDs[id] = check end function sudokuGUI.IsCheckedMenuItem(id) if sudokuGUI.menuCheckIDs[id] == nil then sudokuGUI.menuCheckIDs[id] = sudokuGUI.frame:GetMenuBar():IsChecked(id) end return sudokuGUI.menuCheckIDs[id] end -- ---------------------------------------------------------------------------- function main() sudokuGUI.block_refresh = true -- initialize the fonts and colours to use (must always exist) sudokuGUI.InitFontsAndColours() -- initialize the printing defaults sudokuGUI.printData:SetPaperId(wx.wxPAPER_LETTER); sudokuGUI.pageSetupData:SetMarginTopLeft(wx.wxPoint(25, 25)); sudokuGUI.pageSetupData:SetMarginBottomRight(wx.wxPoint(25, 25)); -- Create the main frame for the program sudokuGUI.frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "wxLuaSudoku", wx.wxDefaultPosition, wx.wxSize(300,320)) sudokuGUI.frame:SetSizeHints(300, 300); local bitmap = wx.wxBitmap(sudokuGUIxpmdata) local icon = wx.wxIcon() icon:CopyFromBitmap(bitmap) sudokuGUI.frame:SetIcon(icon) local function MItem(menu, id, text, help, bmp) local m = wx.wxMenuItem(menu, id, text, help) m:SetBitmap(bmp) bmp:delete() return m end local fileMenu = wx.wxMenu("", 0) fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_NEW, "&New...\tCtrl-N", "Clear the current puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_NEW, wx.wxART_TOOLBAR))) fileMenu:AppendCheckItem(sudokuGUI.ID_CREATE, "&Create...\tCtrl-T", "Enter the initial values for the puzzle") fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_GENERATE, "&Generate...\tCtrl-G", "Generate a new puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_EXECUTABLE_FILE, wx.wxART_TOOLBAR))) fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_OPEN, "&Open...\tCtrl-O", "Open a puzzle file", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_OPEN, wx.wxART_TOOLBAR))) fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_SAVEAS, "&Save as...\tCtrl-S", "Save the current puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_SAVE_AS, wx.wxART_TOOLBAR))) fileMenu:AppendSeparator() fileMenu:Append(sudokuGUI.ID_PAGESETUP, "Page S&etup...", "Setup the printout page") --fileMenu:Append(sudokuGUI.ID_PRINTSETUP, "Print Se&tup...", "Setup the printer") fileMenu:Append(sudokuGUI.ID_PRINTPREVIEW, "Print Pre&view...", "Preview the printout") fileMenu:Append(MItem(fileMenu, sudokuGUI.ID_PRINT, "&Print...", "Print the puzzle", wx.wxArtProvider.GetBitmap(wx.wxART_PRINT, wx.wxART_TOOLBAR))) fileMenu:AppendSeparator() fileMenu:Append(sudokuGUI.ID_EXIT, "E&xit\tCtrl-X", "Quit the program") local editMenu = wx.wxMenu("", 0) editMenu:Append(sudokuGUI.ID_COPY_PUZZLE, "Copy puzzle", "Copy the puzzle to the clipboard") editMenu:AppendSeparator() editMenu:Append(sudokuGUI.ID_RESET, "Re&set...\tCtrl-R", "Reset the puzzle to the initial state") editMenu:AppendSeparator() editMenu:Append(MItem(editMenu, sudokuGUI.ID_UNDO, "&Undo\tCtrl-Z", "Undo the last entry", wx.wxArtProvider.GetBitmap(wx.wxART_UNDO, wx.wxART_TOOLBAR))) editMenu:Append(MItem(editMenu, sudokuGUI.ID_REDO, "&Redo\tCtrl-Y", "Redo the last entry", wx.wxArtProvider.GetBitmap(wx.wxART_REDO, wx.wxART_TOOLBAR))) editMenu:AppendSeparator() editMenu:Append(sudokuGUI.ID_PREFERENCES, "P&references...", "Show the preferences dialog") editMenu:AppendSeparator() editMenu:Append(sudokuGUI.ID_SAVE_PREFERENCES, "Sa&ve preferences...", "Save the preferences") local viewMenu = wx.wxMenu("", 0) viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_ERRORS, "Mark &errors\tCtrl-E", "Mark duplicate values in puzzle") viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_MISTAKES, "Mark &mistakes\tCtrl-M", "Mark wrong values in puzzle") viewMenu:AppendSeparator() viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_TOOLBAR, "Show toolbar", "Show the toolbar") viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_TOOLBAR_LABELS, "Show toolbar labels", "Show labels on the toolbar") viewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_STATUSBAR, "Show statusbar", "Show the statusbar") viewMenu:Check(sudokuGUI.ID_SHOW_TOOLBAR, true) viewMenu:Check(sudokuGUI.ID_SHOW_TOOLBAR_LABELS, true) viewMenu:Check(sudokuGUI.ID_SHOW_STATUSBAR, true) local possibleMenu = wx.wxMenu("", 0) possibleMenu:AppendCheckItem( sudokuGUI.ID_SHOW_POSSIBLE, "Show calculated &possible\tCtrl-P", "Show calculated possible values for the cells") possibleMenu:AppendCheckItem( sudokuGUI.ID_SHOW_USER_POSSIBLE, "Show/Edit pencil marks\tCtrl-l", "Show and edit user set possible values for the cells") possibleMenu:AppendSeparator() possibleMenu:AppendCheckItem( sudokuGUI.ID_SHOW_POSSIBLE_LINE, "Show possible in a &line", "Show possible values for the cells in a line") possibleMenu:AppendSeparator() local userPossMenu = wx.wxMenu("", 0) userPossMenu:Append( sudokuGUI.ID_USER_POSSIBLE_CLEAR, "Clear all...", "Clear all pencil marks") userPossMenu:Append( sudokuGUI.ID_USER_POSSIBLE_SETALL, "Set all...", "Set all pencil marks") userPossMenu:Append( sudokuGUI.ID_USER_POSSIBLE_INIT, "Calculate...", "Initialize pencil marks to calculated possible") possibleMenu:Append(sudokuGUI.ID_USER_POSSIBLE_MENU, "Pencil marks", userPossMenu, "Setup user possible values") possibleMenu:AppendSeparator() local possViewMenu = wx.wxMenu("", 0) possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKED, "Mark &naked groups", "Mark all naked groups in possible values") possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDEN, "Mark &hidden groups", "Mark all hidden groups in possible values") possViewMenu:AppendSeparator() possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKEDPAIRS, "Mark naked pairs", "Mark naked pairs in possible values") possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDENPAIRS, "Mark hidden pairs", "Mark hidden pairs in possible values") possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKEDTRIPLETS, "Mark naked triplets", "Mark naked triplets in possible values") possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDENTRIPLETS, "Mark hidden triplets", "Mark hidden triplets in possible values") possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_NAKEDQUADS, "Mark naked quads", "Mark naked quads in possible values") possViewMenu:AppendCheckItem( sudokuGUI.ID_SHOW_HIDDENQUADS, "Mark hidden quads", "Mark hidden quads in possible values") possibleMenu:Append(sudokuGUI.ID_SHOW_MENU, "Mark &groups", possViewMenu, "Mark naked/hidden groups") local solveMenu = wx.wxMenu("", 0) solveMenu:Append(sudokuGUI.ID_VERIFY_PUZZLE, "Verify unique solution...", "Verify that the puzzle has only one solution") solveMenu:AppendSeparator() solveMenu:Append(sudokuGUI.ID_SHOW_SOLUTION, "Show solution", "Show the solution to the puzzle") solveMenu:AppendSeparator() local elimSolveMenu = wx.wxMenu("", 0) elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKED, "Eliminate &naked groups", "Eliminate all naked groups from possible values") elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDEN, "Eliminate &hidden groups", "Eliminate all hidden groups from possible values") elimSolveMenu:AppendSeparator() elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKEDPAIRS, "Eliminate naked pairs", "Eliminate naked pairs from possible values") elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDENPAIRS, "Eliminate hidden pairs", "Eliminate hidden pairs from possible values") elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKEDTRIPLETS, "Eliminate naked triplets", "Eliminate naked triplets from possible values") elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDENTRIPLETS, "Eliminate hidden triplets", "Eliminate hidden triplets from possible values") elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_NAKEDQUADS, "Eliminate naked quads", "Eliminate naked quads from possible values") elimSolveMenu:AppendCheckItem(sudokuGUI.ID_ELIMINATE_HIDDENQUADS, "Eliminate hidden quads", "Eliminate hidden quads from possible values") solveMenu:Append(sudokuGUI.ID_ELIMINATE_MENU, "&Eliminate groups", elimSolveMenu, "Remove possible values using naked and hidden groups") solveMenu:AppendSeparator() solveMenu:Append(sudokuGUI.ID_SOLVE_SCANSINGLES, "Solve (scan singles)\tCtrl-1", "Solve all cells with only one possibility") solveMenu:Append(sudokuGUI.ID_SOLVE_SCANROWS, "Solve (scan rows)\tCtrl-2", "Solve cells in rows with only one possible value") solveMenu:Append(sudokuGUI.ID_SOLVE_SCANCOLS, "Solve (scan cols)\tCtrl-3", "Solve cells in cols with only one possible value") solveMenu:Append(sudokuGUI.ID_SOLVE_SCANBLOCKS, "Solve (scan blocks)\tCtrl-4", "Solve cells in blocks with only one possible value") solveMenu:AppendSeparator() solveMenu:Append(sudokuGUI.ID_SOLVE_SCANNING, "Solve (&scanning)\tCtrl-L", "Solve the puzzle by only scanning") solveMenu:Append(sudokuGUI.ID_SOLVE_BRUTEFORCE, "Solve (&brute force)\tCtrl-B", "Solve the puzzle by guessing values") local helpMenu = wx.wxMenu("", 0) helpMenu:Append(sudokuGUI.ID_ABOUT, "&About...", "About the wxLuaSudoku Application") helpMenu:Append(MItem(helpMenu, sudokuGUI.ID_HELP, "&Help...", "Help using the wxLuaSudoku application", wx.wxArtProvider.GetBitmap(wx.wxART_HELP, wx.wxART_TOOLBAR))) local menuBar = wx.wxMenuBar() menuBar:Append(fileMenu, "&File") menuBar:Append(editMenu, "&Edit") menuBar:Append(viewMenu, "&View") menuBar:Append(possibleMenu, "&Possible") menuBar:Append(solveMenu, "&Solve") menuBar:Append(helpMenu, "&Help") sudokuGUI.frame:SetMenuBar(menuBar) local toolBar = sudokuGUI.frame:CreateToolBar(wx.wxNO_BORDER + wx.wxTB_TEXT) local tbSize = toolBar:GetToolBitmapSize() -- required to force help icon to right size in MSW toolBar:AddTool(sudokuGUI.ID_NEW, "New", wx.wxArtProvider.GetBitmap(wx.wxART_NEW, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "New...", "Clear the current puzzle") toolBar:AddCheckTool(sudokuGUI.ID_CREATE, "Create", wx.wxArtProvider.GetBitmap(wx.wxART_ADD_BOOKMARK, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, "Create...", "Enter initial values for the puzzle") toolBar:AddTool(sudokuGUI.ID_GENERATE, "Generate", wx.wxArtProvider.GetBitmap(wx.wxART_EXECUTABLE_FILE, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Generate...", "Generate a new puzzle") toolBar:AddTool(sudokuGUI.ID_OPEN, "Open", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_OPEN, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Open...", "Open a puzzle file") toolBar:AddTool(sudokuGUI.ID_SAVEAS, "Save", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_SAVE_AS, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Save as...", "Save the current puzzle") toolBar:AddTool(sudokuGUI.ID_PRINT, "Print", wx.wxArtProvider.GetBitmap(wx.wxART_PRINT, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Print...", "Print the puzzle") toolBar:AddSeparator() toolBar:AddTool(sudokuGUI.ID_UNDO, "Undo", wx.wxArtProvider.GetBitmap(wx.wxART_UNDO, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Undo", "Undo the last entry") toolBar:AddTool(sudokuGUI.ID_REDO, "Redo", wx.wxArtProvider.GetBitmap(wx.wxART_REDO, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Redo", "Redo the last entry") toolBar:AddSeparator() toolBar:AddTool(sudokuGUI.ID_HELP, "Help", wx.wxArtProvider.GetBitmap(wx.wxART_HELP, wx.wxART_TOOLBAR, tbSize), wx.wxNullBitmap, wx.wxITEM_NORMAL, "Help...", "Help on wxLuaSudoku") toolBar:Realize() sudokuGUI.frame:CreateStatusBar(2) local stat_width = sudokuGUI.frame:GetStatusBar():GetTextExtent("Step : 00000") sudokuGUI.frame:SetStatusWidths({-1, stat_width}) sudokuGUI.frame:SetStatusText("Welcome to wxLuaSudoku.", 0) -- ------------------------------------------------------------------------ -- Use single centralized menu/toolbar event handler sudokuGUI.frame:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED, sudokuGUI.OnMenuEvent) -- ------------------------------------------------------------------------ local values = { 5,0,0, 8,0,3, 0,6,0, 1,0,6, 0,9,2, 0,8,5, 0,0,8, 5,0,7, 0,4,0, 0,0,1, 0,3,4, 0,7,0, 0,9,0, 0,0,8, 1,3,4, 3,0,0, 0,2,0, 5,9,0, 0,0,5, 1,0,0, 0,0,3, 0,0,0, 0,0,9, 0,0,0, 0,0,7, 3,0,0, 4,0,9 } local solution = { 5,4,9, 8,1,3, 2,6,7, 1,7,6, 4,9,2, 3,8,5, 2,3,8, 5,6,7, 9,4,1, 8,5,1, 9,3,4, 6,7,2, 7,9,2, 6,5,8, 1,3,4, 3,6,4, 7,2,1, 5,9,8, 9,8,5, 1,4,6, 7,2,3, 4,1,3, 2,7,9, 8,5,6, 6,2,7, 3,8,5, 4,1,9 } local s = sudoku.CreateTable() sudoku.SetValues(s, values) sudokuGUI.sudokuTables_pos = 1 sudokuGUI.sudokuTables[1] = s sudokuGUI.sudokuSolnTable = sudoku.CreateTable() sudoku.SetValues(sudokuGUI.sudokuSolnTable, solution) sudokuGUI.panel = wx.wxPanel(sudokuGUI.frame, wx.wxID_ANY) --sudokuGUI.panel:SetBackgroundColour(wx.wxColour(0,0,0)) local gridsizer = wx.wxGridSizer(9, 9, 2, 2) for i = 1, 81 do local win = sudokuGUI.CreateCellWindow( sudokuGUI.panel, i, size ) gridsizer:Add(win, 1, wx.wxALL+wx.wxGROW+ wx.wxALIGN_CENTER, 0) sudokuGUI.cellWindows[i] = win end local topsizer = wx.wxBoxSizer(wx.wxVERTICAL) topsizer:Add(gridsizer, 1, wx.wxALL+wx.wxGROW+wx.wxALIGN_CENTER, 0) sudokuGUI.panel:SetSizer( topsizer ) --topsizer:Fit(sudokuGUI.frame) --topsizer:SetSizeHints( sudokuGUI.frame ) -- ------------------------------------------------------------------------ -- After being created - connect the size event to help MSW repaint the -- child windows sudokuGUI.cellWindows[1]:Connect(wx.wxEVT_SIZE, function (event) local width, height = sudokuGUI.cellWindows[1]:GetClientSizeWH() sudokuGUI.GetCellBestSize(width, height) sudokuGUI.Refresh() event:Skip(true) end ) -- save the config when closing the frame sudokuGUI.frame:Connect(wx.wxEVT_CLOSE_WINDOW, function (event) event:Skip(true) -- allow it to really exit sudokuGUI.ConfigSave(false) end ) local cell_width, cell_height = sudokuGUI.cellWindows[1]:GetClientSizeWH() sudokuGUI.GetCellBestSize(cell_width, cell_height) --sudokuGUI.UpdateTable() sudokuGUI.frame:SetClientSize(300,300) sudokuGUI.block_refresh = false sudokuGUI.ConfigLoad() sudokuGUI.frame:Show(true) collectgarbage("collect") -- cleanup any locals end main() if false then function ProfileBegin() Profile_Counters = {} Profile_Names = {} local function hook () local f = debug.getinfo(2, "f").func if Profile_Counters[f] == nil then -- first time `f' is called? Profile_Counters[f] = 1 Profile_Names[f] = debug.getinfo(2, "Sn") --TableDump(Profile_Names[f]) else -- only increment the counter Profile_Counters[f] = Profile_Counters[f] + 1 end end debug.sethook(hook, "c") -- turn on the hook end function ProfileEnd() debug.sethook() -- turn off the hook function getname (func) local n = Profile_Names[func] if n.what == "C" then return n.name end local loc = string.format("[%s]:%s", n.short_src, n.linedefined) if n.namewhat ~= "" then return string.format("%s (%s)", loc, n.name) else return string.format("%s", loc) end end for func, count in pairs(Profile_Counters) do print(getname(func), count) end end s = sudoku.CreateTable() s.flags[sudoku.ELIMINATE_HIDDEN_PAIRS] = true s.flags[sudoku.ELIMINATE_HIDDEN_TRIPLETS] = true s.flags[sudoku.ELIMINATE_HIDDEN_QUADS] = true t = os.time() --ProfileBegin() for n = 1, 10 do a, b = sudoku.FindAllNakedHiddenGroups(s, true) --a, b, c = sudoku.FindPossibleCountRowColBlock(s) --a, b, c = sudoku.FindAllPossibleGroups(s) end --ProfileEnd() print(os.time()-t) end --[[ for i = 1, 1 do local s = sudoku.GeneratePuzzle() s = sudoku.GeneratePuzzleDifficulty(s, 35, true) sudoku.UpdateTable(s) local n, h = sudoku.FindAllNakedHiddenGroups(s, true) --TableDump(n) --TableDump(h) local c = 0 cnp = TableCount(n.pairs.cells) cnt = TableCount(n.triplets.cells) cnq = TableCount(n.quads.cells) chp = TableCount(h.pairs.cells) cht = TableCount(h.triplets.cells) chq = TableCount(h.quads.cells) print(i, string.format("n %03d %03d %03d h %03d %03d %03d", cnp, cnt, cnq, chp, cht, chq)) a[string.format("n %03d %03d %03d h %03d %03d %03d", cnp, cnt, cnq, chp, cht, chq)] = TableCopy(s.values) if (cnp > 0) and (cnt > 0) and (cnq > 0) and (chp > 0) and (cht > 0) and (chq > 0) then break end end ]] -- Call wx.wxGetApp():MainLoop() last to start the wxWidgets event loop, -- otherwise the wxLua program will exit immediately. -- Does nothing if running from wxLua, wxLuaFreeze, or wxLuaEdit since the -- MainLoop is already running or will be started by the C++ program. wx.wxGetApp():MainLoop()