Add ipuz support - crossword.koplugin - Unnamed repository; edit this file 'description' to name the repository. (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules (DIR) README (DIR) LICENSE --- (DIR) commit 905ccad98cb817500375572f5a1975b499a7e228 (DIR) parent de84eea66adc3722e8dfd10f6d4de9247c704f4f (HTM) Author: Scarlett McAllister <no+reply@roygbyte.com> Date: Sun, 31 Dec 2023 12:38:13 -0400 Add ipuz support Coding is wet and wild! Needs to be tidied up. Diffstat: M gridsquare.lua | 4 ++-- M library.lua | 7 ++++--- M main.lua | 2 +- M puzzle.lua | 169 +++++++++++++++++++++++++++---- A sample.ipuz | 136 +++++++++++++++++++++++++++++++ M spec/unit/crossword_spec.lua | 9 +++++++-- 6 files changed, 301 insertions(+), 26 deletions(-) --- (DIR) diff --git a/gridsquare.lua b/gridsquare.lua @@ -35,9 +35,9 @@ function GridSquare:init() -- and which square is selected (state 2) local state_bg_color if self.state == "1" then - state_bg_color = self.dark_mode and Blitbuffer.COLOR_GRAY_3 or Blitbuffer.COLOR_LIGHT_GRAY + state_bg_color = self.dark_mode and Blitbuffer.COLOR_GRAY_5 or Blitbuffer.COLOR_LIGHT_GRAY elseif self.state == "2" then - state_bg_color = self.dark_mode and Blitbuffer.COLOR_GRAY_5 or Blitbuffer.COLOR_DARK_GRAY + state_bg_color = self.dark_mode and Blitbuffer.COLOR_GRAY_3 or Blitbuffer.COLOR_DARK_GRAY end local text_fg_color = self.dark_mode and Blitbuffer.COLOR_WHITE or Blitbuffer.COLOR_BLACK (DIR) diff --git a/library.lua b/library.lua @@ -40,7 +40,7 @@ end --[[-- Given a directory, return a table of files located within this directory. Filter -the files by type and extension, showing only JSON files. These are assumed to be +the files by type and extension, showing only JSON and IPUZ files. These are assumed to be the crossword puzzles. ]] function Library:getFilesInDirectory(path_to_dir) @@ -57,11 +57,12 @@ function Library:getFilesInDirectory(path_to_dir) or attributes.mode == "file" and f ~= "." and f ~= ".." - and util.stringEndsWith(f, ".json") + and (util.stringEndsWith(f, ".json") or + util.stringEndsWith(f, ".ipuz")) then local title = (attributes.mode == "directory") and f or -- Use the file name as the title - Puzzle:initializePuzzle(("%s/%s"):format(path_to_dir, f)).title -- Use the puzzle's name as the title + Puzzle:initializePuzzleFromFile(("%s/%s"):format(path_to_dir, f)).title -- Use the puzzle's name as the title local item = { title = title, -- The item's name to show the user. filename = f, -- The item's name in the filesystem. (DIR) diff --git a/main.lua b/main.lua @@ -130,7 +130,7 @@ function Crossword:showLibraryView() local library = Library:new{ puzzle_dir = self.puzzle_dir, onSelectPuzzle = function(item) - local puzzle = Puzzle:initializePuzzle(("%s/%s"):format(item.path_to_dir, item.filename)) + local puzzle = Puzzle:initializePuzzleFromFile(("%s/%s"):format(item.path_to_dir, item.filename)) -- Because the puzzle is added to history, it needs to also be saved. Otherwise, when -- it's loaded from history there will be no content to restore. puzzle:save() (DIR) diff --git a/puzzle.lua b/puzzle.lua @@ -1,11 +1,129 @@ local md5 = require("ffi/sha2").md5 local logger = require("logger") local json = require("json") +local util = require("util") local Guess = require("guess") local Solve = require("solve") local State = require("state") +local supported_formats = { + ipuz = { + fields = {"kind", "dimensions", "puzzle", "solution", "clues"}, + callback = (function(puzzle, json_object) + puzzle.title = json_object.title + puzzle.size = { + cols = json_object.dimensions.width, + rows = json_object.dimensions.height + } + puzzle.solves = {} + puzzle.guesses = {} + -- title is probably not enough for uniqueness... + -- maybe title and first + second clue/answer? + puzzle.id = md5(json_object.title) + across = {} + for i, row in pairs(json_object.solution) do + across_word = "" + for j, letter in pairs(row) do + if letter == "#" then + if across_word ~= "" then + table.insert(across, across_word) + end + across_word = "" + else + across_word = across_word .. letter + end + end + if across_word ~= "" then + table.insert(across, across_word) + end + end + local down = {} + for i = 1, #json_object.solution do + local down_word = "" + for j, row in pairs(json_object.solution) do + local letter = row[i] + if letter ~= "#" then + down_word = down_word .. letter + end + if letter == "#" or j == #json_object.solution then + if down_word ~= "" then + -- the puzzle position is found by finding the col+row combination + -- that points to the position of the first letter of the word. + -- this is typically found by subtracting the word length, which + -- accommodates all rows except the last. On the last row we need + -- to subtract 1, because we're still positioned ontop of the + -- row which is the word. + local row_offset = string.len(down_word) + if j == #json_object.solution then + row_offset = row_offset - 1 + end + table.insert( + down, + { + word = down_word, + pos = tonumber(json_object.puzzle[j - row_offset][i]) + } + ) + down_word = "" + end + end + end + end + for i, word in pairs(down) do + print(word.pos .. word.word) + end + table.sort(down, function(a, b) + return a.pos < b.pos + end) + for i, word in pairs(down) do + down[i] = word.word + end + -- Make the clues. + local function format_clues(unformatted_clues) + local clues = {} + for i, clue in pairs(unformatted_clues) do + table.insert(clues, clue[1] .. ". " .. clue[2]) + end + return clues + end + down_clues = format_clues(json_object.clues.Down) + across_clues = format_clues(json_object.clues.Across) + -- Make grid nums. Could be combined with across word logic, probs. + local gridnums = {} + for i, row in pairs(json_object.puzzle) do + for j, letter in pairs(row) do + if letter == "#" then + letter = "0" + end + table.insert(gridnums, letter) + end + end + puzzle:createSolves(across_clues, across, Solve.ACROSS, gridnums) + puzzle:createSolves(down_clues, down, Solve.DOWN, gridnums) + end), + }, + nyt = { + fields = {"answers", "clues", "grid", "size"}, + callback = (function(puzzle, json_object) + puzzle.size = json_object.size + -- Initialize the solves. + puzzle.solves = {} + -- Initialize the player's inputs. + puzzle.guesses = {} + -- Initialize the puzzle's title, etc. + puzzle.title = json_object.title + puzzle.editor = json_object.editor + puzzle.id = md5(json_object.title) + -- Create the down and across solves. + puzzle:createSolves(json_object.clues.down, json_object.answers.down, + Solve.DOWN, json_object.gridnums) + puzzle:createSolves(json_object.clues.across, json_object.answers.across, + Solve.ACROSS, json_object.gridnums) + end) + } +} + local Puzzle = State:new{ size = { cols = nil, @@ -22,7 +140,7 @@ function Puzzle:new(o) return o end -function Puzzle:initializePuzzle(path_to_file) +function Puzzle:initializePuzzleFromFile(path_to_file) local file, err = io.open(path_to_file, "rb") if not file then @@ -33,8 +151,8 @@ function Puzzle:initializePuzzle(path_to_file) file:close() local Puzzle = require("puzzle") - local puzzle = Puzzle:new{} - puzzle:init(json.decode(file_content)) + local puzzle = Puzzle:new{} + puzzle:initializePuzzleFromJson(json.decode(file_content)) puzzle:load() return puzzle @@ -48,26 +166,35 @@ function Puzzle:loadById(puzzle_id) return puzzle end -function Puzzle:init(json_object) +function Puzzle:initializePuzzleFromJson(json_object) -- Lazy error checking if not json_object then logger.dbg("Puzzle: json_object must be set for puzzle to be created.") return false end - -- Onto the initialization. - self.size = json_object.size - -- Initialize the solves. - self.solves = {} - -- Initialize the player's inputs. - self.guesses = {} - -- Initialize the puzzle's title, etc. - self.title = json_object.title - self.editor = json_object.editor - self.id = md5(json_object.title) - -- Create the down and across solves. - self:createSolves(json_object.clues.down, json_object.answers.down, Solve.DOWN, json_object.gridnums) - self:createSolves(json_object.clues.across, json_object.answers.across, Solve.ACROSS, json_object.gridnums) + + local function select_format_cb(supported_formats, json_object) + for i, format in pairs(supported_formats) do + is_format = true + for k, field in pairs(format.fields) do + if not json_object[field] then + is_format = false + break + end + end + if is_format then + return format.callback + end + end + end + + local format_cb = select_format_cb(supported_formats, json_object) + if not format_cb then + return _("Could not find supported format in JSON object") + end + self = format_cb(self, json_object) end + -- For lack of better naming, the "solves" are the combination of -- word, direction, and clue that make up a crossword puzzle. This method -- creates the solves from a four separate lists. That fours lists are @@ -77,7 +204,7 @@ end -- really, the object doesn't care where the data is coming from. It just wants -- the data. Nom, nom, nom! function Puzzle:createSolves(clues, answers, direction, grid_nums) - for i, clue in ipairs(clues) do + for i, clue in ipairs(clues) do local solve = Solve:new{ word = answers[i], direction = direction, @@ -311,6 +438,12 @@ function Puzzle:getSolveByPos(row, col, direction) -- if not solve and index then -- return nil -- else + print(solve.word) + print(solve.direction) + print(solve.start) + print(solve.clue_num) + print(solve.grid_num) + print(solve.grid_indices) return solve, index -- end end (DIR) diff --git a/sample.ipuz b/sample.ipuz @@ -0,0 +1,136 @@ +{ +"version":"http://ipuz.org/v2" +,"kind":["http://ipuz.org/crossword#1"] +,"title":"You Ain't Seen Nothing Yet!" +,"author":"By Tarun Krishnamurthy" +,"copyright":"© 2023 uclick LLC" +,"url":"https://picayune.uclick.com/comics/fcx/data/fcx231208-data.xml" +,"publisher":"Universal Crossword" +,"date":"12/08/2023" +,"dimensions":{"width":15,"height":15} +,"puzzle":[ + ["1","2","3","4","5","#","6","7","8","#","9","10","11","12","13"] + ,["14",0,0,0,0,"#","15",0,0,"#","16",0,0,0,0] + ,["17",0,0,0,0,"18",0,0,0,"19",0,0,0,0,0] + ,["20",0,0,0,"#","21",0,0,0,0,"#","#","22",0,0] + ,["#","#","#","23","24",0,0,"#","25",0,"26","27",0,"#","#"] + ,["#","28","29",0,0,0,0,"30",0,0,0,0,0,"31","32"] + ,["33",0,0,"#","34",0,0,0,"#","#","35",0,0,0,0] + ,["36",0,0,"37","#","38",0,0,"39","40","#","41",0,0,0] + ,["42",0,0,0,"43","#","#","44",0,0,"45","#","46",0,0] + ,["47",0,0,0,0,"48","49",0,0,0,0,"50",0,0,"#"] + ,["#","#","51",0,0,0,0,"#","52",0,0,0,"#","#","#"] + ,["53","54",0,"#","#","55",0,"56",0,0,"#","57","58","59","60"] + ,["61",0,0,"62","63",0,0,0,0,0,"64",0,0,0,0] + ,["65",0,0,0,0,"#","66",0,0,"#","67",0,0,0,0] + ,["68",0,0,0,0,"#","69",0,0,"#","70",0,0,0,0] +] +,"solution":[ + ["G","R","E","T","A","#","P","G","A","#","I","N","S","T","A"] + ,["R","E","R","U","N","#","I","N","S","#","P","U","P","I","L"] + ,["A","N","O","N","Y","M","O","U","S","D","O","N","O","R","S"] + ,["B","O","S","E","#","E","N","S","U","E","#","#","N","E","O"] + ,["#","#","#","I","C","E","E","#","C","A","N","E","S","#","#"] + ,["#","N","A","N","O","T","E","C","H","N","O","L","O","G","Y"] + ,["R","I","P","#","P","U","R","R","#","#","M","A","R","I","E"] + ,["A","X","E","S","#","P","S","A","T","S","#","L","I","R","A"] + ,["C","O","C","O","A","#","#","S","H","O","P","#","N","T","H"] + ,["E","N","O","U","G","H","I","S","E","N","O","U","G","H","#"] + ,["#","#","S","P","E","A","R","#","H","A","M","M","#","#","#"] + ,["E","S","T","#","#","L","O","F","A","T","#","P","E","R","U"] + ,["D","O","U","B","L","E","N","E","G","A","T","I","V","E","S"] + ,["G","U","M","B","O","#","E","A","U","#","E","R","A","S","E"] + ,["E","R","E","C","T","#","D","R","E","#","D","E","N","T","S"] +] +,"clues":{ + "Across":[ + ["1","Environmentalist Thunberg"] + ,["6","Org. for Tiger Woods"] + ,["9","Pic-sharing app"] + ,["14","Second airing, say"] + ,["15","___ and outs"] + ,["16","Part of the eye"] + ,["17","Undisclosed contributors at a blood bank"] + ,["20","Brand of headphones"] + ,["21","Follow as a result"] + ,["22","Prefix with \"natal\""] + ,["23","Frozen drink whose name sounds like its first two letters"] + ,["25","Candy ___ (Christmas decorations)"] + ,["28","Small field in Silicon Valley?"] + ,["33","Paper problem"] + ,["34","Contented cat's sound"] + ,["35","Curie who researched radioactivity"] + ,["36","There are two in a coordinate plane"] + ,["38","Exams for HS juniors"] + ,["41","Currency that anagrams to \"rial\""] + ,["42","Drink served with marshmallows"] + ,["44","Word after \"pop-up\" or \"set up\""] + ,["46","Unspecified degree"] + ,["47","\"I will not put up with this anymore!\""] + ,["51","Javelin, for one"] + ,["52","Soccer great Mia"] + ,["53","Educated guess: Abbr."] + ,["55","Like some diet food, in ads"] + ,["57","Land of Lima and llamas"] + ,["61","Emphatic grammatical constructions ... or the words hidden in 17-, 28- or 47-Across?"] + ,["65","Cajun stew"] + ,["66","___ de cologne"] + ,["67","Wipe clean"] + ,["68","Standing up straight"] + ,["69","Nickname that drops \"An\""] + ,["70","Fender flaws"] + ] + ,"Down":[ + ["1","Snatch"] + ,["2","Nevada slots city"] + ,["3","Greek god of love"] + ,["4","Catch an NPR show, say"] + ,["5","One or more"] + ,["6","Trailblazers"] + ,["7","Wildebeests"] + ,["8","In and of itself"] + ,["9","Wall St. debut"] + ,["10","Holy sister"] + ,["11","Funding, as a TV program"] + ,["12","Michelin product"] + ,["13","In addition"] + ,["18","Get-together"] + ,["19","University administrator"] + ,["24","Police officer"] + ,["26","Devour, in slang"] + ,["27","Airline to Jerusalem"] + ,["28","President who said, \"I am not a crook\""] + ,["29","Popular Halloween outfit with faux fur"] + ,["30","Unrefined"] + ,["31","Waist measurement"] + ,["32","\"For sure!\""] + ,["33","Indy 500, e.g."] + ,["37","Salad alternative"] + ,["39","City for Dutch royalty"] + ,["40","Beethoven's \"Moonlight ___\""] + ,["43","What birthday candles signify"] + ,["45","Fluffy toy dog, for short"] + ,["48","Robust"] + ,["49","Pressed, as clothes"] + ,["50","Baseball official"] + ,["53","Microsoft browser"] + ,["54","Sweet's counterpart"] + ,["56","Common cause of goose bumps"] + ,["58","\"Dear ___ Hansen\""] + ,["59","Many a squiggle on sheet music"] + ,["60","Employs"] + ,["62","Original \"Monty Python\" network"] + ,["63","Realtor's unit"] + ,["64","Big name in talks"] + ] +} +,"volatile":{ + "app.crossword.yourealwaysbe:playdata":"*" + ,"app.crossword.yourealwaysbe:supporturl":"" + ,"app.crossword.yourealwaysbe:shareurl":"" + ,"app.crossword.yourealwaysbe:ioversion":"" + ,"app.crossword.yourealwaysbe:images":"" +} +,"app.crossword.yourealwaysbe:supporturl":"http://www.uclick.com/client/spi/fcx/" +,"app.crossword.yourealwaysbe:ioversion":3 +} (DIR) diff --git a/spec/unit/crossword_spec.lua b/spec/unit/crossword_spec.lua @@ -5,8 +5,10 @@ describe("Crossword plugin module", function() local Solve local path_to_odd_puzzle = "plugins/crossword.koplugin/nyt_crosswords/2009/08/19.json" local path_to_even_puzzle = "plugins/crossword.koplugin/nyt_crosswords/2009/08/17.json" + local path_to_ipuz_puzzle = "plugins/crossword.koplugin/sample.ipuz" local odd_puzzle -- Size is 15 rows by 16 columns local even_puzzle -- Size is 15 rows by 15 columns + local ipuz_puzzle local game_view setup(function() @@ -18,18 +20,21 @@ describe("Crossword plugin module", function() Puzzle = require("puzzle") Solve = require("solve") - odd_puzzle = Puzzle:initializePuzzle(path_to_odd_puzzle) - even_puzzle = Puzzle:initializePuzzle(path_to_even_puzzle) + odd_puzzle = Puzzle:initializePuzzleFromFile(path_to_odd_puzzle) + even_puzzle = Puzzle:initializePuzzleFromFile(path_to_even_puzzle) + ipuz_puzzle = Puzzle:initializePuzzleFromFile(path_to_ipuz_puzzle) end) describe("Puzzles", function() it("should have correct count of rows", function() assert.are.same(15, odd_puzzle.size.rows) assert.are.same(15, even_puzzle.size.rows) + assert.are.same(15, ipuz_puzzle.size.rows) end) it("should have correct count of columns", function() assert.are.same(16, odd_puzzle.size.cols) assert.are.same(15, even_puzzle.size.cols) + assert.are.same(15, ipuz_puzzle.size.cols) end) it("should return correct index from coordinates", function() assert.are.same(1, even_puzzle:getIndexFromCoordinates(1,1))