commit 1bac07b8b12cdb3019a1914418944a54b5768d73
parent 5b8c9d9ec1fbf4a5299c023e769d85503d345128
Author: Michael Camilleri <[email protected]>
Date: Sun, 28 Jun 2026 22:46:27 +0900
Ignore empty check cells in recent changes
Checking a puzzle could make empty cells look like peer changes when the
user next opened the game. The check path skipped empty cells in the live
grid, but GameMutator still emitted a move for every non-block target, so
Check Puzzle uploaded fresh empty cells that the peer-change ledger could
later read as clears.
This commit limits check and clear bulk emissions to cells whose state
actually changed, while keeping reveal's existing already-correct emission
behaviour. It also makes PeerChangeLedger ignore first-seen empty cells so
old or malformed empty move entries cannot seed highlightable clears.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
4 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/Crossmate/Persistence/GameMutator.swift b/Crossmate/Persistence/GameMutator.swift
@@ -144,11 +144,13 @@ final class GameMutator {
let batch = UUID()
collectingBroadcast {
for (cell, priorState) in zip(applicable, before) {
+ let journalKind = self.kind(kind, ifChangedFrom: priorState, atRow: cell.row, atCol: cell.col)
+ if journalKind == nil, kind != .reveal { continue }
emitMove(
atRow: cell.row,
atCol: cell.col,
beforeState: priorState,
- journalKind: self.kind(kind, ifChangedFrom: priorState, atRow: cell.row, atCol: cell.col),
+ journalKind: journalKind,
batchID: batch
)
}
diff --git a/Crossmate/Sync/PeerChangeLedger.swift b/Crossmate/Sync/PeerChangeLedger.swift
@@ -46,6 +46,7 @@ enum PeerChangeLedger {
for (position, provenance) in current {
let letter = provenance.cell.letter
if let existing = recorded[position], existing.letter == letter { continue }
+ if recorded[position] == nil, letter.isEmpty { continue }
let writer = provenance.cell.authorID ?? provenance.writerAuthorID
result.append(
PeerChange(
diff --git a/Tests/Unit/GameMutatorTests.swift b/Tests/Unit/GameMutatorTests.swift
@@ -106,6 +106,28 @@ struct GameMutatorTests {
#expect(game.squares[0][0].mark == .pencil(checked: .wrong))
}
+ @Test("checking an empty puzzle emits no empty cell moves")
+ func checkingEmptyPuzzleEmitsNoEmptyCellMoves() async throws {
+ let (game, capture, updater, persistence) = try makeMutatorWithUpdater(actingAuthorID: "bob")
+ let mutator = makeMutator(
+ game: game,
+ updater: updater,
+ gameID: capture.gameID,
+ actingAuthorID: "bob"
+ )
+
+ mutator.checkCells(game.puzzle.cells.flatMap { $0 })
+ await updater.flush()
+
+ #expect(await capture.collector.allGameIDs.isEmpty)
+ #expect(try cellFromMoves(
+ persistence: persistence,
+ gameID: capture.gameID,
+ row: 0,
+ col: 0
+ ) == nil)
+ }
+
@Test("revealCells sets entry to solution and marks revealed")
func revealCellsSetsAnswer() throws {
let (game, mutator, _, _) = try makeTestGame()
diff --git a/Tests/Unit/PeerChangeLedgerTests.swift b/Tests/Unit/PeerChangeLedgerTests.swift
@@ -33,17 +33,25 @@ struct PeerChangeLedgerTests {
private let p2 = GridPosition(row: 0, col: 1)
private let p3 = GridPosition(row: 1, col: 0)
- @Test("A first build seeds every current cell at .distantPast")
+ @Test("A first build seeds every non-empty current cell at .distantPast")
func seedingRecordsDistantPast() {
let current = [
p1: prov("A", author: "bob", writer: "bob", at: t(10)),
p2: prov("B", author: "bob", writer: "bob", at: t(20)),
+ p3: prov("", author: nil, writer: "bob", at: t(30)),
]
let upserts = PeerChangeLedger.upserts(current: current, recorded: [:], seeding: true)
#expect(upserts.count == 2)
#expect(upserts.allSatisfy { $0.changedAt == .distantPast })
}
+ @Test("A first-seen empty cell records nothing")
+ func firstSeenEmptyCellIgnored() {
+ let current = [p1: prov("", author: nil, writer: "bob", at: t(40))]
+ let upserts = PeerChangeLedger.upserts(current: current, recorded: [:], seeding: false)
+ #expect(upserts.isEmpty)
+ }
+
@Test("After the seed, a genuine letter change records the move's real timestamp")
func letterChangeRecordsRealTimestamp() {
let recorded = [p1: PeerChange(position: p1, letter: "A", authorID: "bob", changedAt: .distantPast)]