Title: 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 is 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 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 a json object, reading 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 [class@Ipuz.Puzzle] object. This is the parent class of all puzzle types, and has common properties associated with all puzzles, such as title, author, etc.

The [class@Ipuz.Grid] object is the parent class of all gridded puzzles and provides basic access to a square grid of cells. The [iface@Ipuz.Clues] 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, A 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 in the grid can have 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 caller make any changes they want to a puzzle, even if it results in a non-standard grid. Once a set of changes is made, callers can then call the _fix() category of functions in order to enforce well-known heuristics within the puzzle. This allows the user 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 of 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 clues to fill the open spaces.

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.set_size (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 clues to match
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')