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))