crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 87a34db1e8a7643fa858cf371ce38ac368da7117
parent 7e4231a4ffbe21e9f6f7f670e658c1fcbe9275b6
Author: Michael Camilleri <[email protected]>
Date:   Mon, 15 Jun 2026 18:17:56 +0900

Show canonical rebus fills on a solved puzzle

When a rebus square accepts more than one reading — most visibly a
Schrödinger square — the solver may only ever type one of them, so the
finished grid can show only a partial entry rather than the full answer.
The reveal path preserves a correct entry on purpose, and completion
sealing keeps it too, so the canonical fill will never appear, even once
the puzzle is done.

This commit substitutes the canonical solution for any multi-character
rebus square once the puzzle reads as solved, so the cell shows the
whole fill at the end. The substitution happens only when rendering the
grid: the stored entry, its authorship, and its marks are left
untouched, nothing is corrected or re-authored, and replay frames keep
their reconstructed history. Ordinary single-letter cells are
unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Views/GridView.swift | 20++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/Crossmate/Views/GridView.swift b/Crossmate/Views/GridView.swift @@ -24,6 +24,11 @@ struct GridView: View { let replayCells = replayFrame?.cells let isReplaying = replayFrame != nil let replayCursor = replayFrame?.cursor + // Once the puzzle is solved, rebus squares show their canonical fill + // (e.g. a Schrödinger square reads "MTWA" rather than the "WA" the + // solver typed for the down answer). Purely a render-time substitution + // — the stored entry, its authorship, and marks are left untouched. + let showsCanonicalRebus = !isReplaying && session.game.completionState == .solved // Peer cursor tints are rendered in a separate Canvas layer (see // `RemoteCursorTints`) so this 441-cell grid no longer re-evaluates on // every peer cursor move — only on local selection and letter changes. @@ -70,16 +75,18 @@ struct GridView: View { let r = index / width let c = index % width let pos = GridPosition(row: r, col: c) + let cell = session.puzzle.cells[r][c] let square = session.game.squares[r][c] // During replay the cell's letter/mark/author come from the // reconstructed history, not the live square. let replayCell = replayCells?[pos] let entry = isReplaying ? (replayCell?.letter ?? "") : square.entry + let displayEntry = canonicalRebusFill(for: cell, when: showsCanonicalRebus) ?? entry let mark = isReplaying ? (replayCell?.mark ?? .none) : square.mark let letterAuthorID = isReplaying ? replayCell?.cellAuthorID : square.letterAuthorID CellView( - cell: session.puzzle.cells[r][c], - entry: entry, + cell: cell, + entry: displayEntry, mark: mark, crossRefPattern: cellGroups[pos].map { patternPalette[$0 % patternPalette.count] @@ -101,6 +108,15 @@ struct GridView: View { } } + /// The canonical fill to display for a rebus square on a solved puzzle, or + /// `nil` to fall back to the stored entry. Only multi-character solutions + /// (rebus/Schrödinger squares) differ from what the solver typed; ordinary + /// single-letter cells return `nil` and render their entry unchanged. + private func canonicalRebusFill(for cell: Puzzle.Cell, when isSolved: Bool) -> String? { + guard isSolved, let solution = cell.solution, solution.count > 1 else { return nil } + return solution + } + } // MARK: - Layout