crossmate

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

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 }