Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
84 changes: 79 additions & 5 deletions lib/src/model/broadcast/broadcast.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/styles/lichess_icons.dart';
import 'package:lichess_mobile/src/styles/styles.dart';
import 'package:lichess_mobile/src/utils/l10n_context.dart';

part 'broadcast.freezed.dart';

Expand Down Expand Up @@ -52,6 +57,12 @@ enum BroadcastResult {
noResultPgnTag => false,
_ => true,
};

Color? colorFor(Side side, BuildContext context) => switch (this) {
whiteWins => side == Side.white ? context.lichessColors.good : context.lichessColors.error,
blackWins => side == Side.white ? context.lichessColors.error : context.lichessColors.good,
_ => null,
};
}

@freezed
Expand Down Expand Up @@ -119,6 +130,33 @@ typedef BroadcastTournamentGroup = ({
bool live,
});

typedef BroadcastCustomPointsPerColor = ({double win, double draw});
typedef BroadcastCustomScoring = BySide<BroadcastCustomPointsPerColor>;

extension BroadcastCustomScoringExt on BroadcastCustomScoring {
String pointsForResult(Side side, BroadcastResult result) {
final customScore =
(side == Side.white
? switch (result) {
BroadcastResult.whiteWins => this[side]?.win,
BroadcastResult.draw || BroadcastResult.whiteHalfWins => this[side]?.draw,
_ => 0.0,
}
: switch (result) {
BroadcastResult.blackWins => this[side]?.win,
BroadcastResult.draw || BroadcastResult.blackHalfWins => this[side]?.draw,
_ => 0.0,
}) ??
0.0;
return customScore == 0.5
? result.resultToString(side) // '½' looks nicer than '0.5'
: NumberFormat('0.##').format(customScore);
}
}

String resultString(BroadcastCustomScoring? customScoring, Side side, BroadcastResult result) =>
customScoring?.pointsForResult(side, result) ?? result.resultToString(side);

