Moves.swift (3718B)
1 import Foundation 2 3 /// A row/column coordinate inside a puzzle grid. Also used as the dictionary 4 /// key for `GridState` and `MovesValue.cells`. 5 struct GridPosition: Hashable, Sendable, Codable { 6 let row: Int 7 let col: Int 8 } 9 10 /// Per-cell state in the merged grid — the result of `GridStateMerger.merge`. 11 /// `authorID` is the *preserved* cell author from the winning entry. 12 struct GridCell: Equatable, Sendable, Codable { 13 var letter: String 14 var markKind: Int16 15 var checkedWrong: Bool 16 var authorID: String? 17 } 18 19 /// The merged grid for a single game: only cells that have ever been touched 20 /// appear; untouched cells are absent rather than carrying empty values. 21 typealias GridState = [GridPosition: GridCell] 22 23 /// One device's contribution to a game: every cell this `(authorID, deviceID)` 24 /// pair has ever touched, with the wall-clock timestamp of each touch. Merging 25 /// across all `MovesValue`s for a game reconstructs the current grid via per-cell 26 /// last-writer-wins on `TimestampedCell.updatedAt`. 27 struct MovesValue: Equatable, Sendable { 28 let gameID: UUID 29 let authorID: String 30 let deviceID: String 31 var cells: [GridPosition: TimestampedCell] 32 var updatedAt: Date 33 } 34 35 /// A single cell touch within a `MovesValue`. The cell-level `authorID` is the 36 /// *preserved* author for the square — it can differ from the parent record's 37 /// `authorID` (which is the iCloud user who wrote this row) when a reveal-of- 38 /// correct or a same-letter rewrite preserves the original author of the 39 /// letter. The merger uses cell-level `authorID` when populating `GridCell`. 40 struct TimestampedCell: Equatable, Sendable { 41 var letter: String 42 var markKind: Int16 43 var checkedWrong: Bool 44 var updatedAt: Date 45 var authorID: String? 46 } 47 48 enum MovesCodec { 49 /// Wire format for `MovesValue.cells`. Each entry's `authorID` is the 50 /// preserved cell-level author — distinct from the parent record's 51 /// authorID, which identifies the iCloud user who wrote the record. 52 struct Payload: Codable, Equatable { 53 struct Entry: Codable, Equatable { 54 let row: Int 55 let col: Int 56 let letter: String 57 let markKind: Int16 58 let checkedWrong: Bool 59 let updatedAt: Date 60 let authorID: String? 61 } 62 let entries: [Entry] 63 } 64 65 static func encode(_ cells: [GridPosition: TimestampedCell]) throws -> Data { 66 let entries = cells 67 .map { position, cell in 68 Payload.Entry( 69 row: position.row, 70 col: position.col, 71 letter: cell.letter, 72 markKind: cell.markKind, 73 checkedWrong: cell.checkedWrong, 74 updatedAt: cell.updatedAt, 75 authorID: cell.authorID 76 ) 77 } 78 .sorted { lhs, rhs in 79 lhs.row != rhs.row ? lhs.row < rhs.row : lhs.col < rhs.col 80 } 81 return try JSONEncoder().encode(Payload(entries: entries)) 82 } 83 84 static func decode(_ data: Data) throws -> [GridPosition: TimestampedCell] { 85 let payload = try JSONDecoder().decode(Payload.self, from: data) 86 var cells: [GridPosition: TimestampedCell] = [:] 87 for entry in payload.entries { 88 let position = GridPosition(row: entry.row, col: entry.col) 89 cells[position] = TimestampedCell( 90 letter: entry.letter, 91 markKind: entry.markKind, 92 checkedWrong: entry.checkedWrong, 93 updatedAt: entry.updatedAt, 94 authorID: entry.authorID 95 ) 96 } 97 return cells 98 } 99 }