Updated score and winner handling

This commit is contained in:
2026-04-21 18:38:00 +02:00
parent 522441b0ca
commit 9364f0d9d6
19 changed files with 286 additions and 179 deletions

View File

@@ -36,6 +36,7 @@ class GroupDetailView extends StatefulWidget {
}
class _GroupDetailViewState extends State<GroupDetailView> {
late final AppLocalizations loc;
late final AppDatabase db;
bool isLoading = true;
late Group _group;
@@ -51,13 +52,12 @@ class _GroupDetailViewState extends State<GroupDetailView> {
super.initState();
_group = widget.group;
db = Provider.of<AppDatabase>(context, listen: false);
loc = AppLocalizations.of(context);
_loadStatistics();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
@@ -259,28 +259,36 @@ class _GroupDetailViewState extends State<GroupDetailView> {
/// Determines the best player in the group based on match wins
String _getBestPlayer(List<Match> matches) {
final bestPlayerCounts = <Player, int>{};
final mvpCounts = <Player, int>{};
// Count wins for each player
for (var match in matches) {
if (match.winner != null &&
_group.members.any((m) => m.id == match.winner?.id)) {
print(match.winner);
bestPlayerCounts.update(
match.winner!,
(value) => value + 1,
ifAbsent: () => 1,
);
final mvps = match.mvp;
for (final mvpPlayer in mvps) {
if (_group.members.any((m) => m.id == mvpPlayer.id)) {
mvpCounts.update(mvpPlayer, (value) => value + 1, ifAbsent: () => 1);
}
}
}
// Sort players by win count
final sortedPlayers = bestPlayerCounts.entries.toList()
final sortedMvps = mvpCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
// Get the best player
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
if (sortedMvps.isEmpty) {
return '-';
}
return bestPlayer;
// Check if there are multiple players with the same value
final highestMvpCount = sortedMvps.first.value;
final topPlayers = sortedMvps
.where((entry) => entry.value == highestMvpCount)
.toList();
switch (topPlayers.length) {
case 0:
return '-';
case 1:
return topPlayers.first.key.name;
default:
return loc.tie;
}
}
}

View File

@@ -43,21 +43,25 @@ class _HomeViewState extends State<HomeView> {
Match(
name: 'Skeleton Match',
game: Game(
name: '',
name: 'Skeleton Game',
ruleset: Ruleset.singleWinner,
description: '',
description: 'This is a skeleton game description.',
color: GameColor.blue,
icon: '',
),
group: Group(
name: 'Skeleton Group',
description: '',
description: 'This is a skeleton group description.',
members: [
Player(name: 'Skeleton Player 1', description: ''),
Player(name: 'Skeleton Player 2', description: ''),
],
),
notes: '',
notes: 'These are skeleton notes.',
players: [
Player(name: 'Skeleton Player 1', description: ''),
Player(name: 'Skeleton Player 2', description: ''),
],
),
);
@@ -231,7 +235,8 @@ class _HomeViewState extends State<HomeView> {
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) {
setState(() {
recentMatches[matchIndex].winner = winner;
// TODO: fix
//recentMatches[matchIndex].winner = winner;
});
}
}

View File

@@ -277,7 +277,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
group: selectedGroup,
players: selectedPlayers,
game: tempGame,
winner: widget.matchToEdit!.winner,
createdAt: widget.matchToEdit!.createdAt,
endedAt: widget.matchToEdit!.endedAt,
notes: widget.matchToEdit!.notes,
@@ -314,9 +313,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
matchId: widget.matchToEdit!.id,
playerId: player.id,
);
if (widget.matchToEdit!.winner?.id == player.id) {
updatedMatch.winner = null;
}
}
}

View File

