crossmate

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

commit 984269e2e365158c7856f2e8a25ab3795103666d
parent 8a88a8d61c94b3539dfd10b2b52dc11dba2e580b
Author: Michael Camilleri <[email protected]>
Date:   Thu, 18 Jun 2026 10:56:50 +0900

Derive the Game List strip order from one participant list

This commit collapses GameSummary's two participant arrays into a single
source of truth. The full list and the strip-specific ordering held the
same players in different orders, so a pure score change altered neither
stored value and the row would not re-diff. GameSummary now keeps only
allParticipants, and stripParticipants becomes a computed ordering of
that list, so a score shift flows through the summary's equality.

Each player's score now travels on the participant itself. The summary
builders take a scoreByAuthorID and stamp it onto every
GameParticipantSummary, and the strip sorts by that stored score rather
than a side table threaded through a second pass. The friend roster,
which has no scores, keeps the defaulted empty map and reads zero.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Models/ParticipantSummaries.swift | 12++++++++++--
MCrossmate/Persistence/GameStore.swift | 39++++++++++++++++++---------------------
2 files changed, 28 insertions(+), 23 deletions(-)

diff --git a/Crossmate/Models/ParticipantSummaries.swift b/Crossmate/Models/ParticipantSummaries.swift @@ -6,6 +6,9 @@ struct GameParticipantSummary: Identifiable, Equatable { let name: String let color: PlayerColor let isLocal: Bool + /// This player's puzzle score, used to order the Game List strip. Zero + /// for contexts that don't supply scores (e.g. the friend roster). + let score: Int var id: String { authorID } } @@ -17,6 +20,7 @@ enum ParticipantSummaries { localAuthorID: String?, localName: String, localColor: PlayerColor, + scoreByAuthorID: [String: Int] = [:], additionalAuthorIDs: [String] = [] ) -> [GameParticipantSummary] { let remotes = remoteParticipants( @@ -25,6 +29,7 @@ enum ParticipantSummaries { nicknamesByAuthor: nicknamesByAuthor, localAuthorID: localAuthorID, localColor: localColor, + scoreByAuthorID: scoreByAuthorID, additionalAuthorIDs: additionalAuthorIDs ) guard let localAuthorID, !localAuthorID.isEmpty else { return remotes } @@ -33,7 +38,8 @@ enum ParticipantSummaries { authorID: localAuthorID, name: resolvedLocalName(localName), color: localColor, - isLocal: true + isLocal: true, + score: scoreByAuthorID[localAuthorID] ?? 0 ), ] + remotes } @@ -44,6 +50,7 @@ enum ParticipantSummaries { nicknamesByAuthor: [String: String], localAuthorID: String?, localColor: PlayerColor, + scoreByAuthorID: [String: Int] = [:], additionalAuthorIDs: [String] = [] ) -> [GameParticipantSummary] { var authorIDs = Set(namesByAuthor.keys) @@ -67,7 +74,8 @@ enum ParticipantSummaries { authorID: authorID, name: name, color: color, - isLocal: false + isLocal: false, + score: scoreByAuthorID[authorID] ?? 0 )) } return summaries.sorted { diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift @@ -33,7 +33,18 @@ struct GameSummary: Identifiable, Equatable { let isAccessRevoked: Bool let hasUnreadOtherMoves: Bool let allParticipants: [GameParticipantSummary] - let stripParticipants: [GameParticipantSummary] + + /// The participants ordered for the Game List strip: highest scorer at the + /// leading edge. Derived from `allParticipants`, whose summaries already + /// carry each player's score, so the ordering lives in one place. + var stripParticipants: [GameParticipantSummary] { + ParticipantSummaries.sortedByScore( + allParticipants, + score: \.score, + name: \.name, + id: \.authorID + ) + } init?( entity: GameEntity, @@ -133,15 +144,11 @@ struct GameSummary: Identifiable, Equatable { self.isOwned = entity.databaseScope == 0 self.isShared = entity.ckShareRecordName != nil || entity.databaseScope == 1 self.isAccessRevoked = entity.isAccessRevoked - let allParticipants = Self.computeParticipants( + self.allParticipants = Self.computeParticipants( entity: entity, localAuthorID: localAuthorID, localName: localName, - localColor: localColor - ) - self.allParticipants = allParticipants - self.stripParticipants = Self.stripParticipants( - allParticipants, + localColor: localColor, scoreByAuthorID: scoreByAuthorID ) self.hasUnreadOtherMoves = Self.computeHasUnread( @@ -171,7 +178,8 @@ struct GameSummary: Identifiable, Equatable { entity: GameEntity, localAuthorID: String?, localName: String, - localColor: PlayerColor + localColor: PlayerColor, + scoreByAuthorID: [String: Int] ) -> [GameParticipantSummary] { guard entity.ckShareRecordName != nil || entity.databaseScope == 1 else { return [] @@ -201,19 +209,8 @@ struct GameSummary: Identifiable, Equatable { nicknamesByAuthor: nicknames, localAuthorID: localAuthorID, localName: localName, - localColor: localColor - ) - } - - private static func stripParticipants( - _ participants: [GameParticipantSummary], - scoreByAuthorID: [String: Int] - ) -> [GameParticipantSummary] { - ParticipantSummaries.sortedByScore( - participants, - score: { scoreByAuthorID[$0.authorID] ?? 0 }, - name: \.name, - id: \.authorID + localColor: localColor, + scoreByAuthorID: scoreByAuthorID ) }