crossmate

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

PuzzleGridStats.swift (4844B)


      1 import Foundation
      2 
      3 struct PuzzleGridStats {
      4     struct CellState {
      5         let entry: String
      6         let mark: CellMark
      7         let authorID: String?
      8 
      9         init(entry: String, mark: CellMark, authorID: String?) {
     10             self.entry = entry
     11             self.mark = mark
     12             self.authorID = authorID
     13         }
     14 
     15         init(_ state: JournalCellState) {
     16             self.entry = state.letter
     17             self.mark = state.mark
     18             self.authorID = state.cellAuthorID
     19         }
     20     }
     21 
     22     struct ParticipantCount: Identifiable {
     23         let authorID: String?
     24         let name: String
     25         let color: PlayerColor?
     26         let count: Int
     27 
     28         var id: String { authorID ?? "unattributed" }
     29     }
     30 
     31     let fillableCellCount: Int
     32     let filledCellCount: Int
     33     let revealedSquareCount: Int
     34     let countsByAuthorID: [String?: Int]
     35 
     36     init(
     37         puzzle: Puzzle,
     38         hasRemotePlayers: Bool,
     39         localAuthorID: String?,
     40         countsIncorrectEntries: Bool = true,
     41         stateAt: (Int, Int) -> CellState
     42     ) {
     43         var fillableCellCount = 0
     44         var filledCellCount = 0
     45         var revealedSquareCount = 0
     46         var countsByAuthorID: [String?: Int] = [:]
     47 
     48         for r in 0..<puzzle.height {
     49             for c in 0..<puzzle.width {
     50                 let cell = puzzle.cells[r][c]
     51                 guard !cell.isBlock else { continue }
     52                 fillableCellCount += 1
     53 
     54                 let state = stateAt(r, c)
     55                 if !state.entry.isEmpty {
     56                     filledCellCount += 1
     57                 }
     58                 if state.mark.isRevealed {
     59                     revealedSquareCount += 1
     60                     continue
     61                 }
     62                 guard !state.entry.isEmpty else { continue }
     63                 if !countsIncorrectEntries, cell.solution != nil, !cell.accepts(state.entry) {
     64                     continue
     65                 }
     66                 let authorID = Self.normalizedAuthorID(
     67                     state.authorID,
     68                     hasRemotePlayers: hasRemotePlayers,
     69                     localAuthorID: localAuthorID
     70                 )
     71                 countsByAuthorID[authorID, default: 0] += 1
     72             }
     73         }
     74 
     75         self.fillableCellCount = fillableCellCount
     76         self.filledCellCount = filledCellCount
     77         self.revealedSquareCount = revealedSquareCount
     78         self.countsByAuthorID = countsByAuthorID
     79     }
     80 
     81     func participantCounts(
     82         entries: [PlayerRoster.Entry],
     83         localName: String,
     84         localColor: PlayerColor,
     85         includesUnattributedLocalWhenNoRemote: Bool = false
     86     ) -> [ParticipantCount] {
     87         let usesLocalFallback = entries.isEmpty
     88         let entryByAuthorID = Dictionary(
     89             entries.map { ($0.authorID, $0) },
     90             uniquingKeysWith: { first, _ in first }
     91         )
     92         let rosterAuthorIDs = Set(entries.map(\.authorID))
     93         let hasRemotePlayers = entries.contains { !$0.isLocal }
     94 
     95         let rosterCounts: [ParticipantCount]
     96         if usesLocalFallback {
     97             rosterCounts = [
     98                 ParticipantCount(
     99                     authorID: nil,
    100                     name: localName,
    101                     color: localColor,
    102                     count: countsByAuthorID[nil] ?? 0
    103                 )
    104             ]
    105         } else {
    106             rosterCounts = entries.map { entry in
    107                 ParticipantCount(
    108                     authorID: entry.authorID,
    109                     name: entry.name,
    110                     color: entry.color,
    111                     count: countsByAuthorID[entry.authorID] ?? 0
    112                 )
    113             }
    114         }
    115 
    116         let extraCounts = countsByAuthorID.compactMap { authorID, count -> ParticipantCount? in
    117             if let authorID, rosterAuthorIDs.contains(authorID) {
    118                 return nil
    119             }
    120             if authorID == nil && usesLocalFallback {
    121                 return nil
    122             }
    123             if let authorID, let entry = entryByAuthorID[authorID] {
    124                 return ParticipantCount(authorID: authorID, name: entry.name, color: entry.color, count: count)
    125             }
    126             if authorID == nil && includesUnattributedLocalWhenNoRemote && !hasRemotePlayers {
    127                 return ParticipantCount(authorID: nil, name: localName, color: localColor, count: count)
    128             }
    129             if authorID == nil {
    130                 return nil
    131             }
    132             return ParticipantCount(authorID: authorID, name: "Player", color: nil, count: count)
    133         }
    134 
    135         return rosterCounts + extraCounts
    136     }
    137 
    138     private static func normalizedAuthorID(
    139         _ authorID: String?,
    140         hasRemotePlayers: Bool,
    141         localAuthorID: String?
    142     ) -> String? {
    143         guard let authorID else {
    144             return hasRemotePlayers ? nil : localAuthorID
    145         }
    146         return authorID
    147     }
    148 }