@@ -203,7 +203,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
text: loc.enter_results,
icon: Icons.emoji_events,
onPressed: () async {
match.winner = await Navigator.push(
await Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: true,
@@ -237,54 +237,29 @@ class _MatchDetailViewState extends State<MatchDetailView> {
}
/// Returns the widget to be displayed in the result [InfoTile]
/// TODO: Update when score logic is overhauled
Widget getResultWidget(AppLocalizations loc) {
if (isSingleRowResult()) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getResultRow(loc),
children: getSingleResultRow(loc),
);
} else {
return Column(
children: [
for (var player in match.players)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
player.name,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
'0 ${loc.points}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
],
),
],
);
return getScoreResultWidget(loc);
}
}
/// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet
/// TODO: Update when score logic is overhauled
List<Widget> getResultRow(AppLocalizations loc) {
if (match.winner != null && match.game.ruleset == Ruleset.singleWinner) {
List<Widget> getSingleResultRow(AppLocalizations loc) {
// Single Winner
if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) {
return [
Text(
loc.winner,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.winner!.name,
match.mvp.first.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -292,6 +267,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
),
),
];
// Single Loser
} else if (match.game.ruleset == Ruleset.singleLoser) {
return [
Text(
@@ -299,7 +275,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.winner!.name,
match.mvp.first.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -307,6 +283,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
),
),
];
// No result entered yet
} else {
return [
Text(
@@ -317,6 +294,46 @@ class _MatchDetailViewState extends State<MatchDetailView> {
}
}
/// Returns the result widget for scores
Widget getScoreResultWidget(AppLocalizations loc) {
List<(String, int)> playerScores = [];
for (var player in match.players) {
int score = match.scores[player.id]?.score ?? 0;
playerScores.add((player.name, score));
}
if (widget.match.game.ruleset == Ruleset.highestScore) {
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (widget.match.game.ruleset == Ruleset.lowestScore) {
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
}
return Column(
children: [
for (var score in playerScores)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
score.$1,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
getPointLabel(loc, score.$2),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
],
),
],
);
}
// Returns if the result can be displayed in a single row
bool isSingleRowResult() {
return match.game.ruleset == Ruleset.singleWinner ||

View File

@@ -5,6 +5,7 @@ import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
@@ -14,20 +15,11 @@ class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match
/// [match]: The match for which the winner is to be selected
/// [onWinnerChanged]: Optional callback invoked when the winner is changed
const MatchResultView({
super.key,
required this.match,
this.ruleset = Ruleset.singleWinner,
this.onWinnerChanged,
});
const MatchResultView({super.key, required this.match, this.onWinnerChanged});
/// The match for which the winner is to be selected
final Match match;
/// The ruleset of the match, determines how the winner is selected or how
/// scores are entered
final Ruleset ruleset;
/// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged;
@@ -38,6 +30,8 @@ class MatchResultView extends StatefulWidget {
class _MatchResultViewState extends State<MatchResultView> {
late final AppDatabase db;
late final Ruleset ruleset;
/// List of all players who participated in the match
late final List<Player> allPlayers;
@@ -51,6 +45,8 @@ class _MatchResultViewState extends State<MatchResultView> {
void initState() {
db = Provider.of<AppDatabase>(context, listen: false);
ruleset = widget.match.game.ruleset;
allPlayers = widget.match.players;
allPlayers.sort((a, b) => a.name.compareTo(b.name));
@@ -59,13 +55,17 @@ class _MatchResultViewState extends State<MatchResultView> {
(index) => TextEditingController(),
);
if (widget.match.winner != null) {
if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.winner!.id,
(p) => p.id == widget.match.mvp.first.id,
);
} else if (rulesetSupportsScoreEntry()) {
/// TODO: Update when score logic is overhauled
for (int i = 0; i < allPlayers.length; i++) {
final scoreList = widget.match.scores[allPlayers[i].id];
final score = scoreList?.score ?? 0;
controller[i].text = score.toString();
}
}
}
super.initState();
@@ -154,7 +154,6 @@ class _MatchResultViewState extends State<MatchResultView> {
child: ListView.separated(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
print(allPlayers[index].name);
return ScoreListTile(
text: allPlayers[index].name,
controller: controller[index],
@@ -176,6 +175,11 @@ class _MatchResultViewState extends State<MatchResultView> {
text: loc.save_changes,
sizeRelativeToWidth: 0.95,
onPressed: () async {
final ending = DateTime.now();
await db.matchDao.updateMatchEndedAt(
matchId: widget.match.id,
endedAt: ending,
);
await _handleSaving();
if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer);
@@ -190,12 +194,12 @@ class _MatchResultViewState extends State<MatchResultView> {
/// Handles saving or removing the winner in the database
/// based on the current selection.
Future<void> _handleSaving() async {
if (widget.ruleset == Ruleset.singleWinner) {
if (ruleset == Ruleset.singleWinner) {
await _handleWinner();
} else if (widget.ruleset == Ruleset.singleLoser) {
} else if (ruleset == Ruleset.singleLoser) {
await _handleLoser();
} else if (widget.ruleset == Ruleset.lowestScore ||
widget.ruleset == Ruleset.highestScore) {
} else if (ruleset == Ruleset.lowestScore ||
ruleset == Ruleset.highestScore) {
await _handleScores();
}
@@ -204,9 +208,9 @@ class _MatchResultViewState extends State<MatchResultView> {
Future<bool> _handleWinner() async {
if (_selectedPlayer == null) {
await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else {
await db.scoreEntryDao.setWinner(
return await db.scoreEntryDao.setWinner(
matchId: widget.match.id,
playerId: _selectedPlayer!.id,
);
@@ -215,33 +219,33 @@ class _MatchResultViewState extends State<MatchResultView> {
Future<bool> _handleLoser() async {
if (_selectedPlayer == null) {
/// TODO: Update when score logic is overhauled
return false;
return await db.scoreEntryDao.removeLooser(matchId: widget.match.id);
} else {
/// TODO: Update when score logic is overhauled
return false;
return await db.scoreEntryDao.setLooser(
matchId: widget.match.id,
playerId: _selectedPlayer!.id,
);
}
}
/// Handles saving the scores for each player in the database.
Future<bool> _handleScores() async {
Future<void> _handleScores() async {
for (int i = 0; i < allPlayers.length; i++) {
var text = controller[i].text;
if (text.isEmpty) {
text = '0';
}
final score = int.parse(text);
await db.playerMatchDao.updatePlayerScore(
await db.scoreEntryDao.addScore(
matchId: widget.match.id,
playerId: allPlayers[i].id,
newScore: score,
entry: ScoreEntry(roundNumber: 0, score: score, change: 0),
);
}
return false;
}
String getTitleForRuleset(AppLocalizations loc) {
switch (widget.ruleset) {
switch (ruleset) {
case Ruleset.singleWinner:
return loc.select_winner;
case Ruleset.singleLoser:
@@ -252,12 +256,10 @@ class _MatchResultViewState extends State<MatchResultView> {
}
bool rulesetSupportsWinnerSelection() {
return widget.ruleset == Ruleset.singleWinner ||
widget.ruleset == Ruleset.singleLoser;
return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser;
}
bool rulesetSupportsScoreEntry() {
return widget.ruleset == Ruleset.lowestScore ||
widget.ruleset == Ruleset.highestScore;
return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
}
}

View File

@@ -37,20 +37,16 @@ class _MatchViewState extends State<MatchView> {
Match(
name: 'Skeleton match name',
game: Game(
name: '',
name: 'Game name',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
icon: '',
),
group: Group(
name: 'Group name',
description: '',
members: List.filled(5, Player(name: 'Player', description: '')),
members: List.filled(5, Player(name: 'Player')),
),
winner: Player(name: 'Player', description: ''),
players: [Player(name: 'Player', description: '')],
notes: '',
players: [Player(name: 'Player')],
),
);
@@ -116,7 +112,7 @@ class _MatchViewState extends State<MatchView> {
Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20,
child: MainMenuButton(
text: 'Spiel erstellen',
text: loc.create_match,
icon: RpgAwesome.clovers_card,
onPressed: () async {
Navigator.push(

View File

@@ -140,8 +140,8 @@ class _StatisticsViewState extends State<StatisticsView> {
// Getting the winners
for (var match in matches) {
final winner = match.winner;
if (winner != null) {
final mvps = match.mvp;
for (var winner in mvps) {
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
// -1 means winner not found in winCounts
if (index != -1) {

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -44,7 +45,6 @@ class _MatchTileState extends State<MatchTile> {
Widget build(BuildContext context) {
final match = widget.match;
final group = match.group;
final winner = match.winner;
final players = [...match.players]
..sort((a, b) => a.name.compareTo(b.name));
final loc = AppLocalizations.of(context);
@@ -131,7 +131,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12),
],
if (winner != null) ...[
if (match.mvp.isNotEmpty) ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
@@ -155,7 +155,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(width: 8),
Expanded(
child: Text(
'${loc.winner}: ${winner.name}',
getWinner(loc),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -248,4 +248,21 @@ class _MatchTileState extends State<MatchTile> {
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
}
}
String getWinner(AppLocalizations loc) {
if (widget.match.mvp.isEmpty) return '';
final ruleset = widget.match.game.ruleset;
if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) {
return '${loc.winner}: ${widget.match.mvp.first.name}';
} else if (ruleset == Ruleset.lowestScore ||
ruleset == Ruleset.lowestScore) {
final mvp = widget.match.mvp;
final mvpScore = widget.match.scores[mvp.first.id]?.score ?? 0;
final mvpNames = mvp.map((player) => player.name).join(', ');
return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})';
}
return '${loc.winner}: n.A.';
}
}