commit 5e688df84884a75c038e09b923b2910c210a5fbb
parent 46a8f86ec2037ad71368553fd944841dae1c72b2
Author: Michael Camilleri <[email protected]>
Date: Mon, 11 May 2026 01:18:27 +0900
Fix movement when deleting first letter of a word
Diffstat:
2 files changed, 85 insertions(+), 3 deletions(-)
diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift
@@ -164,9 +164,7 @@ final class PlayerSession {
// Walk every clue in order: all acrosses, then all downs. Stepping past
// the last across rolls into the first down (and vice versa going
// backwards), which matches how most crossword apps behave.
- let ordered: [(Puzzle.Direction, Puzzle.Clue)] =
- puzzle.acrossClues.map { (.across, $0) }
- + puzzle.downClues.map { (.down, $0) }
+ let ordered = orderedClues()
guard !ordered.isEmpty else { return }
let currentNumber = currentClueNumber()
@@ -181,6 +179,11 @@ final class PlayerSession {
moveToClueStart(number: newClue.number)
}
+ private func orderedClues() -> [(Puzzle.Direction, Puzzle.Clue)] {
+ puzzle.acrossClues.map { (.across, $0) }
+ + puzzle.downClues.map { (.down, $0) }
+ }
+
private func moveWord(by offset: Int) {
let clues = direction == .across ? puzzle.acrossClues : puzzle.downClues
guard !clues.isEmpty else { return }
@@ -257,6 +260,21 @@ final class PlayerSession {
}
}
+ private func moveToClueEnd(direction newDirection: Puzzle.Direction, number: Int) {
+ guard let start = puzzle.cell(numbered: number) else { return }
+ let (dr, dc) = step(for: newDirection)
+ var row = start.row
+ var col = start.col
+ while isValid(row: row + dr, col: col + dc),
+ !puzzle.cells[row + dr][col + dc].isBlock {
+ row += dr
+ col += dc
+ }
+ direction = newDirection
+ selectedRow = row
+ selectedCol = col
+ }
+
// MARK: - Input
func enter(_ letter: String) {
@@ -401,6 +419,12 @@ final class PlayerSession {
}
private func retreat() {
+ let start = wordStart(row: selectedRow, col: selectedCol, direction: direction)
+ if selectedRow == start.row && selectedCol == start.col {
+ retreatToPreviousClueEnd()
+ return
+ }
+
let (dr, dc) = step(for: direction)
var r = selectedRow - dr
var c = selectedCol - dc
@@ -414,6 +438,19 @@ final class PlayerSession {
}
}
+ private func retreatToPreviousClueEnd() {
+ let ordered = orderedClues()
+ guard !ordered.isEmpty else { return }
+
+ let currentNumber = currentClueNumber()
+ let currentIndex = ordered.firstIndex {
+ $0.0 == direction && $0.1.number == currentNumber
+ } ?? 0
+ let previousIndex = ((currentIndex - 1) % ordered.count + ordered.count) % ordered.count
+ let (newDirection, newClue) = ordered[previousIndex]
+ moveToClueEnd(direction: newDirection, number: newClue.number)
+ }
+
private func isValid(row: Int, col: Int) -> Bool {
row >= 0 && row < puzzle.height && col >= 0 && col < puzzle.width
}
diff --git a/Tests/Unit/PlayerSessionNavigationTests.swift b/Tests/Unit/PlayerSessionNavigationTests.swift
@@ -75,6 +75,51 @@ struct PlayerSessionNavigationTests {
#expect(session.currentClue()?.number == firstAcross.number)
}
+ @Test("Backspace from first down moves to final across end")
+ func backspaceFromFirstDownMovesToFinalAcrossEnd() throws {
+ let session = try makeNavigationSession()
+ let finalAcross = try #require(session.puzzle.acrossClues.last)
+ let firstDown = try #require(session.puzzle.downClues.first)
+
+ session.selectClue(direction: .down, number: firstDown.number)
+ session.deleteBackward()
+
+ #expect(session.direction == .across)
+ #expect(session.currentClue()?.number == finalAcross.number)
+ #expect(session.selectedRow == 2)
+ #expect(session.selectedCol == 2)
+ }
+
+ @Test("Backspace from first across moves to final down end")
+ func backspaceFromFirstAcrossMovesToFinalDownEnd() throws {
+ let session = try makeNavigationSession()
+ let firstAcross = try #require(session.puzzle.acrossClues.first)
+ let finalDown = try #require(session.puzzle.downClues.last)
+
+ session.selectClue(direction: .across, number: firstAcross.number)
+ session.deleteBackward()
+
+ #expect(session.direction == .down)
+ #expect(session.currentClue()?.number == finalDown.number)
+ #expect(session.selectedRow == 2)
+ #expect(session.selectedCol == 2)
+ }
+
+ @Test("Backspace from final across start moves to previous across end")
+ func backspaceFromFinalAcrossStartMovesToPreviousAcrossEnd() throws {
+ let session = try makeNavigationSession()
+ let firstAcross = try #require(session.puzzle.acrossClues.first)
+ let finalAcross = try #require(session.puzzle.acrossClues.last)
+
+ session.selectClue(direction: .across, number: finalAcross.number)
+ session.deleteBackward()
+
+ #expect(session.direction == .across)
+ #expect(session.currentClue()?.number == firstAcross.number)
+ #expect(session.selectedRow == 0)
+ #expect(session.selectedCol == 2)
+ }
+
private func makeNavigationSession() throws -> PlayerSession {
let source = """
Title: Navigation Test