crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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 }