Editing the Acrostic Grid

Status: Approved | January 2025

Author: Jonathan Blandford

Reviewer: Tanmay Patil

Overview

Acrostic puzzles have some unique constraints that need to be satisfied before they’re a well formed puzzle. The editor lets you edit parts of the constraints putting the puzzle in partially formed or error states. Changing some of the constraints can be particularly destructive. It’s not clear what grid editing actions should exist and what’s an acceptable level of destruction. In addition, it’s not clear what the correct behavior for the _fix() functions — and the UI — should be.

Problem Statement

Acrostics have the following constraints:

  1. The quote string can be read from the grid (and vice versa)

  2. The first letter of each answer spell out the source string

  3. Every cell in the grid is used exactly once for an answer, and has a 1:1 mapping between a clue and a cell.

We introduce the concept of the acrostic being in a well formed, partially formed, or an error state in the editor.

When all the contraints are met and the puzzle is complete, it’s in a well-formed state. When constraints are all possible to be met, but we don’t have a full set of answers, the puzzle is in a partially formed state. When we have contradictions in the set of characters then we’re in an error state.

We don’t have the APIs currently to handle the partially formed and error states. Consequentially the puzzle is hard to work with in the editor.

Existing fix functions

IpuzAcrostic has the following fix functions:

  • fix_quote(): Syncs between the grid and to the quote string. Takes an argument to indicate direction of synchronization. Will (potentially) resize the board and clears all quote strings.

  • fix_source(): Syncs between the clues and the source string. Takes an argument to indicate direction of synchronization. Changes the number of clues in the puzzle and clears them.

  • set_answers(): Takes a list of answers that satisfies the constraints set by the quote and source strings. It will set the clues. Unlike the quote_str and source_str, the answers are stored as the clue cells and aren’t kept as a lookaside value.

  • fix_labels(): Updates the labels of every cell to match that of the clue.

Proposal

tl;dr: Never let the puzzle get into an error state in the editor as the result of a user action.

Supported User Actions

We want to support the following user actions in the editor:

  • Edit the quote string

  • Edit the source string

  • Edit and write individual answers

  • Use the autofill functionality on either a portion of the answer space, or the full puzzle.

Edit the quote string

The user edits the quote string

Editing the quote string will change the size and shape of the grid, as well as the valid characters for the grid. It will also (potentially) invalidate any clues that currently exists. It’s possible to make minor changes to a quote and keep the puzzle largely the same. Alternatively, it’s also possible to put the puzzle into an error state invalidating everything.

When the user changes the quote string, we should see if the source string still works with the original quote. If so we are in a partially formed state, otherwise we’re in an error state. We can then choose the actions in one of the following two options:

Option 1: Partially Formed

  1. Take a snapshot of all existing answers in the puzzle.

  2. Recreate the grid with the new quote string

  3. Go through each answer serially as they exists, and see if it can be used with the quote letters. Add back the ones that work. Add any clues as well.

  4. Update the puzzle labels to not include clues

  5. Write the puzzle to the undo stack.

Option 2: Error State

  1. Possibly warn the user about a destructive change?

  2. Recreate the grid with the new quote string

  3. Clear the source string

  4. Clear all answers

  5. Update the puzzle labels to not include clues

  6. Write the puzzle to the undo stack.

That would make the callback look something like:

static void
quote_string_changed_cb (quote_str)
{
  answer_list = snapshot_answers ();
  set_quote_str (quote_str);
  fix_quote_str ();
  state = check_acrostic_state ();
  if (state == ERROR)
    {
       set_source_str ("");
       fix_source_str ();
       // state should be partial now, by definition
    }
  else if (state == PARTIAL)
    {
       for (guint i = 0; i < answer_list->len; i++)
         {
           // This call will fail if there aren't enough letters
           set_answer (i, answer_list[i]);
         }
       // Recheck to see if all the letters are used
       state = check_acrostic_state ();
    }

  if (state == WELL_FORMED)
    fix_labels (CLUES);
  else
    fix_labels (NO_CLUES);

  push_change ();
}

