Immutable XwordState

Status: Implemented | 2023

XwordState is mutable, and we carefully maintain it for the lifetime of a puzzle. This has a few shortcomings:

  • Implementing Undo/Redo is hard. We’d like to keep a history of states.

  • The tests have to implement the “compare old and new states” logic themselves, instead of just using a hypothetical play_state_equal(a, b).

Turning XwordStateFocus into XwordStateAction

Issue #11 is about renaming the XwordStateFocus enum to XwordStateAction, since now it contains PLAY_STATE_DELETE. I propose that we bite the bullet and make XwordStateAction a complete description of the actions that can be taken on a XwordState.

  typedef enum {
    PLAY_STATE_ACTION_NONE,
    PLAY_STATE_ACTION_UP,
    PLAY_STATE_ACTION_DOWN,
    PLAY_STATE_ACTION_LEFT,
    PLAY_STATE_ACTION_RIGHT,
    PLAY_STATE_ACTION_FORWARD,
    PLAY_STATE_ACTION_BACK,
    PLAY_STATE_ACTION_FORWARD_CLUE,
    PLAY_STATE_ACTION_BACK_CLUE,
    PLAY_STATE_ACTION_BACKSPACE,
    PLAY_STATE_ACTION_DELETE,
    PLAY_STATE_ACTION_SWITCH,
    PLAY_STATE_ACTION_GUESS,       /* has data, see below */
    PLAY_STATE_ACTION_SELECT_CLUE, /* has data, see below */
  } XwordStateActionKind;

  typedef struct {
    XwordStateActionKind kind;

    union {
      char *guess;
      ClueId select_clue;
      /* etc. for other actions that take arguments */
    } u;
  } XwordStateAction;

Then we can have a single

XwordState play_state_dispatch(const XwordState *state, XwordStateAction action);

which returns a new state, not the old one modified. It takes the whole action description and acts upon it; we can then remove play_state_clue_selected() et al piecemeal.

Along with “immutable puzzles” below, this would make testing easier, and would open the door for implementing Undo/Redo.

Immutable puzzles

Right now IpuzCrossword owns its IpuzGuesses, but other than setters and getters, the only place where it is used is in the internal ipuz_crossword_game_won_solution. I think we should remove the guesses from the crossword, and instead have

gboolean ipuz_crossword_game_won (IpuzCrossword *xword, IpuzGuesses *guesses);

(Or call it is_solution or whatever.)

Undo/Redo

NOTE: This section is obsolete. We ended up not undoing the actions, but using a PuzzleStack instead.

With the above, we can make XwordState own the IpuzGuesses. Then we can have this undo stack easily:

state 0 -> action -> state 1 -> action -> state 2 -> action -> state 3
 with                 with                 with                 with
guesses              guesses              guesses              guesses

Whether we keep all the states, or just the ones that change the guesses, is up to you. Emacs does the same “collapse undo steps while typing” thing.

Editability and libipuz paraphernalia

I don’t know if you’ll later want to fuse guesses into a crossword to turn them into the crossword’s built-in solution.

Maybe we need an EditState different from XwordState? It seems that the behavior should be different, since the editor will have actions like EDIT_ACTION_TURN_CELL_INTO_BLOCK or EDIT_ACTION_TURN_CELL_INTO_NULL, or whatever actions we have later to add fancy markers to individual grid lines.

(I know, I know, I should get going on the new layout code…)

I think it’s fine to have IpuzCrossword have a mutability API, but it would be easier if the crossword editor app kept it immutable by copying the old xword to a new one and then modifying cells in the new one. This way we can also have an undo stack for the editor.