Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
compute ratingDiffs and performances per FideTC.
and display it in the UI. For scoreGroups with different TC sections
  • Loading branch information
allanjoseph98 committed Jan 29, 2026
commit 99fe198c652ca51c16dd79f96ebe8c245d8fb2ea
72 changes: 53 additions & 19 deletions modules/relay/src/main/RelayPlayer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ import lila.relay.RelayGroup.ScoreGroup
// Player in a tournament with current performance rating and list of games
case class RelayPlayer(
player: StudyPlayer.WithFed,
ratingsMap: Map[FideTC, IntRating],
score: Option[Float],
ratingDiff: Option[IntRatingDiff],
performance: Option[IntRating],
ratingDiffs: Option[Map[FideTC, IntRatingDiff]],
performances: Option[Map[FideTC, IntRating]],
tiebreaks: Option[Seq[(Tiebreak, TiebreakPoint)]],
rank: Option[RelayPlayer.Rank],
games: Vector[RelayPlayer.Game]
):
export player.player.*
def withGame(game: RelayPlayer.Game) = copy(games = games :+ game)
def withGame(game: RelayPlayer.Game, player: StudyPlayer.WithFed) =
copy(
games = games :+ game,
ratingsMap = player.rating
.ifFalse(ratingsMap.contains(game.fideTC))
.fold(ratingsMap)(r => ratingsMap + (game.fideTC -> r))
)
def eloGames: Vector[Elo.Game] = games.flatMap(_.eloGame)
def toTieBreakPlayer: Option[Tiebreak.Player] = player.id.map: id =>
Tiebreak.Player(id = id.toString, rating = player.rating.map(_.into(Elo)))
Expand Down Expand Up @@ -76,7 +83,7 @@ object RelayPlayer:
type RelayPlayers = SeqMap[StudyPlayer.Id, RelayPlayer]

def empty(player: StudyPlayer.WithFed) =
RelayPlayer(player, None, None, None, None, None, Vector.empty)
RelayPlayer(player, Map.empty, None, None, None, None, None, Vector.empty)

