commit 7497b0e087480865b11c3393fb38bb4eae22e690
parent dc110fd819b95cf132b9abb24d953ff5b9654a63
Author: Michael Camilleri <[email protected]>
Date: Tue, 30 Jun 2026 13:23:56 +0900
Add partial fill hints
This commit adds 'Fill Quarter' and 'Fill Half' to the Hints menu so the
user can ask Crossmate to solve a random slice of the remaining puzzle
without revealing the whole grid.
The new commands compute their target count from the still-empty
fillable cells, round up so a non-empty remainder always fills at least
one square, and route the selected cells through the existing reveal
pipeline.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
4 files changed, 76 insertions(+), 0 deletions(-)
diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift
@@ -326,6 +326,26 @@ final class PlayerSession {
publishLocalCompletion()
}
+ func fillQuarter() {
+ fillRemainingPuzzle(fraction: 0.25)
+ }
+
+ func fillHalf() {
+ fillRemainingPuzzle(fraction: 0.5)
+ }
+
+ private func fillRemainingPuzzle(fraction: Double) {
+ let remaining = puzzle.cells.flatMap { $0 }.filter { cell in
+ guard !cell.isBlock, cell.solution != nil, !cell.expectsBlank else { return false }
+ return game.squares[cell.row][cell.col].entry.isEmpty
+ }
+ guard !remaining.isEmpty else { return }
+
+ let fillCount = max(1, Int((Double(remaining.count) * fraction).rounded(.up)))
+ mutator.revealCells(Array(remaining.shuffled().prefix(fillCount)))
+ publishLocalCompletion()
+ }
+
func clearCurrentWord() {
mutator.clearCells(currentWordCells())
}
diff --git a/Crossmate/Views/Puzzle/PuzzleCommands.swift b/Crossmate/Views/Puzzle/PuzzleCommands.swift
@@ -94,6 +94,13 @@ struct PuzzleCommands: Commands {
Divider()
+ Button("Fill Quarter") { target?.session.fillQuarter() }
+ .disabled(!isEnabled)
+ Button("Fill Half") { target?.session.fillHalf() }
+ .disabled(!isEnabled)
+
+ Divider()
+
Button("Reveal Square") { target?.requestReveal(.square) }
.keyboardShortcut("r", modifiers: .command)
.disabled(!isEnabled)
diff --git a/Crossmate/Views/Puzzle/PuzzleModifiers.swift b/Crossmate/Views/Puzzle/PuzzleModifiers.swift
@@ -129,6 +129,10 @@ struct PuzzleToolbarModifier: ViewModifier {
Button("Check Puzzle") { session.checkPuzzle() }
}
Section {
+ Button("Fill Quarter") { session.fillQuarter() }
+ Button("Fill Half") { session.fillHalf() }
+ }
+ Section {
Button("Reveal Square") { confirmReveal(.square) }
Button("Reveal Word") { confirmReveal(.word) }
Button("Reveal Puzzle") { confirmReveal(.puzzle) }
diff --git a/Tests/Unit/GameMutatorTests.swift b/Tests/Unit/GameMutatorTests.swift
@@ -172,6 +172,41 @@ struct GameMutatorTests {
#expect(game.squares[0][0].mark == .revealed)
}
+ @Test("fill quarter reveals a quarter of empty fillable cells")
+ func fillQuarterRevealsQuarterOfRemainingCells() throws {
+ let (game, mutator, _, _) = try makeTestGame()
+ let session = PlayerSession(game: game, mutator: mutator)
+
+ session.fillQuarter()
+
+ #expect(revealedCellCount(in: game) == 2)
+ #expect(revealedCells(in: game).allSatisfy { cell in
+ game.squares[cell.row][cell.col].entry == cell.solution?.uppercased()
+ })
+ }
+
+ @Test("fill half counts only still-empty cells")
+ func fillHalfCountsOnlyEmptyRemainingCells() throws {
+ let (game, mutator, _, _) = try makeTestGame()
+ let session = PlayerSession(game: game, mutator: mutator)
+
+ mutator.setLetter("A", atRow: 0, atCol: 0, pencil: false)
+ mutator.setLetter("B", atRow: 0, atCol: 1, pencil: false)
+ mutator.setLetter("C", atRow: 0, atCol: 2, pencil: false)
+ mutator.setLetter("D", atRow: 1, atCol: 0, pencil: false)
+
+ session.fillHalf()
+
+ #expect(revealedCellCount(in: game) == 2)
+ #expect(revealedCells(in: game).allSatisfy { cell in
+ game.squares[cell.row][cell.col].entry == cell.solution?.uppercased()
+ })
+ #expect(!game.squares[0][0].mark.isRevealed)
+ #expect(!game.squares[0][1].mark.isRevealed)
+ #expect(!game.squares[0][2].mark.isRevealed)
+ #expect(!game.squares[1][0].mark.isRevealed)
+ }
+
@Test("clearCells clears non-revealed cells")
func clearCellsClearsNonRevealed() throws {
let (game, mutator, _, _) = try makeTestGame()
@@ -402,6 +437,16 @@ struct GameMutatorTests {
)
}
+ private func revealedCellCount(in game: Game) -> Int {
+ game.squares.flatMap { $0 }.filter { $0.mark.isRevealed }.count
+ }
+
+ private func revealedCells(in game: Game) -> [Puzzle.Cell] {
+ game.puzzle.cells.flatMap { $0 }.filter { cell in
+ game.squares[cell.row][cell.col].mark.isRevealed
+ }
+ }
+
/// Reads back the per-cell entry at `(row, col)` from the local-device
/// MovesEntity for `gameID`. Used by emission tests to verify what got
/// persisted into the cells blob.