feat: added statistic tile factory
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'
|
||||
show translateStatisticTypeToString;
|
||||
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
|
||||
|
||||
/// Build the [StatisticsTile] for a given [Statistic].
|
||||
|
||||
Widget buildStatisticTile({
|
||||
required Statistic statistic,
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
double? width,
|
||||
int itemCount = 5,
|
||||
}) {
|
||||
final filteredMatches = _getFilterMatches(statistic, matches);
|
||||
final filteredPlayers = _getFilteredPlayers(
|
||||
statistic,
|
||||
players,
|
||||
filteredMatches,
|
||||
);
|
||||
|
||||
print('Building tile for statistic: $statistic');
|
||||
print('Filtered matches count: ${filteredMatches.length}');
|
||||
print('Filtered players count: ${filteredPlayers.length}');
|
||||
|
||||
final values = _computeValuesForType(
|
||||
type: statistic.type,
|
||||
matches: filteredMatches,
|
||||
players: filteredPlayers,
|
||||
);
|
||||
print(values);
|
||||
|
||||
return StatisticsTile(
|
||||
icon: _getStatisticIcon(type: statistic.type),
|
||||
title: translateStatisticTypeToString(statistic.type, context),
|
||||
width: width ?? MediaQuery.sizeOf(context).width * 0.95,
|
||||
values: values,
|
||||
itemCount: itemCount,
|
||||
barColor: _getStatisticColor(statistic),
|
||||
statistic: statistic,
|
||||
);
|
||||
}
|
||||
|
||||
List<Match> _getFilterMatches(Statistic statistic, List<Match> matches) {
|
||||
List<Match> filteredMatches = matches;
|
||||
|
||||
// Filter timeframe
|
||||
if (statistic.scopes.contains(StatisticScope.timeframe) &&
|
||||
statistic.timeframe != null) {
|
||||
final minDate = _getMinimumDate(timeframe: statistic.timeframe!);
|
||||
print(
|
||||
'Filtering matches by timeframe: ${statistic.timeframe}, minDate: $minDate',
|
||||
);
|
||||
if (minDate != null) {
|
||||
filteredMatches = matches
|
||||
.where((m) => m.endedAt != null && m.endedAt!.isAfter(minDate))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Filter games
|
||||
if (statistic.scopes.contains(StatisticScope.selectedGames) &&
|
||||
(statistic.selectedGames?.isNotEmpty ?? false)) {
|
||||
final gameIds = statistic.selectedGames!.map((g) => g.id).toSet();
|
||||
filteredMatches = filteredMatches
|
||||
.where((match) => gameIds.contains(match.game.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filter groups
|
||||
if (statistic.scopes.contains(StatisticScope.selectedGroups) &&
|
||||
(statistic.selectedGroups?.isNotEmpty ?? false)) {
|
||||
final groupIds = statistic.selectedGroups!.map((g) => g.id).toSet();
|
||||
filteredMatches = filteredMatches
|
||||
.where((m) => m.group != null && groupIds.contains(m.group!.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return filteredMatches;
|
||||
}
|
||||
|
||||
/// Returns a [Player] List with the selected players depending on
|
||||
List<Player> _getFilteredPlayers(
|
||||
Statistic statistic,
|
||||
List<Player> allPlayers,
|
||||
List<Match> filteredMatches,
|
||||
) {
|
||||
// allPlayers
|
||||
if (statistic.scopes.contains(StatisticScope.allPlayers)) {
|
||||
return allPlayers;
|
||||
}
|
||||
|
||||
// selectedGroups -> only members
|
||||
if (statistic.scopes.contains(StatisticScope.selectedGroups) &&
|
||||
(statistic.selectedGroups?.isNotEmpty ?? false)) {
|
||||
final Set<String> ids = {
|
||||
for (final g in statistic.selectedGroups!)
|
||||
for (final p in g.members) p.id,
|
||||
};
|
||||
return allPlayers.where((p) => ids.contains(p.id)).toList();
|
||||
}
|
||||
|
||||
// Else -> all players from filtered matches
|
||||
final Set<String> ids = {
|
||||
for (final m in filteredMatches)
|
||||
for (final p in m.players) p.id,
|
||||
};
|
||||
return allPlayers.where((p) => ids.contains(p.id)).toList();
|
||||
}
|
||||
|
||||
/// Returns a [DateTime] with the minimum time and date the [timeframe] allows
|
||||
DateTime? _getMinimumDate({required Timeframe timeframe}) {
|
||||
final now = DateTime.now();
|
||||
switch (timeframe) {
|
||||
case Timeframe.last7Days:
|
||||
return now.subtract(const Duration(days: 7));
|
||||
case Timeframe.last30Days:
|
||||
return now.subtract(const Duration(days: 30));
|
||||
case Timeframe.last90Days:
|
||||
return now.subtract(const Duration(days: 90));
|
||||
case Timeframe.last180Days:
|
||||
return now.subtract(const Duration(days: 180));
|
||||
case Timeframe.lastYear:
|
||||
return now.subtract(const Duration(days: 365));
|
||||
case Timeframe.allTime:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the statistic values for each player based on the statistic type
|
||||
/// and returns a list of (Player, value) tuples sorted descending by value.
|
||||
List<(Player, num)> _computeValuesForType({
|
||||
required StatisticType type,
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
}) {
|
||||
switch (type) {
|
||||
case StatisticType.totalMatches:
|
||||
return _sortDesc(
|
||||
players.map((p) => (p, _matchesPlayed(p, matches) as num)).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.totalWins:
|
||||
return _sortDesc(
|
||||
players.map((p) => (p, _wins(p, matches) as num)).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.totalLosses:
|
||||
return _sortDesc(
|
||||
players
|
||||
.map(
|
||||
(p) =>
|
||||
(p, (_matchesPlayed(p, matches) - _wins(p, matches)) as num),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
case StatisticType.totalScore:
|
||||
return _sortDesc(
|
||||
players.map((p) => (p, _totalScore(p, matches) as num)).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.averageScore:
|
||||
return _sortDesc(
|
||||
players.map((p) {
|
||||
final scores = _scoresOf(p, matches);
|
||||
final avg = scores.isEmpty
|
||||
? 0.0
|
||||
: double.parse(
|
||||
(scores.reduce((a, b) => a + b) / scores.length)
|
||||
.toStringAsFixed(2),
|
||||
);
|
||||
return (p, avg as num);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.bestScore:
|
||||
return _sortDesc(
|
||||
players.map((p) {
|
||||
final scores = _scoresOf(p, matches);
|
||||
final best = scores.isEmpty ? 0 : scores.reduce(max);
|
||||
return (p, best as num);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.worstScore:
|
||||
// Ascending here is more meaningful for "worst", but keep the
|
||||
// existing tile semantics (largest bar = top entry) by sorting
|
||||
// descending on the inverse — i.e. show smallest score on top.
|
||||
final entries = players.map((p) {
|
||||
final scores = _scoresOf(p, matches);
|
||||
final worst = scores.isEmpty ? 0 : scores.reduce(min);
|
||||
return (p, worst as num);
|
||||
}).toList();
|
||||
entries.sort((a, b) => a.$2.compareTo(b.$2));
|
||||
return entries;
|
||||
|
||||
case StatisticType.winrate:
|
||||
return _sortDesc(
|
||||
players.map((p) {
|
||||
final played = _matchesPlayed(p, matches);
|
||||
final wins = _wins(p, matches);
|
||||
final rate = played == 0
|
||||
? 0.0
|
||||
: double.parse((wins / played).toStringAsFixed(2));
|
||||
return (p, rate as num);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper functions for different statistic types */
|
||||
|
||||
/// Detemerines how many matches the player has played in the given list of matches.
|
||||
int _matchesPlayed(Player p, List<Match> matches) =>
|
||||
matches.where((m) => m.players.any((mp) => mp.id == p.id)).length;
|
||||
|
||||
/// Determines how many matches the player has won in the given list of matches.
|
||||
int _wins(Player p, List<Match> matches) =>
|
||||
matches.where((m) => m.mvp.any((mp) => mp.id == p.id)).length;
|
||||
|
||||
/// Determines the total score of the player in the given list of matches.
|
||||
int _totalScore(Player p, List<Match> matches) {
|
||||
var total = 0;
|
||||
for (final m in matches) {
|
||||
final s = m.scores[p.id];
|
||||
if (s != null) total += s.score;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Returns a list of all scores the player has achieved in the given list of matches.
|
||||
List<int> _scoresOf(Player p, List<Match> matches) => [
|
||||
for (final m in matches)
|
||||
if (m.scores[p.id] != null) m.scores[p.id]!.score,
|
||||
];
|
||||
|
||||
/// Returns the list of entries sorted descending by the statistic value.
|
||||
List<(Player, num)> _sortDesc(List<(Player, num)> entries) {
|
||||
entries.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
return entries;
|
||||
}
|
||||
|
||||
/* Icon and color */
|
||||
|
||||
/// Returns the icon for the given statistic type.
|
||||
IconData _getStatisticIcon({required StatisticType type}) {
|
||||
switch (type) {
|
||||
case StatisticType.totalMatches:
|
||||
return Icons.casino;
|
||||
case StatisticType.totalWins:
|
||||
return Icons.emoji_events;
|
||||
case StatisticType.totalLosses:
|
||||
return Icons.sentiment_dissatisfied;
|
||||
case StatisticType.totalScore:
|
||||
return Icons.scoreboard;
|
||||
case StatisticType.averageScore:
|
||||
return Icons.show_chart;
|
||||
case StatisticType.bestScore:
|
||||
return Icons.trending_up;
|
||||
case StatisticType.worstScore:
|
||||
return Icons.trending_down;
|
||||
case StatisticType.winrate:
|
||||
return Icons.percent;
|
||||
}
|
||||
}
|
||||
|
||||
/// The bar colors for the statistic tiles
|
||||
const List<Color> _palette = <Color>[
|
||||
Color(0xFF4CAF50), // green
|
||||
Color(0xFF2196F3), // blue
|
||||
Color(0xFFFF9800), // orange
|
||||
Color(0xFFE91E63), // pink
|
||||
Color(0xFF9C27B0), // purple
|
||||
Color(0xFF00BCD4), // cyan
|
||||
Color(0xFFFFC107), // amber
|
||||
Color(0xFF3F51B5), // indigo
|
||||
Color(0xFF8BC34A), // light green
|
||||
Color(0xFFFF5722), // deep orange
|
||||
];
|
||||
|
||||
/// Returns a color from the palette based on the statistic's ID as random seed.
|
||||
Color _getStatisticColor(Statistic stat) {
|
||||
final seed = stat.id.hashCode;
|
||||
return _palette[seed.abs() % _palette.length];
|
||||
}
|
||||
|
||||
/* Skeleton data */
|
||||
|
||||
/// A placeholder tile with mock data for the loading state.
|
||||
Widget buildSkeletonStatisticTile({required BuildContext context}) {
|
||||
final count = 4 + Random().nextInt(5); // 4..8
|
||||
final values = <(Player, num)>[
|
||||
for (var i = 0; i < count; i++)
|
||||
(Player(name: 'Player ${i + 1}'), count - i),
|
||||
];
|
||||
|
||||
return StatisticsTile(
|
||||
icon: Icons.bar_chart,
|
||||
title: 'Skeleton title',
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
values: values,
|
||||
itemCount: count,
|
||||
barColor: _palette[Random().nextInt(_palette.length)],
|
||||
statistic: Statistic(
|
||||
type: StatisticType.totalMatches,
|
||||
scopes: [StatisticScope.allPlayers],
|
||||
timeframe: Timeframe.last7Days,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
|
||||
class StatisticsView extends StatefulWidget {
|
||||
/// A view that displays player statistics
|
||||
@@ -21,36 +21,19 @@ class StatisticsView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _StatisticsViewState extends State<StatisticsView> {
|
||||
int matchCount = 0;
|
||||
int groupCount = 0;
|
||||
|
||||
List<(Player, int)> winCounts = List.filled(6, (
|
||||
Player(name: 'Skeleton Player'),
|
||||
1,
|
||||
));
|
||||
List<(Player, int)> matchCounts = List.filled(6, (
|
||||
Player(name: 'Skeleton Player'),
|
||||
1,
|
||||
));
|
||||
List<(Player, double)> winRates = List.filled(6, (
|
||||
Player(name: 'Skeleton Player'),
|
||||
1,
|
||||
));
|
||||
bool isLoading = true;
|
||||
List<Widget> statisticTiles = List.generate(
|
||||
4,
|
||||
(_) => const InfoTile(
|
||||
icon: Icons.sports_score,
|
||||
title: 'Skeleton Statistic',
|
||||
width: double.infinity,
|
||||
content: Text('Skeleton content'),
|
||||
),
|
||||
);
|
||||
List<Match> _allMatches = const [];
|
||||
List<Player> _allPlayers = const [];
|
||||
List<Widget> statisticTiles = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getStatisticTiles(context);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
getStatisticTiles(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -66,13 +49,14 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
child: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [...statisticTiles],
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...statisticTiles,
|
||||
SizedBox(height: MediaQuery.paddingOf(context).bottom + 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -86,17 +70,16 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => CreateStatisticView(
|
||||
onStatisticCreated: loadStatisticData,
|
||||
onStatisticCreated: () => getStatisticTiles(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
final newTile = InfoTile(
|
||||
icon: Icons.sports_score,
|
||||
title: newStatistic.type.name,
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
content: Text(
|
||||
'${newStatistic.id}\n${newStatistic.scopes}\n${newStatistic.type}\n${newStatistic.timeframe}\n${newStatistic.selectedGroups}\n${newStatistic.selectedGames}\n',
|
||||
),
|
||||
if (!context.mounted) return;
|
||||
final newTile = buildStatisticTile(
|
||||
statistic: newStatistic,
|
||||
matches: _allMatches,
|
||||
players: _allPlayers,
|
||||
context: context,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
@@ -111,205 +94,47 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads matches and players from the database
|
||||
/// and calculates statistics for each player
|
||||
void loadStatisticData() {
|
||||
Future<void> getStatisticTiles(BuildContext context) async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
statisticTiles = List.generate(
|
||||
4,
|
||||
(index) => Column(
|
||||
children: [
|
||||
buildSkeletonStatisticTile(context: context),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
|
||||
Future.wait([
|
||||
final results = await Future.wait([
|
||||
db.statisticDao.getAllStatistics(),
|
||||
db.matchDao.getAllMatches(),
|
||||
db.playerDao.getAllPlayers(),
|
||||
db.matchDao.getMatchCount(),
|
||||
db.groupDao.getGroupCount(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
]).then((results) async {
|
||||
if (!mounted) return;
|
||||
]);
|
||||
|
||||
final matches = results[0] as List<Match>;
|
||||
final players = results[1] as List<Player>;
|
||||
matchCount = results[2] as int;
|
||||
groupCount = results[3] as int;
|
||||
if (!mounted) return;
|
||||
|
||||
winCounts = _calculateWinsForAllPlayers(
|
||||
matches: matches,
|
||||
players: players,
|
||||
context: context,
|
||||
);
|
||||
matchCounts = _calculateMatchAmountsForAllPlayers(
|
||||
matches: matches,
|
||||
players: players,
|
||||
context: context,
|
||||
);
|
||||
winRates = computeWinRatePercent(
|
||||
winCounts: winCounts,
|
||||
matchCounts: matchCounts,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getStatisticTiles(BuildContext context) async {
|
||||
isLoading = true;
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
final statistics = await db.statisticDao.getAllStatistics();
|
||||
final statistics = results[0] as List<Statistic>;
|
||||
_allMatches = results[1] as List<Match>;
|
||||
_allPlayers = results[2] as List<Player>;
|
||||
|
||||
setState(() {
|
||||
statisticTiles = [];
|
||||
for (var statistic in statistics) {
|
||||
statisticTiles.add(
|
||||
InfoTile(
|
||||
icon: Icons.sports_score,
|
||||
title: statistic.type.name,
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
content: Text(statistic.toString()),
|
||||
statisticTiles = [
|
||||
for (final statistic in statistics) ...[
|
||||
buildStatisticTile(
|
||||
statistic: statistic,
|
||||
matches: _allMatches,
|
||||
players: _allPlayers,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
statisticTiles.add(
|
||||
SizedBox(height: MediaQuery.sizeOf(context).height * 0.02),
|
||||
);
|
||||
}
|
||||
],
|
||||
];
|
||||
isLoading = false;
|
||||
});
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
/// Calculates the number of wins for each player
|
||||
/// and returns a sorted list of tuples (playerName, winCount)
|
||||
List<(Player, int)> _calculateWinsForAllPlayers({
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
List<(Player, int)> winCounts = [];
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Getting the winners
|
||||
for (var match in matches) {
|
||||
final mvps = match.mvp;
|
||||
for (var winner in mvps) {
|
||||
final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id);
|
||||
// -1 means winner not found in winCounts
|
||||
if (index != -1) {
|
||||
final current = winCounts[index].$2;
|
||||
winCounts[index] = (winner, current + 1);
|
||||
} else {
|
||||
winCounts.add((winner, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding all players with zero wins
|
||||
for (var player in players) {
|
||||
final index = winCounts.indexWhere((entry) => entry.$1.id == player.id);
|
||||
// -1 means player not found in winCounts
|
||||
if (index == -1) {
|
||||
winCounts.add((player, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace player IDs with names
|
||||
for (int i = 0; i < winCounts.length; i++) {
|
||||
final playerId = winCounts[i].$1.id;
|
||||
final player = players.firstWhere(
|
||||
(p) => p.id == playerId,
|
||||
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||
);
|
||||
winCounts[i] = (player, winCounts[i].$2);
|
||||
}
|
||||
|
||||
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
|
||||
return winCounts;
|
||||
}
|
||||
|
||||
/// Calculates the number of matches played for each player
|
||||
/// and returns a sorted list of tuples (playerName, matchCount)
|
||||
List<(Player, int)> _calculateMatchAmountsForAllPlayers({
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
List<(Player, int)> matchCounts = [];
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Counting matches for each player
|
||||
for (var match in matches) {
|
||||
for (Player player in match.players) {
|
||||
// Check if the player is already in matchCounts
|
||||
final index = matchCounts.indexWhere(
|
||||
(entry) => entry.$1.id == player.id,
|
||||
);
|
||||
|
||||
// -1 -> not found
|
||||
if (index == -1) {
|
||||
// Add new entry
|
||||
matchCounts.add((player, 1));
|
||||
} else {
|
||||
// Update existing entry
|
||||
final currentMatchAmount = matchCounts[index].$2;
|
||||
matchCounts[index] = (player, currentMatchAmount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding all players with zero matches
|
||||
for (var player in players) {
|
||||
final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id);
|
||||
// -1 means player not found in matchCounts
|
||||
if (index == -1) {
|
||||
matchCounts.add((player, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace player IDs with names
|
||||
for (int i = 0; i < matchCounts.length; i++) {
|
||||
final playerId = matchCounts[i].$1.id;
|
||||
final player = players.firstWhere(
|
||||
(p) => p.id == playerId,
|
||||
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||
);
|
||||
matchCounts[i] = (player, matchCounts[i].$2);
|
||||
}
|
||||
|
||||
matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
|
||||
return matchCounts;
|
||||
}
|
||||
|
||||
List<(Player, double)> computeWinRatePercent({
|
||||
required List<(Player, int)> winCounts,
|
||||
required List<(Player, int)> matchCounts,
|
||||
}) {
|
||||
final Map<Player, int> winsMap = {for (var e in winCounts) e.$1: e.$2};
|
||||
final Map<Player, int> matchesMap = {for (var e in matchCounts) e.$1: e.$2};
|
||||
|
||||
// Get all unique player names
|
||||
final player = {...matchesMap.keys};
|
||||
|
||||
// Calculate win rates
|
||||
final result = player.map((name) {
|
||||
final int w = winsMap[name] ?? 0;
|
||||
final int m = matchesMap[name] ?? 0;
|
||||
// Calculate percentage and round to 2 decimal places
|
||||
// Avoid division by zero
|
||||
final double percent = (m > 0)
|
||||
? double.parse(((w / m)).toStringAsFixed(2))
|
||||
: 0;
|
||||
return (name, percent);
|
||||
}).toList();
|
||||
|
||||
// Sort the result: first by winrate descending,
|
||||
// then by wins descending in case of a tie
|
||||
result.sort((a, b) {
|
||||
final cmp = b.$2.compareTo(a.$2);
|
||||
if (cmp != 0) return cmp;
|
||||
final wa = winsMap[a.$1] ?? 0;
|
||||
final wb = winsMap[b.$1] ?? 0;
|
||||
return wb.compareTo(wa);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user