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:
The quote string can be read from the grid (and vice versa)
The first letter of each answer spell out the source string
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
andsource_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
Take a snapshot of all existing answers in the puzzle.
Recreate the grid with the new quote string
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.
Update the puzzle labels to not include clues
Write the puzzle to the undo stack.
Option 2: Error State
Possibly warn the user about a destructive change?
Recreate the grid with the new quote string
Clear the source string
Clear all answers
Update the puzzle labels to not include clues
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.
Take a snapshot of all existing answers in the puzzle.
Recreate the answers with the new first letters
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.
Write the answers to the puzzle
Update the puzzle labels to not include clues
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.