GridStateMerger.swift (2203B)
1 import Foundation 2 3 /// Reduces every `(author, device)` `MovesValue` for a single game into one 4 /// `GridState`. Per-cell last-writer-wins on wall-clock `updatedAt`; ties are 5 /// broken first by the writing user's `authorID` (lex-min wins), then by 6 /// `deviceID`, so the output is deterministic regardless of input order. The 7 /// merged `GridCell.authorID` is the *cell-level* preserved author from the 8 /// winning entry — not the parent record's author — so reveal-of-correct and 9 /// same-letter rewrites can hand off authorship without losing it. 10 enum GridStateMerger { 11 12 static func merge(_ moves: [MovesValue]) -> GridState { 13 var winners: [GridPosition: Winner] = [:] 14 for view in moves { 15 for (position, cell) in view.cells { 16 let candidate = Winner( 17 cell: cell, 18 writerAuthorID: view.authorID, 19 deviceID: view.deviceID 20 ) 21 if let current = winners[position] { 22 if shouldReplace(current: current, with: candidate) { 23 winners[position] = candidate 24 } 25 } else { 26 winners[position] = candidate 27 } 28 } 29 } 30 31 var grid: GridState = [:] 32 for (position, winner) in winners { 33 grid[position] = GridCell( 34 letter: winner.cell.letter, 35 markKind: winner.cell.markKind, 36 checkedWrong: winner.cell.checkedWrong, 37 authorID: winner.cell.authorID 38 ) 39 } 40 return grid 41 } 42 43 private struct Winner { 44 var cell: TimestampedCell 45 var writerAuthorID: String 46 var deviceID: String 47 } 48 49 private static func shouldReplace(current: Winner, with candidate: Winner) -> Bool { 50 if candidate.cell.updatedAt != current.cell.updatedAt { 51 return candidate.cell.updatedAt > current.cell.updatedAt 52 } 53 if candidate.writerAuthorID != current.writerAuthorID { 54 return candidate.writerAuthorID < current.writerAuthorID 55 } 56 return candidate.deviceID < current.deviceID 57 } 58 }