crossmate

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

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 }