@freezed
sealed class BroadcastRound with _$BroadcastRound {
const factory BroadcastRound({
Expand All @@ -129,6 +167,7 @@ sealed class BroadcastRound with _$BroadcastRound {
required DateTime? startsAt,
required DateTime? finishedAt,
required bool startsAfterPrevious,
required BroadcastCustomScoring? customScoring,
}) = _BroadcastRound;
}

Expand All @@ -153,7 +192,7 @@ sealed class BroadcastGame with _$BroadcastGame {

const factory BroadcastGame({
required BroadcastGameId id,
required IMap<Side, BroadcastPlayerWithClock> players,
required BySide<BroadcastPlayerWithClock> players,
required String fen,
required Move? lastMove,
required Duration? thinkTime,
Expand Down Expand Up @@ -221,32 +260,67 @@ sealed class BroadcastPlayerWithOverallResult with _$BroadcastPlayerWithOverallR
required int played,
required double? score,
required int? rank,
required int? ratingDiff,
required int? performance,
required StatByFideTC? ratingsMap,
required StatByFideTC? ratingDiffs,
required StatByFideTC? performances,
required IList<BroadcastTieBreakDetail>? tieBreaks,
required String? team,
}) = _BroadcastPlayerWithOverallResult;
}

typedef BroadcastTieBreakDetail = ({String extendedCode, String description, double points});

typedef BroadcastFideData = ({({int? standard, int? rapid, int? blitz}) ratings, int? birthYear});
typedef StatByFideTC = IMap<BroadcastFideTC, int>;

enum BroadcastFideTC {
standard,
rapid,
blitz;

IconData get icon => switch (this) {
BroadcastFideTC.standard => LichessIcons.classical,
BroadcastFideTC.rapid => LichessIcons.rapid,
BroadcastFideTC.blitz => LichessIcons.blitz,
};

String i18nName(BuildContext context) => switch (this) {
BroadcastFideTC.standard => context.l10n.classical,
BroadcastFideTC.rapid => context.l10n.rapid,
BroadcastFideTC.blitz => context.l10n.blitz,
};
}

typedef BroadcastFideData = ({StatByFideTC ratings, int? birthYear});

typedef BroadcastPlayerWithGameResults = ({
BroadcastPlayerWithOverallResult playerWithOverallResult,
BroadcastFideData fideData,
IList<BroadcastPlayerGameResult> games,
});

enum BroadcastPoints { one, half, zero }
enum BroadcastPoints {
one,
half,
zero;

BroadcastResult resultFor(Side side) => switch (this) {
BroadcastPoints.one =>
side == Side.white ? BroadcastResult.whiteWins : BroadcastResult.blackWins,
BroadcastPoints.zero =>
side == Side.white ? BroadcastResult.blackWins : BroadcastResult.whiteWins,
BroadcastPoints.half => BroadcastResult.draw,
};
}

@freezed
sealed class BroadcastPlayerGameResult with _$BroadcastPlayerGameResult {
const factory BroadcastPlayerGameResult({
required BroadcastRoundId roundId,
required BroadcastGameId gameId,
required Side color,
required BroadcastFideTC fideTC,
required BroadcastPoints? points,
required double? customPoints,
required int? ratingDiff,
required BroadcastPlayer opponent,
}) = _BroadcastPlayerGameResult;
Expand Down
47 changes: 40 additions & 7 deletions lib/src/model/broadcast/broadcast_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ BroadcastTournamentGroup _tournamentGroupFromPick(RequiredPick pick) {
return (id: id, name: name, active: active, live: live);
}

BroadcastCustomPointsPerColor _customPointsPerColorFromPick(RequiredPick pick) =>
(win: pick('win').asDoubleOrThrow(), draw: pick('draw').asDoubleOrThrow());

BroadcastCustomScoring _customScoringFromPick(RequiredPick pick) => {
Side.white: _customPointsPerColorFromPick(pick('white').required()),
Side.black: _customPointsPerColorFromPick(pick('black').required()),
}.lock;

BroadcastRound _roundFromPick(RequiredPick pick) {
final live = pick('ongoing').asBoolOrFalse();
final finished = pick('finished').asBoolOrFalse();
Expand All @@ -218,6 +226,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) {
startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(),
finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(),
startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(),
customScoring: pick('customScoring').letOrNull(_customScoringFromPick),
);
}

Expand Down Expand Up @@ -326,8 +335,9 @@ BroadcastPlayerWithOverallResult _playerWithOverallResultFromPick(RequiredPick p
played: pick('played').asIntOrThrow(),
score: pick('score').asDoubleOrNull(),
rank: pick('rank').asIntOrNull(),
ratingDiff: pick('ratingDiff').asIntOrNull(),
performance: pick('performance').asIntOrNull(),
ratingsMap: pick('ratingsMap').letOrNull(pickStats),
ratingDiffs: pick('ratingDiffs').letOrNull(pickStats),
performances: pick('performances').letOrNull(pickStats),
tieBreaks: pick('tiebreaks').asListOrNull(_tieBreakDetailFromPick)?.toIList(),
team: pick('team').asStringOrNull(),
);
Expand All @@ -351,15 +361,36 @@ BroadcastPlayerWithGameResults _makePlayerWithGameResultsFromJson(Map<String, dy

BroadcastFideData _fideDataFromPick(Pick pick) {
return (
ratings: (
standard: pick('ratings', 'standard').asIntOrNull(),
rapid: pick('ratings', 'rapid').asIntOrNull(),
blitz: pick('ratings', 'blitz').asIntOrNull(),
),
ratings: pick('ratings').letOrNull(pickStats) ?? const IMap.empty(),
birthYear: pick('year').asIntOrNull(),
);
}

BroadcastFideTC _fideTCFromString(RequiredPick pick) {
final tc = pick.asStringOrNull();
switch (tc) {
case 'standard':
return BroadcastFideTC.standard;
case 'rapid':
return BroadcastFideTC.rapid;
case 'blitz':
return BroadcastFideTC.blitz;
default:
throw PickException('Unknown FIDE time control: $tc');
}
}

StatByFideTC pickStats(RequiredPick pick) {
final standard = pick('standard').asIntOrNull();
final rapid = pick('rapid').asIntOrNull();
final blitz = pick('blitz').asIntOrNull();
return {
if (standard != null) BroadcastFideTC.standard: standard,
if (rapid != null) BroadcastFideTC.rapid: rapid,
if (blitz != null) BroadcastFideTC.blitz: blitz,
}.lock;
}

BroadcastPlayerGameResult _playerGameResultFromPick(RequiredPick pick) {
final pointsString = pick('points').asStringOrNull();
BroadcastPoints? points;
Expand All @@ -375,8 +406,10 @@ BroadcastPlayerGameResult _playerGameResultFromPick(RequiredPick pick) {
roundId: pick('round').asBroadcastRoundIdOrThrow(),
gameId: pick('id').asBroadcastGameIdOrThrow(),
color: pick('color').asSideOrThrow(),
fideTC: _fideTCFromString(pick('fideTC').required()),
ratingDiff: pick('ratingDiff').asIntOrNull(),
points: points,
customPoints: pick('customPoints').asDoubleOrNull(),
opponent: _playerFromPick(pick('opponent').required()),
);
}
Expand Down
20 changes: 17 additions & 3 deletions lib/src/view/broadcast/broadcast_boards_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class BroadcastBoardsTab extends ConsumerWidget {
title: value.round.name,
tournamentSlug: tournamentSlug,
roundSlug: value.round.slug,
customScoring: value.round.customScoring,
),
AsyncError(:final error) => Center(
child: Center(child: Text('Could not load broadcast: $error')),
Expand All @@ -83,6 +84,7 @@ class BroadcastPreview extends ConsumerStatefulWidget {
required this.title,
required this.tournamentSlug,
required this.roundSlug,
required this.customScoring,
});

// A circular progress indicator is used instead of shimmers currently
Expand All @@ -92,14 +94,16 @@ class BroadcastPreview extends ConsumerStatefulWidget {
games = null,
title = '',
tournamentSlug = '',
roundSlug = '';
roundSlug = '',
customScoring = null;

final BroadcastTournamentId tournamentId;
final BroadcastRoundId roundId;
final IList<BroadcastGame>? games;
final String title;
final String tournamentSlug;
final String roundSlug;
final BroadcastCustomScoring? customScoring;
@override
ConsumerState<BroadcastPreview> createState() => _BroadcastPreviewState();
}
Expand Down Expand Up @@ -210,6 +214,7 @@ class _BroadcastPreviewState extends ConsumerState<BroadcastPreview> {
boardSize: boardSize,
boardWithMaybeEvalBarWidth: boardWithMaybeEvalBarWidth,
playingSide: playingSide,
customScoring: widget.customScoring,
);
}, childCount: games == null ? numberLoadingBoards : games.length),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
Expand Down Expand Up @@ -238,6 +243,7 @@ class ObservedBoardThumbnail extends ConsumerStatefulWidget {
required this.boardSize,
required this.boardWithMaybeEvalBarWidth,
required this.playingSide,
required this.customScoring,
});

