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 }