Edit the source string

The user edits the source string

We should make setting of a source to be secondary to the quote, and prevent the user from writing one that’s not a subset of its characters.

Editing the source string will change the number of answers, or as well as the valid characters for the grid. It will also (potentially) invalidate any clues that currently exists.

  1. Take a snapshot of all existing answers in the puzzle.

  2. Recreate the answers with the new first letters

  3. Go through each answer serially as they exists, and see if it can be used with the new initial letters and quote letters. Add back the ones that work.

  4. Write the answers to the puzzle

  5. Update the puzzle labels to not include clues

  6. Write the puzzle to the undo stack.

That would make the callback look something like:

static void
source_string_changed_cb (source_str)
{
  answer_list = snapshot_answers ();
  set_source_str (quote_str);
  fix_grid ();
  state = check_acrostic_state ();
  if (state == ERROR)
    {
       // We shouldn't let you set a source_str that's invalid
       g_assert_not_reached();
    }
  else if (state == PARTIAL)
    {
       for (guint i = 0; i < answer_list->len; i++)
         {
           set_answer (i, answer_list[i]);
         }
       fix_labels (NO_CLUES);
    }
  else // Well formed puzzle
    {
      fix_labels (NO_CLUES);
    }
  push_change ();
}

Edit an answer

This is a little simpler from the perspective of the puzzle, though perhaps a more complex API. The one thing we do is update the answer. One challenge is that we don’t want to have the puzzle in an error state, which means the answer widget should only emit

static void
source_answer_changed_cb (index, new_answer)
{
  set_answer (index, new_answer);
  state = check_acrostic_state ();

  if (state == ERROR)
    g_assert_not_reached();
  else if (state == PARTIAL)
    fix_labels (NO_CLUES);
  else // Well formed puzzle
    fix_labels (NO_CLUES);
  push_change ();
}

Libipuz changes

We need to be able to represent the puzzle in libipuz when it’s in a partially-formed state. This is for two reasons. First, we need to be able to push the puzzle to the undo stack when it’s in this form. We have to capture those changes when they occur. Second, the user may want to save an acrostic while in the middle of creating it.

As a convention, I propose we don’t update the labels in the editor to include the clue numbers when we’re in a partially formed state. That will avoid the numbers bouncing around when editing, and give a visual indication that it’s not done. We may want to write them out when saving.

To implement this we should:

  • Change set_answers() to accept a partial list of answers, or potentially just one answer.

  • Change fix_labels() to take an argument about whether it should map the clues, or just include a cell number.

NOTE: It’s possible to manually set the puzzle into an error state and save it through raw calls to the library, or through manually editing the file. The editor shouldn’t allow that, and should fix up the puzzle as best as possible when loading from disk.

Post push

Once a new acrostic puzzle has been pushed, we need to prepare it for updating widgets. One important thing to do is to recalculate the Charset of characters in the puzzle, and the Charset of the current set of answers. If they’re identical, then the puzzle is well formed. That will be useful to pass to the various update() functions.

Actions

  • [ ] Create widgets for editing an acrostic grid.

    • [ ] Widget for quote_str

    • [ ] Widget for source_str

    • [ ] Widget for answers. The autofill fill feature will be embedded in the answer widget.

  • [ ] Setup callbacks for widgets

  • [ ] Post push propagation and validation

  • [X] (libipuz) Change fix_labels() to take an enum indicating how to take the

  • [ ] (libipuz) Add an set_answer() function equivalent for just one answer.

  • [X] (libipuz) Add ipuz_charset_subset()

Other Thoughts

One other long-standing action is to make IpuzAcrostic inherit from IpuzGrid. We don’t really use any of the functions from IpuzCrossword other than fix_all() and fix_style)_.

It’s good to not refactor too much at once so we will do that as a separate action. Bbut care should be taken in the implementation to make sure that we don’t make that task harder.