commit 46a8f86ec2037ad71368553fd944841dae1c72b2
parent e4f3eda2a2463b946a69887865b437b1c387fbdd
Author: Michael Camilleri <[email protected]>
Date: Mon, 11 May 2026 01:13:38 +0900
Fix movement when typing last letter in a direction
Diffstat:
3 files changed, 109 insertions(+), 10 deletions(-)
diff --git a/Crossmate.xcodeproj/project.pbxproj b/Crossmate.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 00F2108848ADC7B4BF3AA0AE /* PlayerSessionNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46801B570FC0B2C791ECDED3 /* PlayerSessionNavigationTests.swift */; };
014134FB81566B5D41168260 /* PerGameZoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283C5C97180C805B6C5BF622 /* PerGameZoneTests.swift */; };
0241DC498C645FE1BDA00FB0 /* NYTPuzzleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0938B0ACB40772EE522D77C /* NYTPuzzleFetcher.swift */; };
02943BA53D2130B910E6DC00 /* EnsureGameEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CEBA27A8AC4FCC92ADE1B4 /* EnsureGameEntityTests.swift */; };
@@ -131,6 +132,7 @@
46148CF0F4D719692F81A6EC /* PlayerPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPreferences.swift; sourceTree = "<group>"; };
462CE0FD356F6137C9BFD30F /* ImportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportService.swift; sourceTree = "<group>"; };
465F2BB469EFE84CF3733398 /* Game.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = "<group>"; };
+ 46801B570FC0B2C791ECDED3 /* PlayerSessionNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSessionNavigationTests.swift; sourceTree = "<group>"; };
47532AED239AEF476D8E9206 /* NotificationStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStateTests.swift; sourceTree = "<group>"; };
4803C2E84FC5B3BFE593171D /* NameBroadcaster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameBroadcaster.swift; sourceTree = "<group>"; };
4AF633D73818BD59F759FAC4 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@@ -251,6 +253,7 @@
ED8154F949E1D94252F70765 /* NYTAuthServiceTests.swift */,
C54223FED97577A593B7964E /* NYTToXDConverterTests.swift */,
1813630FA05C194AFF43855C /* PlayerRosterTests.swift */,
+ 46801B570FC0B2C791ECDED3 /* PlayerSessionNavigationTests.swift */,
ACD511B95227C7D57F32A2AC /* PresencePublisherTests.swift */,
FDE193CAB325C991952D7CE5 /* PUZToXDConverterTests.swift */,
B8560440C548752EE93E0ED9 /* PuzzleCatalogTests.swift */,
@@ -511,6 +514,7 @@
C89A15D812E372FE1C56039B /* PUZToXDConverterTests.swift in Sources */,
014134FB81566B5D41168260 /* PerGameZoneTests.swift in Sources */,
04062BCD473ED244159B1066 /* PlayerRosterTests.swift in Sources */,
+ 00F2108848ADC7B4BF3AA0AE /* PlayerSessionNavigationTests.swift in Sources */,
090039C84FAE212D5B0EA03F /* PresencePublisherTests.swift in Sources */,
F34EDFD45E2F5006807DDAC7 /* PuzzleCatalogTests.swift in Sources */,
7FCD3F582B5ADC235E1F88A0 /* PuzzleNotificationTextTests.swift in Sources */,
diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift
@@ -384,10 +384,10 @@ final class PlayerSession {
let r = selectedRow + dr
let c = selectedCol + dc
// If we're still inside the current word, step one cell. Otherwise
- // we've hit the end of the word, so jump to the next clue in the same
- // direction by number. Without that, we'd fall through the block into
- // a different word in the same row/column — which for down clues is
- // almost never the next clue by number.
+ // we've hit the end of the word, so jump to the next clue in the full
+ // clue list. Without that, we'd fall through the block into a different
+ // word in the same row/column — which for down clues is almost never
+ // the next clue by number.
if isValid(row: r, col: c) && !puzzle.cells[r][c].isBlock {
selectedRow = r
selectedCol = c
@@ -397,12 +397,7 @@ final class PlayerSession {
}
private func advanceToNextClue() {
- let clues = direction == .across ? puzzle.acrossClues : puzzle.downClues
- guard !clues.isEmpty else { return }
- let currentNumber = currentClueNumber()
- let currentIndex = clues.firstIndex { $0.number == currentNumber } ?? -1
- let nextIndex = (currentIndex + 1) % clues.count
- moveToClueStart(number: clues[nextIndex].number)
+ moveClue(by: +1)
}
private func retreat() {
diff --git a/Tests/Unit/PlayerSessionNavigationTests.swift b/Tests/Unit/PlayerSessionNavigationTests.swift
@@ -0,0 +1,100 @@
+import Foundation
+import Testing
+
+@testable import Crossmate
+
+@Suite("PlayerSession navigation", .serialized)
+@MainActor
+struct PlayerSessionNavigationTests {
+ @Test("Next clue from final across moves to first down")
+ func nextClueFromFinalAcrossMovesToFirstDown() throws {
+ let session = try makeNavigationSession()
+ let finalAcross = try #require(session.puzzle.acrossClues.last)
+ let firstDown = try #require(session.puzzle.downClues.first)
+
+ session.selectClue(direction: .across, number: finalAcross.number)
+ session.goToNextClue()
+
+ #expect(session.direction == .down)
+ #expect(session.currentClue()?.number == firstDown.number)
+ }
+
+ @Test("Previous clue from first down moves to final across")
+ func previousClueFromFirstDownMovesToFinalAcross() 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.goToPreviousClue()
+
+ #expect(session.direction == .across)
+ #expect(session.currentClue()?.number == finalAcross.number)
+ }
+
+ @Test("Next clue from final down moves to first across")
+ func nextClueFromFinalDownMovesToFirstAcross() throws {
+ let session = try makeNavigationSession()
+ let firstAcross = try #require(session.puzzle.acrossClues.first)
+ let finalDown = try #require(session.puzzle.downClues.last)
+
+ session.selectClue(direction: .down, number: finalDown.number)
+ session.goToNextClue()
+
+ #expect(session.direction == .across)
+ #expect(session.currentClue()?.number == firstAcross.number)
+ }
+
+ @Test("Typing past final across moves to first down")
+ func typingPastFinalAcrossMovesToFirstDown() throws {
+ let session = try makeNavigationSession()
+ let finalAcross = try #require(session.puzzle.acrossClues.last)
+ let firstDown = try #require(session.puzzle.downClues.first)
+
+ session.selectClue(direction: .across, number: finalAcross.number)
+ session.select(row: 2, col: 2)
+ session.setDirection(.across)
+ session.enter("H")
+
+ #expect(session.direction == .down)
+ #expect(session.currentClue()?.number == firstDown.number)
+ }
+
+ @Test("Typing past final down moves to first across")
+ func typingPastFinalDownMovesToFirstAcross() throws {
+ let session = try makeNavigationSession()
+ let firstAcross = try #require(session.puzzle.acrossClues.first)
+ let finalDown = try #require(session.puzzle.downClues.last)
+
+ session.selectClue(direction: .down, number: finalDown.number)
+ session.select(row: 2, col: 2)
+ session.setDirection(.down)
+ session.enter("H")
+
+ #expect(session.direction == .across)
+ #expect(session.currentClue()?.number == firstAcross.number)
+ }
+
+ private func makeNavigationSession() throws -> PlayerSession {
+ let source = """
+ Title: Navigation Test
+ Author: Test
+
+
+ ABC
+ D#E
+ FGH
+
+
+ A1. First across ~ ABC
+ A3. Final across ~ FGH
+ D1. First down ~ ADF
+ D2. Final down ~ CEH
+ """
+
+ let puzzle = Puzzle(xd: try XD.parse(source))
+ let game = Game(puzzle: puzzle)
+ let mutator = GameMutator(game: game, gameID: UUID(), movesUpdater: nil)
+ return PlayerSession(game: game, mutator: mutator)
+ }
+}