Revamping the libipuz edit APIs

Status: Reviewed federico@gnome.org | partially-implemented

Summary

Currently, all editing is split between xword-state.c and libipuz. We propose a structured way to handle editing to make it easier to reason about, and to put the majority of the complexity in libipuz.

Currently, the fix() API is implemented, but the linters are not.

Problem Statement

There currently exists a mixture of API levels used to mutate puzzles through the editor. For example, it’s possible to change an individual cell within the puzzle by calling ipuz_cell_set_*, or ipuz_style_set_*. Similarly, you can change a clue. However, all these functions bring some challenges with them, that can lead to an internally inconsistent state:

  • Changing the cell_type can leave the numbering and clue cells inconsistent.

  • Changing the symmetry can be super destructive

  • Even clues are super-tricky: We need to make sure every cell knows which clue it’s part, and vice-versa.

Additionally, there are some conventions we want to enforce that aren’t part of the ipuz spec. In particular:

  • For crosswords, there are only across/down clues, and every cell is part of at least one clue.

  • We are correctly numbered. Clue numbers increase by 1, and there are no gaps

  • We store the clue cells within the puzzle, and they don’t have discontinuities

We tried to work around this in calls from xword-state.c by introducing the ipuz_crossword_set_cell_type() function, as well as storing the symmetry within the puzzle. This is always immediatedly followed but a call to ipuz_crossword_calculate_clues(). This level of granularity isn’t sufficient for what we need to do and hasn’t aged well.

Challenges

Subtypes

Arrowword, Filippine, and Barred puzzles inherit from Crossword, and Acrostics don’t inherit from Crosswords at all. They don’t share the same conventions that Crossword has, though there are some commonalitites. All of these will need their own adjustment to editing.

For example, Filippine puzzles only have across clues, and have a vertical line that’s highlighted with the answer. Barred puzzles don’t have blocks or nulls, and expect “T”, “L”, and “TL” styles to exist. Arrowwords need to have the arrow updated. Symmetry makes almost no sense for Acrostics.

NOTE: In addition, both Filippine and Acrostic puzzles will benefit from a locked-down custom editor that doesn’t even let you change the grid directly. They might want to bypass XwordState’s editing system entirely. Given that we called it XwordState, the introduction of an AcrosticState wouldn’t be surprising, but this may be substantial work and sources of errors.

Raw Mode

Some puzzles do need to break the rules. There are fun puzzles with diagonal clues, or ‘J’ hooks that don’t reveal themselves immediately. Maybe you do want a barred puzzle with blocks and/or discontinuities. We may want to consider adding a raw mode to the editor that just uses the _set() functions and lets you produce valid-but-unusual puzzles. This mode would require people to automatically set the range of cells of clues, for example.

Proposal

libipuz changes

  • We will have three different APIs for libipuz. First the existing one demarked by the ipuz _set() functions. These will let you manipulate the puzzle however you see fit. Example: ipuz_cell_set_label()

  • We add a set of linting APIs demarked by _check() functions. This will return a list of potentially broken attributes that might need fixing. We will have to define a set of types for each check type. Example: ipuz_crossword_check_cell_labels().

  • We add a correction API, demarked by _fix() functions

    • _fix() functions may need arguments. For example, fix_symmetry() will need to take in a list of modified cells to update. Otherwise, we won’t know which direction to change things.

  • As a convenience, we add an ipuz_crossword_fix_all() function to call the fix functions in the correct order.

  • We remove symmetry as a tracked feature of crosswords.

Here’s a diagram of this behavior.

libipuz editing changes overview
sketch of editing changes

NOTE: We will make both the _check() and _fix() function class methods, so that child types can override or chain to them. Most notably, fix_all() also needs to be a class_method. fix_all() will also need to take in arguments, so the class method will require kwargs.

XwordState changes

  • We move any settings to give us hints about the type of fixes to the Quirks object.

    • We will introduce a mangaged_mode/raw_mode enum to the Quirks object to track whether we want to fix things up automatically or not.

    • As another example, symmetry is another quirk setting

  • All current mutators within xword_state will fix up puzzle based on that mode.

Editor Changes / Raw Mode

  • There’s a (currently unwritten) style tab in the editor that will let you customize the label / style of grids. We will have to prevent the user from breaking assumptions

  • The current edit- widgets were written to expect the puzzle type and base puzzle to change. We will make it so that PUZZLE_KIND is a construct-only property. raw_mode should be a property that can change, and the widgets have to adjust themselves to adapt.