From 9364f0d9d6b024afef444f010e598d9bf4d6047b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 21 Apr 2026 18:38:00 +0200 Subject: [PATCH] Updated score and winner handling --- lib/data/dao/game_dao.dart | 22 ++++- lib/data/dao/game_dao.g.dart | 6 ++ lib/data/dao/match_dao.dart | 28 ++----- lib/data/dao/score_entry_dao.dart | 28 +++++-- lib/data/models/match.dart | 76 +++++++++++++---- .../group_view/group_detail_view.dart | 42 ++++++---- .../views/main_menu/home_view.dart | 15 ++-- .../create_match/create_match_view.dart | 4 - .../match_view/match_detail_view.dart | 83 +++++++++++-------- .../match_view/match_result_view.dart | 68 +++++++-------- .../main_menu/match_view/match_view.dart | 12 +-- .../views/main_menu/statistics_view.dart | 4 +- .../widgets/tiles/match_tile.dart | 23 ++++- lib/services/data_transfer_service.dart | 15 +--- test/db_tests/aggregates/match_test.dart | 12 ++- test/db_tests/aggregates/team_test.dart | 12 ++- .../relationships/player_match_test.dart | 1 + test/db_tests/values/score_test.dart | 4 +- test/services/data_transfer_service_test.dart | 10 +-- 19 files changed, 286 insertions(+), 179 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index f07e2c7..5632abd 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -2,11 +2,12 @@ import 'package:drift/drift.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/game_table.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; import 'package:tallee/data/models/game.dart'; part 'game_dao.g.dart'; -@DriftAccessor(tables: [GameTable]) +@DriftAccessor(tables: [MatchTable, GameTable]) class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameDao(super.db); @@ -44,6 +45,25 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { ); } + Future getGameByMatchId({required String matchId}) async { + final query = select(gameTable).join([ + innerJoin(matchTable, matchTable.gameId.equalsExp(gameTable.id)), + ])..where(matchTable.id.equals(matchId)); + + final result = await query.getSingle(); + final gameRow = result.readTable(gameTable); + + return Game( + id: gameRow.id, + name: gameRow.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == gameRow.ruleset), + description: gameRow.description, + color: GameColor.values.firstWhere((e) => e.name == gameRow.color), + icon: gameRow.icon, + createdAt: gameRow.createdAt, + ); + } + /// Adds a new [game] to the database. /// If a game with the same ID already exists, no action is taken. /// Returns `true` if the game was added, `false` otherwise. diff --git a/lib/data/dao/game_dao.g.dart b/lib/data/dao/game_dao.g.dart index a998fe7..7b86d21 100644 --- a/lib/data/dao/game_dao.g.dart +++ b/lib/data/dao/game_dao.g.dart @@ -5,6 +5,8 @@ part of 'game_dao.dart'; // ignore_for_file: type=lint mixin _$GameDaoMixin on DatabaseAccessor { $GameTableTable get gameTable => attachedDatabase.gameTable; + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; GameDaoManager get managers => GameDaoManager(this); } @@ -13,4 +15,8 @@ class GameDaoManager { GameDaoManager(this._db); $$GameTableTableTableManager get gameTable => $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); + $$GroupTableTableTableManager get groupTable => + $$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable); + $$MatchTableTableTableManager get matchTable => + $$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable); } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 81925c7..0b9300e 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -34,7 +34,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: row.id, ); - final winner = await db.scoreEntryDao.getWinner(matchId: row.id); return Match( id: row.id, name: row.name, @@ -45,7 +44,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { createdAt: row.createdAt, endedAt: row.endedAt, scores: scores, - winner: winner, ); }), ); @@ -68,8 +66,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); - final winner = await db.scoreEntryDao.getWinner(matchId: matchId); - return Match( id: result.id, name: result.name, @@ -80,7 +76,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { createdAt: result.createdAt, endedAt: result.endedAt, scores: scores, - winner: winner, ); } @@ -110,19 +105,14 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } for (final pid in match.scores.keys) { - final playerScores = match.scores[pid]!; - await db.scoreEntryDao.addScoresAsList( - entrys: playerScores, - playerId: pid, - matchId: match.id, - ); - } - - if (match.winner != null) { - await db.scoreEntryDao.setWinner( - matchId: match.id, - playerId: match.winner!.id, - ); + final playerScores = match.scores[pid]; + if (playerScores != null) { + await db.scoreEntryDao.addScore( + entry: playerScores, + playerId: pid, + matchId: match.id, + ); + } } }); } @@ -300,7 +290,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final group = await db.groupDao.getGroupById(groupId: groupId); final players = await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; - final winner = await db.scoreEntryDao.getWinner(matchId: row.id); return Match( id: row.id, name: row.name, @@ -310,7 +299,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { notes: row.notes ?? '', createdAt: row.createdAt, endedAt: row.endedAt, - winner: winner, ); }), ); diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index cdd42f9..9aa3ca2 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; +import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/models/player.dart'; @@ -83,21 +84,21 @@ class ScoreEntryDao extends DatabaseAccessor } /// Retrieves all scores for a specific match. - Future>> getAllMatchScores({ + Future> getAllMatchScores({ required String matchId, }) async { final query = select(scoreEntryTable) ..where((s) => s.matchId.equals(matchId)); final result = await query.get(); - final Map> scoresByPlayer = {}; + final Map scoresByPlayer = {}; for (final row in result) { final score = ScoreEntry( roundNumber: row.roundNumber, score: row.score, change: row.change, ); - scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score); + scoresByPlayer[row.playerId] = score; } return scoresByPlayer; @@ -237,10 +238,25 @@ class ScoreEntryDao extends DatabaseAccessor // Retrieves the winner of a match based on the highest score. Future getWinner({required String matchId}) async { + // Check the ruleset of the match + final ruleset = await db.gameDao + .getGameByMatchId(matchId: matchId) + .then((game) => game.ruleset); + final query = select(scoreEntryTable) - ..where((s) => s.matchId.equals(matchId)) - ..orderBy([(s) => OrderingTerm.desc(s.score)]) - ..limit(1); + ..where((s) => s.matchId.equals(matchId)); + + // If the ruleset is lowestScore, the winner is the player with the lowest + // score so we order by ascending score. + if (ruleset == Ruleset.lowestScore) { + query + ..orderBy([(s) => OrderingTerm.asc(s.score)]) + ..limit(1); + } else { + query + ..orderBy([(s) => OrderingTerm.desc(s.score)]) + ..limit(1); + } final result = await query.getSingleOrNull(); if (result == null) return null; diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 60103de..5c37cbe 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -15,27 +15,25 @@ class Match { final Group? group; final List players; final String notes; - Map> scores; - Player? winner; + Map scores; Match({ - String? id, - DateTime? createdAt, - this.endedAt, required this.name, required this.game, + required this.players, + this.endedAt, this.group, - this.players = const [], this.notes = '', - Map>? scores, - this.winner, + String? id, + DateTime? createdAt, + Map? scores, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), - scores = scores ?? {for (var player in players) player.id: []}; + scores = scores ?? {for (Player p in players) p.id: null}; @override String toString() { - return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, winner: $winner}'; + return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}'; } /// Creates a Match instance from a JSON object where related objects are @@ -71,10 +69,60 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), - 'scores': scores.map( - (playerId, scoreList) => - MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()), - ), + 'scores': scores, 'notes': notes, }; + + List get mvp { + if (players.isEmpty || scores.isEmpty) return []; + + switch (game.ruleset) { + case Ruleset.highestScore: + return _getPlayersWithHighestScore(); + + case Ruleset.lowestScore: + return _getPlayersWithLowestScore(); + + case Ruleset.singleWinner: + return [_getPlayersWithHighestScore().first]; + + case Ruleset.singleLoser: + return [_getPlayersWithLowestScore().first]; + + case Ruleset.multipleWinners: + return []; + } + } + + List _getPlayersWithHighestScore() { + if (players.isEmpty || scores.isEmpty) return []; + + final int highestScore = players + .map((player) => scores[player.id]?.score) + .whereType() + .reduce((max, score) => score > max ? score : max); + + return players.where((player) { + final playerScores = scores[player.id]; + if (playerScores == null) return false; + return playerScores.score == highestScore; + }).toList(); + } + + List _getPlayersWithLowestScore() { + if (players.isEmpty || scores.values.every((score) => score == null)) { + return []; + } + + final int lowestScore = players + .map((player) => scores[player.id]?.score) + .whereType() + .reduce((min, score) => score < min ? score : min); + + return players.where((player) { + final playerScore = scores[player.id]; + if (playerScore == null) return false; + return playerScore.score == lowestScore; + }).toList(); + } } diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 1ef89ef..7feae0f 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -36,6 +36,7 @@ class GroupDetailView extends StatefulWidget { } class _GroupDetailViewState extends State { + late final AppLocalizations loc; late final AppDatabase db; bool isLoading = true; late Group _group; @@ -51,13 +52,12 @@ class _GroupDetailViewState extends State { super.initState(); _group = widget.group; db = Provider.of(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 { /// Determines the best player in the group based on match wins String _getBestPlayer(List matches) { - final bestPlayerCounts = {}; + final mvpCounts = {}; - // 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; + } } } diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 09cec54..d6b9ee6 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -43,21 +43,25 @@ class _HomeViewState extends State { 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 { final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); if (matchIndex != -1) { setState(() { - recentMatches[matchIndex].winner = winner; + // TODO: fix + //recentMatches[matchIndex].winner = winner; }); } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 950b3a8..83e5d0c 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -277,7 +277,6 @@ class _CreateMatchViewState extends State { 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 { matchId: widget.matchToEdit!.id, playerId: player.id, ); - if (widget.matchToEdit!.winner?.id == player.id) { - updatedMatch.winner = null; - } } } diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 81c736d..1ac8a77 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -203,7 +203,7 @@ class _MatchDetailViewState extends State { 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 { } /// 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 getResultRow(AppLocalizations loc) { - if (match.winner != null && match.game.ruleset == Ruleset.singleWinner) { + List 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 { ), ), ]; + // Single Loser } else if (match.game.ruleset == Ruleset.singleLoser) { return [ Text( @@ -299,7 +275,7 @@ class _MatchDetailViewState extends State { 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 { ), ), ]; + // No result entered yet } else { return [ Text( @@ -317,6 +294,46 @@ class _MatchDetailViewState extends State { } } + /// 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 || diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 92424ec..af59cf6 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -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 { late final AppDatabase db; + late final Ruleset ruleset; + /// List of all players who participated in the match late final List allPlayers; @@ -51,6 +45,8 @@ class _MatchResultViewState extends State { void initState() { db = Provider.of(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 { (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 { 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 { 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 { /// Handles saving or removing the winner in the database /// based on the current selection. Future _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 { Future _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 { Future _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 _handleScores() async { + Future _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 { } 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; } } diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 1a202c4..2fb36e7 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -37,20 +37,16 @@ class _MatchViewState extends State { 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 { Positioned( bottom: MediaQuery.paddingOf(context).bottom + 20, child: MainMenuButton( - text: 'Spiel erstellen', + text: loc.create_match, icon: RpgAwesome.clovers_card, onPressed: () async { Navigator.push( diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 3a55115..b0b9c22 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -140,8 +140,8 @@ class _StatisticsViewState extends State { // 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) { diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 4977b8e..5727c71 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -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 { 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 { 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 { 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 { 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.'; + } } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index a0fd57b..05896ea 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -71,20 +71,7 @@ class DataTransferService { 'gameId': m.game.id, 'groupId': m.group?.id, 'playerIds': m.players.map((p) => p.id).toList(), - 'scores': m.scores.map( - (playerId, scores) => MapEntry( - playerId, - scores - .map( - (s) => { - 'roundNumber': s.roundNumber, - 'score': s.score, - 'change': s.change, - }, - ) - .toList(), - ), - ), + 'scores': m.scores, 'notes': m.notes, }, ) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index dee0eb9..245cca1 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -63,7 +63,6 @@ void main() { game: testGame, group: testGroup1, players: [testPlayer4, testPlayer5], - winner: testPlayer4, notes: '', ); testMatch2 = Match( @@ -71,20 +70,19 @@ void main() { game: testGame, group: testGroup2, players: [testPlayer1, testPlayer2, testPlayer3], - winner: testPlayer2, notes: '', ); testMatchOnlyPlayers = Match( name: 'Test Match with Players', game: testGame, players: [testPlayer1, testPlayer2, testPlayer3], - winner: testPlayer3, notes: '', ); testMatchOnlyGroup = Match( name: 'Test Match with Group', game: testGame, group: testGroup2, + players: testGroup2.members, notes: '', ); }); @@ -289,8 +287,8 @@ void main() { matchId: testMatch1.id, ); - expect(fetchedMatch.winner, isNotNull); - expect(fetchedMatch.winner!.id, testPlayer4.id); + expect(fetchedMatch.mvp, isNotNull); + expect(fetchedMatch.mvp.first.id, testPlayer4.id); }); test('Setting a winner works correctly', () async { @@ -304,8 +302,8 @@ void main() { final fetchedMatch = await database.matchDao.getMatchById( matchId: testMatch1.id, ); - expect(fetchedMatch.winner, isNotNull); - expect(fetchedMatch.winner!.id, testPlayer5.id); + expect(fetchedMatch.mvp, isNotNull); + expect(fetchedMatch.mvp.first.id, testPlayer5.id); }); test( diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 327bc8f..0850936 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -343,8 +343,16 @@ void main() { // Verifies that teams with overlapping members are independent. test('Teams with overlapping members are independent', () async { // Create two matches since player_match has primary key {playerId, matchId} - final match1 = Match(name: 'Match 1', game: testGame1, notes: ''); - final match2 = Match(name: 'Match 2', game: testGame2, notes: ''); + final match1 = Match( + name: 'Match 1', + game: testGame1, + players: [testPlayer1, testPlayer2], + ); + final match2 = Match( + name: 'Match 2', + game: testGame2, + players: [testPlayer1, testPlayer2], + ); await database.matchDao.addMatch(match: match1); await database.matchDao.addMatch(match: match2); diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 3db48de..3071edf 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -58,6 +58,7 @@ void main() { testMatchOnlyGroup = Match( name: 'Test Match with Group', game: testGame, + players: testGroup.members, group: testGroup, notes: '', ); diff --git a/test/db_tests/values/score_test.dart b/test/db_tests/values/score_test.dart index fc87cc4..c5e2209 100644 --- a/test/db_tests/values/score_test.dart +++ b/test/db_tests/values/score_test.dart @@ -231,8 +231,8 @@ void main() { ); expect(scores.length, 2); - expect(scores[testPlayer1.id]!.length, 2); - expect(scores[testPlayer2.id]!.length, 1); + expect(scores[testPlayer1.id]!, isNotNull); + expect(scores[testPlayer2.id]!, isNotNull); }); test('getAllMatchScores() with no scores saved', () async { diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 575e52f..12eb4b1 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -64,14 +64,8 @@ void main() { players: [testPlayer1, testPlayer2], notes: 'Test notes', scores: { - testPlayer1.id: [ - ScoreEntry(roundNumber: 1, score: 10, change: 10), - ScoreEntry(roundNumber: 2, score: 20, change: 10), - ], - testPlayer2.id: [ - ScoreEntry(roundNumber: 1, score: 15, change: 15), - ScoreEntry(roundNumber: 2, score: 25, change: 10), - ], + testPlayer1.id: ScoreEntry(roundNumber: 1, score: 10, change: 10), + testPlayer2.id: ScoreEntry(roundNumber: 1, score: 15, change: 15), }, ); });