CellMark.swift (3562B)
1 import Foundation 2 3 /// Non-letter state attached to a single cell. The letter itself lives in 4 /// `Game.entries`; `CellMark` describes how that letter (or the absence of 5 /// one) should be interpreted and rendered. 6 /// 7 /// `.pen` / `.pencil` carry an optional check result that's orthogonal to 8 /// pen-vs-pencil styling: `nil` means the cell hasn't been checked, `.right` 9 /// means a check confirmed the entry, `.wrong` means a check flagged it. 10 /// `.revealed` is a distinct locked state. `.none` is the canonical "no 11 /// mark" value; we never store `.pen(checked: nil)` because it's 12 /// semantically equivalent to `.none`. `.pencil(checked: nil)` is a real 13 /// state (a tentative entry that hasn't been checked) and is preserved. 14 /// 15 /// All marks are shared state — they live on `Game` alongside `entries` so 16 /// every player in a collaborative session sees the same marks. 17 enum CellMark: Sendable, Equatable { 18 case none 19 case pen(checked: CheckResult?) 20 case pencil(checked: CheckResult?) 21 case revealed 22 23 var isRevealed: Bool { 24 if case .revealed = self { return true } 25 return false 26 } 27 28 var isPencil: Bool { 29 if case .pencil = self { return true } 30 return false 31 } 32 33 var checked: CheckResult? { 34 switch self { 35 case .pen(let result), .pencil(let result): 36 return result 37 case .none, .revealed: 38 return nil 39 } 40 } 41 42 var isCheckedRight: Bool { checked == .right } 43 var isCheckedWrong: Bool { checked == .wrong } 44 45 /// Drops a `.wrong` check while preserving everything else. Used when a 46 /// cell is resolved to a correct letter: a lingering wrong-mark on a 47 /// now-correct entry is contradictory, but the pen/pencil styling (and any 48 /// `.right` check) should survive. 49 var withoutWrongCheck: CellMark { 50 switch self { 51 case .pen(.wrong): return .pen(checked: nil) 52 case .pencil(.wrong): return .pencil(checked: nil) 53 case .none, .pen, .pencil, .revealed: return self 54 } 55 } 56 } 57 58 // MARK: - Persistent / wire encoding 59 60 extension CellMark { 61 /// Losslessly maps the eight legal states to one `Int16`. This is the 62 /// single encoding used everywhere a `CellMark` is persisted (Core Data) 63 /// or serialized (Moves wire, realtime edits) — there is no longer any 64 /// `(markKind, checkedRight, checkedWrong)` flattening in the domain. 65 var code: Int16 { 66 switch self { 67 case .none: return 0 68 case .pen(nil): return 1 69 case .pen(.right): return 2 70 case .pen(.wrong): return 3 71 case .pencil(nil): return 4 72 case .pencil(.right): return 5 73 case .pencil(.wrong): return 6 74 case .revealed: return 7 75 } 76 } 77 78 /// Inverse of `code`. Unknown codes decode to `.none`. 79 init(code: Int16) { 80 switch code { 81 case 1: self = .pen(checked: nil) 82 case 2: self = .pen(checked: .right) 83 case 3: self = .pen(checked: .wrong) 84 case 4: self = .pencil(checked: nil) 85 case 5: self = .pencil(checked: .right) 86 case 6: self = .pencil(checked: .wrong) 87 case 7: self = .revealed 88 default: self = .none 89 } 90 } 91 } 92 93 extension CellMark: Codable { 94 init(from decoder: Decoder) throws { 95 let container = try decoder.singleValueContainer() 96 self.init(code: try container.decode(Int16.self)) 97 } 98 99 func encode(to encoder: Encoder) throws { 100 var container = encoder.singleValueContainer() 101 try container.encode(code) 102 } 103 }