ParticipantSummaries.swift (4341B)
1 import CloudKit 2 import Foundation 3 4 struct GameParticipantSummary: Identifiable, Equatable { 5 let authorID: String 6 let name: String 7 let color: PlayerColor 8 let isLocal: Bool 9 /// This player's puzzle score, used to order the Game List strip. Zero 10 /// for contexts that don't supply scores (e.g. the friend roster). 11 let score: Int 12 var id: String { authorID } 13 } 14 15 enum ParticipantSummaries { 16 static func allParticipants( 17 gameID: UUID, 18 namesByAuthor: [String: String], 19 moveAuthorIDs: [String], 20 nicknamesByAuthor: [String: String], 21 localAuthorID: String?, 22 localName: String, 23 localColor: PlayerColor, 24 scoreByAuthorID: [String: Int] = [:], 25 additionalAuthorIDs: [String] = [] 26 ) -> [GameParticipantSummary] { 27 let remotes = remoteParticipants( 28 gameID: gameID, 29 namesByAuthor: namesByAuthor, 30 moveAuthorIDs: moveAuthorIDs, 31 nicknamesByAuthor: nicknamesByAuthor, 32 localAuthorID: localAuthorID, 33 localColor: localColor, 34 scoreByAuthorID: scoreByAuthorID, 35 additionalAuthorIDs: additionalAuthorIDs 36 ) 37 guard let localAuthorID, !localAuthorID.isEmpty else { return remotes } 38 return [ 39 GameParticipantSummary( 40 authorID: localAuthorID, 41 name: resolvedLocalName(localName), 42 color: localColor, 43 isLocal: true, 44 score: scoreByAuthorID[localAuthorID] ?? 0 45 ), 46 ] + remotes 47 } 48 49 static func remoteParticipants( 50 gameID: UUID, 51 namesByAuthor: [String: String], 52 moveAuthorIDs: [String], 53 nicknamesByAuthor: [String: String], 54 localAuthorID: String?, 55 localColor: PlayerColor, 56 scoreByAuthorID: [String: Int] = [:], 57 additionalAuthorIDs: [String] = [] 58 ) -> [GameParticipantSummary] { 59 var authorIDs = Set(namesByAuthor.keys) 60 authorIDs.formUnion(moveAuthorIDs) 61 authorIDs.formUnion(additionalAuthorIDs) 62 authorIDs.remove(CKCurrentUserDefaultName) 63 authorIDs.remove("") 64 if let localAuthorID, !localAuthorID.isEmpty { 65 authorIDs.remove(localAuthorID) 66 } 67 68 // Hand the collaborators a curated, perceptually-spaced colour set 69 // anchored on the local user's (stable) colour: walk them in 70 // sorted-authorID order so the assignment is stable, and let 71 // `assignedCompanions` pick one game-specific option from the table. 72 // The set is derived from `gameID`, so it is distinct within the game 73 // but deliberately differs from game to game — a colour reads as "who 74 // is in this grid", never a permanent badge for a person. 75 let sortedAuthorIDs = authorIDs.sorted() 76 let colors = PlayerColor.assignedCompanions( 77 forSortedAuthorIDs: sortedAuthorIDs, 78 inGame: gameID, 79 anchor: localColor 80 ) 81 var summaries: [GameParticipantSummary] = [] 82 for (authorID, color) in zip(sortedAuthorIDs, colors) { 83 let name = nicknamesByAuthor[authorID] 84 ?? namesByAuthor[authorID] 85 ?? "Waiting for player..." 86 summaries.append(GameParticipantSummary( 87 authorID: authorID, 88 name: name, 89 color: color, 90 isLocal: false, 91 score: scoreByAuthorID[authorID] ?? 0 92 )) 93 } 94 return summaries.sorted { 95 $0.name == $1.name ? $0.authorID < $1.authorID : $0.name < $1.name 96 } 97 } 98 99 private static func resolvedLocalName(_ name: String) -> String { 100 name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Player" : name 101 } 102 103 static func sortedByScore<T>( 104 _ values: [T], 105 score: (T) -> Int, 106 name: (T) -> String, 107 id: (T) -> String 108 ) -> [T] { 109 values.sorted { 110 let lhsScore = score($0) 111 let rhsScore = score($1) 112 if lhsScore != rhsScore { return lhsScore > rhsScore } 113 let lhsName = name($0) 114 let rhsName = name($1) 115 if lhsName != rhsName { return lhsName < rhsName } 116 return id($0) < id($1) 117 } 118 } 119 }