case class Game(
round: RelayRoundId,
Expand All @@ -85,6 +92,7 @@ object RelayPlayer:
color: Color,
points: Option[Outcome.GamePoints],
rated: chess.Rated,
fideTC: FideTC,
customScoring: Option[ByColor[RelayRound.CustomScoring]] = None,
unplayed: Boolean
):
Expand Down Expand Up @@ -122,6 +130,11 @@ object RelayPlayer:
given Writes[Outcome] = Json.writes
given Writes[Outcome.Points] = writeAs(_.show)
given Writes[Outcome.GamePoints] = writeAs(points => Outcome.showPoints(points.some))
given Writes[FideTC] = writeAs(_.toString())
given ratingsMapWrites: OWrites[Map[FideTC, Int]] = OWrites: m =>
JsObject:
m.map: (tc, rating) =>
(tc.toString, Json.toJson(rating))
given Writes[RelayPlayer.Game] = Json.writes
given Writes[Seq[(Tiebreak, TiebreakPoint)]] = Writes: tbs =>
Json.toJson:
Expand All @@ -135,10 +148,13 @@ object RelayPlayer:
Json.toJsObject(p.player) ++ Json
.obj("played" -> p.games.count(_.points.isDefined))
.add("score" -> p.score)
.add("ratingDiff" -> p.ratingDiff)
.add("performance" -> p.performance)
.add("ratingDiff" -> p.ratingDiffs.map(_.head._2)) // API BC grace
.add("ratingDiffs" -> p.ratingDiffs.map(_.view.mapValues(_.value).toMap))
.add("performance" -> p.performances.flatMap(_.headOption).map(_._2)) // API BC grace
.add("performances" -> p.performances.map(_.view.mapValues(_.value).toMap))
.add("tiebreaks" -> p.tiebreaks)
.add("rank" -> p.rank)
.add("rank" -> p.rank) ++
Json.obj("ratingsMap" -> p.ratingsMap.view.mapValues(_.value).toMap)
def full(
tour: RelayTour
)(p: RelayPlayer, fidePlayer: Option[FidePlayer], user: Option[User], follow: Option[Boolean]): JsObject =
Expand All @@ -157,7 +173,8 @@ object RelayPlayer:
"round" -> g.round,
"id" -> g.id,
"opponent" -> g.opponent,
"color" -> g.color
"color" -> g.color,
"fideTC" -> g.fideTC
)
.add("points" -> g.playerPoints)
.add("customPoints" -> g.customPlayerPoints)
Expand Down Expand Up @@ -233,7 +250,7 @@ private final class RelayPlayerApi(
players <- readGamesAndPlayers(sg.toList)
withScore = if tour.showScores then computeScores(players) else players
withRatingDiff <-
if tour.showRatingDiffs then computeRatingDiffs(tour.info.fideTcOrGuess, withScore)
if tour.showRatingDiffs then computeRatingDiffs(withScore)
else fuccess(withScore)
withTiebreaks <- tour.tiebreaks.fold(fuccess(withRatingDiff)): tiebreaks =>
roundRepo
Expand All @@ -251,6 +268,7 @@ private final class RelayPlayerApi(
private def readGamesAndPlayers(tourIds: List[RelayTourId]): Fu[RelayPlayers] =
for
tours <- tourRepo.byIds(tourIds)
toursById = tours.mapBy(_.id)
rounds <-
if sgIsParallel(tours) then roundRepo.byToursOrdered(tourIds)
else tourIds.flatTraverse(roundRepo.byTourOrdered)
Expand Down Expand Up @@ -283,26 +301,34 @@ private final class RelayPlayerApi(
color,
tags.points,
round.rated,
toursById.get(round.tourId).flatMap(_.info.fideTc).getOrElse(FideTC.standard),
round.customScoring,
unplayed = tags.value.contains(RelayGame.unplayedTag)
)
playersAcc.updated(
playerId,
playersAcc
.getOrElse(playerId, RelayPlayer.empty(player))
.withGame(game)
.withGame(game, player)
)

private def computeScores(players: RelayPlayers): RelayPlayers =
players.view
.mapValues: p =>
p.copy(
score = p.games.foldMap(_.playerScore),
performance = Elo.computePerformanceRating(p.eloGames).map(_.into(IntRating))
performances = p.games
.groupBy(_.fideTC)
.foldLeft(Map.empty[FideTC, IntRating]): (acc, entry) =>
val (gameTC, tcGames) = entry
val performanceRating =
Elo.computePerformanceRating(tcGames.flatMap(_.eloGame))
performanceRating.fold(acc)(r => acc + (gameTC -> r.into(IntRating)))
.some
)
.to(SeqMap)

private def computeRatingDiffs(tc: FideTC, players: RelayPlayers): Fu[RelayPlayers] =
private def computeRatingDiffs(players: RelayPlayers): Fu[RelayPlayers] =
players.toList
.traverse: (id, player) =>
val eloGames = player.eloGames
Expand All @@ -311,13 +337,21 @@ private final class RelayPlayerApi(
player.fideId
.so(fidePlayerGet)
.map: fidePlayerOpt =>
for
fidePlayer <- fidePlayerOpt
r <- player.rating.map(_.into(Elo)).orElse(fidePlayer.ratingOf(tc))
p = Elo.Player(r, fidePlayer.kFactorOf(tc))
yield player.copy(ratingDiff = Elo.computeRatingDiff(tc)(p, eloGames).some)
.map: newPlayer =>
id -> (newPlayer | player)
val newPlayer = fidePlayerOpt.fold(player): fidePlayer =>
val newRatingDiffs = player.games
.groupBy(_.fideTC)
.foldLeft(Map.empty[FideTC, IntRatingDiff]): (diffs, entry) =>
val (gameTC, tcGames) = entry
val r = player.ratingsMap
.get(gameTC)
.map(_.into(Elo))
.orElse(fidePlayer.ratingOf(gameTC))
r.fold(diffs): rating =>
val p = Elo.Player(rating, fidePlayer.kFactorOf(gameTC))
val newDiff = Elo.computeRatingDiff(gameTC)(p, tcGames.flatMap(_.eloGame))
diffs + (gameTC -> newDiff)
player.copy(ratingDiffs = newRatingDiffs.some)
id -> newPlayer
.map(_.to(SeqMap))

private def computeTiebreaks(
Expand Down
12 changes: 11 additions & 1 deletion modules/relay/src/main/RelayTeams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,17 @@ final class RelayTeamTable(
players <- chap.players.map(_.map(_.studyPlayer))
teams <- players.traverse(_.team).map(_.toPair).map(Pair.apply)
game = players.mapWithColor: (c, p) =>
RelayPlayer.Game(round.id, chap.id, p, c, points, round.rated, round.customScoring, false)
RelayPlayer.Game(
round.id,
chap.id,
p,
c,
points,
round.rated,
chess.FideTC.standard,
round.customScoring,
false
)
m0 = table.find(_.is(teams)) | TeamMatch(
round.id,
teams.map(TeamWithGames(_, SeqMap.empty)),
Expand Down
6 changes: 4 additions & 2 deletions ui/analyse/src/study/relay/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ export interface RelayRound {
customScoring?: CustomScoring;
}

export type FideTC = 'standard' | 'rapid' | 'blitz';

export interface RelayTourInfo {
format?: string;
tc?: string;
fideTc?: string;
fideTc?: FideTC;
location?: string;
players?: string;
website?: string;
Expand All @@ -84,7 +86,7 @@ export interface RelayTour {
showTeamScores?: boolean;
tier?: number;
dates?: RelayTourDates;
tc?: 'standard' | 'rapid' | 'blitz';
tc?: FideTC;
communityOwner?: LightUser;
}

Expand Down
72 changes: 52 additions & 20 deletions ui/analyse/src/study/relay/relayPlayers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type VNode, dataIcon, hl, onInsert, type MaybeVNodes, spinnerVdom as spinner } from 'lib/view';
import { json as xhrJson } from 'lib/xhr';
import * as licon from 'lib/licon';
import type { Photo, RelayRound, RelayTeamName, RelayTour, RoundId } from './interfaces';
import type { FideTC, Photo, RelayRound, RelayTeamName, RelayTour, RoundId } from './interfaces';
import { playerColoredResult } from './customScoreStatus';
import { playerFedFlag } from '../playerBars';
import { userLink, userTitle } from 'lib/view/userLink';
Expand All @@ -20,6 +20,7 @@ import { convertPlayerFromServer } from '../studyChapters';
import { isTouchDevice } from 'lib/device';
import { pubsub } from 'lib/pubsub';
import { teamLinkData } from './relayTeamLeaderboard';
import perfIcons from 'lib/game/perfIcons';

export type RelayPlayerId = FideId | string;

Expand All @@ -32,8 +33,9 @@ interface Tiebreak {
export interface RelayPlayer extends StudyPlayer {
score?: number;
played?: number;
ratingDiff?: number;
performance?: number;
ratingsMap?: { [tc in FideTC]?: number };
ratingDiffs?: { [tc in FideTC]?: number };
performances?: { [tc in FideTC]?: number };
tiebreaks?: Tiebreak[];
rank?: number;
}
Expand All @@ -44,6 +46,7 @@ interface RelayPlayerGame {
roundObj?: RelayRound;
opponent: RelayPlayer;
color: Color;
fideTC: FideTC;
points?: PointsStr;
customPoints?: number;
ratingDiff?: number;
Expand Down Expand Up @@ -136,12 +139,11 @@ export default class RelayPlayers {
export const playersView = (ctrl: RelayPlayers): VNode =>
ctrl.show ? playerView(ctrl, ctrl.show) : playersList(ctrl);

const ratingCategs = [
['standard', i18n.site.classical],
['rapid', i18n.site.rapid],
['blitz', i18n.site.blitz],
];

const ratingCategs: { [key in FideTC]: string } = {
standard: i18n.site.classical,
rapid: i18n.site.rapid,
blitz: i18n.site.blitz,
};
const playerView = (ctrl: RelayPlayers, show: PlayerToShow): VNode => {
const tour = ctrl.tour;
const p = show.player;
Expand Down Expand Up @@ -215,7 +217,7 @@ const playerView = (ctrl: RelayPlayers, show: PlayerToShow): VNode => {
),
hl('div.fide-player__cards', [
p.fide?.ratings &&
ratingCategs.map(([key, name]) =>
Object.entries(ratingCategs).map(([key, name]: [FideTC, string]) =>
hl(`div.fide-player__card${key === tc ? '.active' : ''}`, [
hl('em', name),
hl('span', [p.fide?.ratings[key] || '-']),
Expand All @@ -226,12 +228,18 @@ const playerView = (ctrl: RelayPlayers, show: PlayerToShow): VNode => {
hl('em', i18n.broadcast.score),
hl('span', [p.score, ' / ', p.played]),
]),
!!p.performance &&
p.performances &&
hl('div.fide-player__card', [
hl('em', i18n.site.performance),
hl('span', [p.performance, p.games.length < 4 ? '?' : '']),
Object.entries(p.performances).map(([tc, value]: [FideTC, number]) =>
hl(
'div',
fideTCAttrs(tc),
`${value}${p.games.filter(g => g.fideTC === tc).length < 4 ? '?' : ''}`,
),
),
]),
p.ratingDiff !== undefined &&
p.ratingDiffs &&
hl('div.fide-player__card', [hl('em', i18n.broadcast.ratingDiff), ratingDiff(p)]),
]),
hl('table.relay-tour__player__games.slist.slist-pad', [
Expand Down Expand Up @@ -310,7 +318,7 @@ export const renderPlayers = (
hl(
'td',
sortByBoth(player.rating, (player.score || 0) * 10),
!!player.rating && [`${player.rating}`, ratingDiff(player)],
player.rating && ratingDiff(player),
),
withScores &&
hl(
Expand Down Expand Up @@ -405,7 +413,7 @@ const renderPlayerTipHead = (ctrl: RelayPlayers, p: StudyPlayer | RelayPlayer):
p.team && hl('a.tpp__player__team', matchOrResultsTeamLink(ctrl, p.team), p.team),
hl('div', [
playerFedFlag(p.fed),
!!p.rating && [`${p.rating}`, isRelayPlayer(p) && !ctrl.hideResultsSinceRoundId() && ratingDiff(p)],
!!p.rating && isRelayPlayer(p) && !ctrl.hideResultsSinceRoundId() && ratingDiff(p),
]),
isRelayPlayer(p) &&
!ctrl.hideResultsSinceRoundId() &&
Expand Down Expand Up @@ -456,7 +464,12 @@ const renderPlayerGames = (ctrl: RelayPlayers, p: RelayPlayerWithGames, withTips
hl('td', op.rating?.toString()),
hl('td.is.color-icon.' + game.color),
hl('td.tpp__games__status', points !== undefined ? coloredPoint(points) : '*'),
hl('td', defined(game.ratingDiff) && hideResultsSinceIndex > i ? ratingDiff(game) : undefined),
hl(
'td',
defined(game.ratingDiff) &&
hideResultsSinceIndex > i &&
ratingDiff(game, p.ratingsMap && Object.keys(p.ratingsMap).length > 1),
),
]);
}),
);
Expand Down Expand Up @@ -503,16 +516,35 @@ const playerTd = (player: RelayPlayer, ctrl: RelayPlayers, withTips: boolean): V
);
};

const ratingDiff = (p: RelayPlayer | RelayPlayerGame) => {
const rd = p.ratingDiff;
return !defined(rd)
const ratingDiff = (p: RelayPlayer | RelayPlayerGame, showIcons: boolean = true) =>
isRelayPlayerGame(p)
? hl('div', showIcons ? fideTCAttrs(p.fideTC) : {}, diffNode(p.ratingDiff))
: p.ratingDiffs &&
Object.entries(p.ratingDiffs).map(([tc, diff]: [FideTC, number]) =>
hl('div', p.ratingsMap && Object.keys(p.ratingsMap).length > 1 ? fideTCAttrs(tc) : {}, [
p.ratingsMap?.[tc],
diffNode(diff),
]),
);

const diffNode = (rd: number | undefined) =>
!defined(rd)
? undefined
: rd > 0
? hl('good.rp', '+' + rd)
: rd < 0
? hl('bad.rp', '−' + -rd)
: hl('span.rp--same', ' ==');
};

const isRelayPlayerGame = (p: RelayPlayer | RelayPlayerGame): p is RelayPlayerGame =>
'round' in p && 'opponent' in p;

const fideTCAttrs = (tc: FideTC): VNodeData => ({
attrs: {
'data-icon': perfIcons[tc === 'standard' ? 'classical' : tc],
title: ratingCategs[tc],
},
});

export const tableAugment = (el: HTMLTableElement) => {
extendTablesortNumber();
Expand Down