Intro to libipuz

Intro

LIBIPUZ is a library for loading, manipulating, and saving puzzles. It loads .ipuz files and provides a programatic interface to manipulate them. It is written in C and Rust and has a C-based API using glib. This API can be accessed through GObject Introspection, and thus can be used in many other languages. It has been tested in C, Python, and Typescript.

The primary goal of libipuz is to provide an API that loosely mirrors the ipuz structure while providing additional functionality. While ipuz files can be loaded directly into memory as json objects, working with the raw structure is often inconvenient. In addition, a robust set of mutators are provided for manipulating / creating a puzzle while maintaining common heuristics.

Fundamentals

Puzzles in libipuz are all represented by the IpuzPuzzle object. This is the parent class of all puzzle kinds and provides common properties associated with all puzzles, such as title, author, etc.

The IpuzGrid object is the parent class of all grid-based puzzles and provides basic access to a rectangular grid of cells. The IpuzClues interface can be used to read the clues of crossword-style puzzles.

Note

We currently only support a limited number of ipuz kinds: Crosswords, Acrostics, and Nonograms, The full set of puzzle types is planned, and Sudoku-style games are currently under development. See this document for current status.

Mutations

Changes in puzzles can often be very complex. For gridded puzzles, small changes can have a large impact on the overall puzzle structure. For example, consider the act of changing a cell from a normal cell to a block in a crossword. This can result in the grid and clues needing renumbering, clues disappearing or appearing, enumeration lengths changing, etc. Symmetry requirements may also need to be maintained.

On the other hand, it occasionally is interesting to have a board with non-standard semantics. As an example, Alphabetic jigsaw crosswords don’t use clue numbers.

Consequentially, the overall philosophy of libipuz is to let the developer make any changes they want to a puzzle, even if it results in a non-standard puzzle. Once a set of changes is made, developers can call the _fix() category of functions in order to enforce well-known heuristics. This allows them to choose which heuristics to apply and how to apply them. The resulting puzzle should be a well formed puzzle, suitable for another round of mutation.

Simple Examples

Here are a few simple examples using libipuz:

C

This snippet will load a puzzle from disk and set a guess of “A” for the first cell.

g_autoptr (IpuzPuzzle) puzzle = NULL;
g_autoptr (IpuzGuesses) guesses = NULL;
g_autoptr (GError) error = NULL;
IpuzCellCoord coord = {
  .row = 0,
  .column = 0,
};

puzzle = ipuz_puzzle_new_from_file (filename, &error);
if (error != NULL)
  {
    g_warning ("Error Loading file: %s", error->message);
    return;
  }

guesses = ipuz_grid_create_guesses (IPUZ_GRID (puzzle));
ipuz_guesses_set_guess (guesses, &coord, "A");

Python

This is a more complex example of creating the simple crossword that appears in the ipuz spec. In this, we set the size of the puzzle and change the solution/type of the cells. Then, we call of the _fix() functions. This will apply valid numbering to the grid and create empty clues to fill the open spaces. We then fill in the clues with appropriate text.

Once done, it will save the puzzle to disk.

simple.ipuz created through libipuz simple.ipuz created through libipuz

import gi
gi.require_version('Ipuz', '1.0')
from gi.repository import (Ipuz)

# Define the puzzle
cells = [ [ "C", "A", "#" ], [ "B", "O", "T" ], [ None, "L", "O" ] ]
across_clues = [ "OR neighbor",
                 "Droid",
                 "Behold!" ]
down_clues = [ "Trucker's radio",
               "MSN competitor",
               "A preposition" ]

# Create a crossword
c = Ipuz.Crossword ()
c.resize (3, 3)
c.set_title ('Example crossword')
c.set_author ('Angela Avery')

# Populate the grid
coord = Ipuz.CellCoord()
for row in range (0, 3):
    for column in range (0, 3):
        val = cells[row][column]
        coord.row = row
        coord.column = column
        cell = c.get_cell(coord)

        if val == '#':
            cell.set_cell_type(Ipuz.CellType.BLOCK)
        elif val == None:
            cell.set_cell_type(Ipuz.CellType.NULL)
        else:
            cell.set_solution(val)

        if row == column:
            style = Ipuz.Style()
            style.set_shapebg(Ipuz.StyleShape.CIRCLE)
            cell.set_style (style, None)

# Update the numbering of the grid and create blank clues to match the numbers
c.fix_numbering ()
c.fix_clues ()

# Set the text of each clue
clue_id = Ipuz.ClueId()

clue_id.direction = Ipuz.ClueDirection.ACROSS
for i in range(0, len (across_clues)):
    clue_id.index = i
    c.get_clue_by_id(clue_id).set_clue_text(across_clues[i])

clue_id.direction = Ipuz.ClueDirection.DOWN
for i in range(0, len (down_clues)):
    clue_id.index = i
    c.get_clue_by_id(clue_id).set_clue_text(down_clues[i])

# Save the puzzle to disk
c.save_to_file ('simple.ipuz')