final BroadcastRoundId roundId;
Expand All @@ -250,6 +256,7 @@ class ObservedBoardThumbnail extends ConsumerStatefulWidget {
final double boardSize;
final double boardWithMaybeEvalBarWidth;
final Side playingSide;
final BroadcastCustomScoring? customScoring;

@override
ConsumerState<ObservedBoardThumbnail> createState() => _ObservedBoardThumbnailState();
Expand Down Expand Up @@ -311,12 +318,14 @@ class _ObservedBoardThumbnailState extends ConsumerState<ObservedBoardThumbnail>
game: widget.game,
side: Side.black,
playingSide: widget.playingSide,
customScoring: widget.customScoring,
),
footer: _PlayerWidget(
width: widget.boardWithMaybeEvalBarWidth,
game: widget.game,
side: Side.white,
playingSide: widget.playingSide,
customScoring: widget.customScoring,
),
),
);
Expand Down Expand Up @@ -349,12 +358,14 @@ class _PlayerWidget extends StatelessWidget {
required this.game,
required this.side,
required this.playingSide,
required this.customScoring,
});

final BroadcastGame game;
final Side side;
final Side playingSide;
final double width;
final BroadcastCustomScoring? customScoring;

@override
Widget build(BuildContext context) {
Expand All @@ -377,8 +388,11 @@ class _PlayerWidget extends StatelessWidget {
const SizedBox(width: 5),
if (game.isOver)
Text(
game.status.resultToString(side),
style: const TextStyle().copyWith(fontWeight: FontWeight.bold),
resultString(customScoring, side, game.status),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: .bold,
color: game.status.colorFor(side, context),
),
)
else if (clock != null)
CountdownClockBuilder(
Expand Down
1 change: 1 addition & 0 deletions lib/src/view/broadcast/broadcast_carousel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class BroadcastCarouselItem extends ConsumerStatefulWidget {
startsAt: null,
finishedAt: null,
startsAfterPrevious: false,
customScoring: null,
),
group: null,
roundToLinkId: BroadcastRoundId(''),
Expand Down
13 changes: 10 additions & 3 deletions lib/src/view/broadcast/broadcast_game_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dartchess/dartchess.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_analysis_controller.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart';
Expand Down Expand Up @@ -476,7 +477,10 @@ class _PlayerWidget extends ConsumerWidget {

final pastClocks = broadcastAnalysisState.clocks;
final pastClock = (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock;

final customScoring = switch (ref.watch(broadcastRoundCustomScoringProvider(roundId))) {
AsyncValue(value: final customScoring?, hasValue: true) => customScoring,
_ => null,
};
return GestureDetector(
onTap: () {
if (player.id != null) {
Expand Down Expand Up @@ -504,8 +508,11 @@ class _PlayerWidget extends ConsumerWidget {
children: [
if (game.isOver) ...[
Text(
game.status.resultToString(side),
style: const TextStyle(fontWeight: FontWeight.bold),
resultString(customScoring, side, game.status),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: .bold,
color: game.status.colorFor(side, context),
),
),
const SizedBox(width: 16.0),
],
Expand Down
6 changes: 6 additions & 0 deletions lib/src/view/broadcast/broadcast_game_screen_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@ final broadcastGameScreenTitleProvider = FutureProvider.autoDispose
final round = await ref.watch(broadcastRoundControllerProvider(roundId).future);
return round.round.name;
}, name: 'BroadcastGameScreenTitleProvider');

final broadcastRoundCustomScoringProvider = FutureProvider.autoDispose
.family<BroadcastCustomScoring?, BroadcastRoundId>((Ref ref, BroadcastRoundId roundId) async {
final round = await ref.watch(broadcastRoundControllerProvider(roundId).future);
return round.round.customScoring;
}, name: 'BroadcastRoundCustomScoringProvider');
1 change: 1 addition & 0 deletions lib/src/view/broadcast/broadcast_list_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class BroadcastListTile extends StatelessWidget {
startsAt: null,
finishedAt: null,
startsAfterPrevious: false,
customScoring: null,
),
group: null,
roundToLinkId: BroadcastRoundId(''),
Expand Down
Loading