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

@@ -2,11 +2,12 @@ import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/game_table.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'; import 'package:tallee/data/models/game.dart';
part 'game_dao.g.dart'; part 'game_dao.g.dart';
@DriftAccessor(tables: [GameTable]) @DriftAccessor(tables: [MatchTable, GameTable])
class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin { class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
GameDao(super.db); GameDao(super.db);
@@ -44,6 +45,25 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
); );
} }
Future<Game> 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. /// Adds a new [game] to the database.
/// If a game with the same ID already exists, no action is taken. /// If a game with the same ID already exists, no action is taken.
/// Returns `true` if the game was added, `false` otherwise. /// Returns `true` if the game was added, `false` otherwise.

View File

@@ -5,6 +5,8 @@ part of 'game_dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable; $GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
GameDaoManager get managers => GameDaoManager(this); GameDaoManager get managers => GameDaoManager(this);
} }
@@ -13,4 +15,8 @@ class GameDaoManager {
GameDaoManager(this._db); GameDaoManager(this._db);
$$GameTableTableTableManager get gameTable => $$GameTableTableTableManager get gameTable =>
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
$$GroupTableTableTableManager get groupTable =>
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
$$MatchTableTableTableManager get matchTable =>
$$MatchTableTableTableManager(_db.attachedDatabase, _db.matchTable);
} }

View File

@@ -34,7 +34,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
matchId: row.id, matchId: row.id,
); );
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match( return Match(
id: row.id, id: row.id,
name: row.name, name: row.name,
@@ -45,7 +44,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,
scores: scores, scores: scores,
winner: winner,
); );
}), }),
); );
@@ -68,8 +66,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId); final scores = await db.scoreEntryDao.getAllMatchScores(matchId: matchId);
final winner = await db.scoreEntryDao.getWinner(matchId: matchId);
return Match( return Match(
id: result.id, id: result.id,
name: result.name, name: result.name,
@@ -80,7 +76,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
createdAt: result.createdAt, createdAt: result.createdAt,
endedAt: result.endedAt, endedAt: result.endedAt,
scores: scores, scores: scores,
winner: winner,
); );
} }
@@ -110,19 +105,14 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
} }
for (final pid in match.scores.keys) { for (final pid in match.scores.keys) {
final playerScores = match.scores[pid]!; final playerScores = match.scores[pid];
await db.scoreEntryDao.addScoresAsList( if (playerScores != null) {
entrys: playerScores, await db.scoreEntryDao.addScore(
playerId: pid, entry: playerScores,
matchId: match.id, playerId: pid,
); matchId: match.id,
} );
}
if (match.winner != null) {
await db.scoreEntryDao.setWinner(
matchId: match.id,
playerId: match.winner!.id,
);
} }
}); });
} }
@@ -300,7 +290,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final group = await db.groupDao.getGroupById(groupId: groupId); final group = await db.groupDao.getGroupById(groupId: groupId);
final players = final players =
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
final winner = await db.scoreEntryDao.getWinner(matchId: row.id);
return Match( return Match(
id: row.id, id: row.id,
name: row.name, name: row.name,
@@ -310,7 +299,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
notes: row.notes ?? '', notes: row.notes ?? '',
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,
winner: winner,
); );
}), }),
); );

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
@@ -83,21 +84,21 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
} }
/// Retrieves all scores for a specific match. /// Retrieves all scores for a specific match.
Future<Map<String, List<ScoreEntry>>> getAllMatchScores({ Future<Map<String, ScoreEntry?>> getAllMatchScores({
required String matchId, required String matchId,
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId)); ..where((s) => s.matchId.equals(matchId));
final result = await query.get(); final result = await query.get();
final Map<String, List<ScoreEntry>> scoresByPlayer = {}; final Map<String, ScoreEntry?> scoresByPlayer = {};
for (final row in result) { for (final row in result) {
final score = ScoreEntry( final score = ScoreEntry(
roundNumber: row.roundNumber, roundNumber: row.roundNumber,
score: row.score, score: row.score,
change: row.change, change: row.change,
); );
scoresByPlayer.putIfAbsent(row.playerId, () => []).add(score); scoresByPlayer[row.playerId] = score;
} }
return scoresByPlayer; return scoresByPlayer;
@@ -237,10 +238,25 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
// Retrieves the winner of a match based on the highest score. // Retrieves the winner of a match based on the highest score.
Future<Player?> getWinner({required String matchId}) async { Future<Player?> 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) final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId)) ..where((s) => s.matchId.equals(matchId));
..orderBy([(s) => OrderingTerm.desc(s.score)])
..limit(1); // 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(); final result = await query.getSingleOrNull();
if (result == null) return null; if (result == null) return null;

View File

@@ -15,27 +15,25 @@ class Match {
final Group? group; final Group? group;
final List<Player> players; final List<Player> players;
final String notes; final String notes;
Map<String, List<ScoreEntry>> scores; Map<String, ScoreEntry?> scores;
Player? winner;
Match({ Match({
String? id,
DateTime? createdAt,
this.endedAt,
required this.name, required this.name,
required this.game, required this.game,
required this.players,
this.endedAt,
this.group, this.group,
this.players = const [],
this.notes = '', this.notes = '',
Map<String, List<ScoreEntry>>? scores, String? id,
this.winner, DateTime? createdAt,
Map<String, ScoreEntry?>? scores,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
scores = scores ?? {for (var player in players) player.id: []}; scores = scores ?? {for (Player p in players) p.id: null};
@override @override
String toString() { 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 /// Creates a Match instance from a JSON object where related objects are
@@ -71,10 +69,60 @@ class Match {
'gameId': game.id, 'gameId': game.id,
'groupId': group?.id, 'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(), 'playerIds': players.map((player) => player.id).toList(),
'scores': scores.map( 'scores': scores,
(playerId, scoreList) =>
MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()),
),
'notes': notes, 'notes': notes,
}; };
List<Player> 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<Player> _getPlayersWithHighestScore() {
if (players.isEmpty || scores.isEmpty) return [];
final int highestScore = players
.map((player) => scores[player.id]?.score)
.whereType<int>()
.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<Player> _getPlayersWithLowestScore() {
if (players.isEmpty || scores.values.every((score) => score == null)) {
return [];
}
final int lowestScore = players
.map((player) => scores[player.id]?.score)
.whereType<int>()
.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();
}
} }

View File

@@ -36,6 +36,7 @@ class GroupDetailView extends StatefulWidget {
} }
class _GroupDetailViewState extends State<GroupDetailView> { class _GroupDetailViewState extends State<GroupDetailView> {
late final AppLocalizations loc;
late final AppDatabase db; late final AppDatabase db;
bool isLoading = true; bool isLoading = true;
late Group _group; late Group _group;
@@ -51,13 +52,12 @@ class _GroupDetailViewState extends State<GroupDetailView> {
super.initState(); super.initState();
_group = widget.group; _group = widget.group;
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
loc = AppLocalizations.of(context);
_loadStatistics(); _loadStatistics();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
@@ -259,28 +259,36 @@ class _GroupDetailViewState extends State<GroupDetailView> {
/// Determines the best player in the group based on match wins /// Determines the best player in the group based on match wins
String _getBestPlayer(List<Match> matches) { String _getBestPlayer(List<Match> matches) {
final bestPlayerCounts = <Player, int>{}; final mvpCounts = <Player, int>{};
// Count wins for each player
for (var match in matches) { for (var match in matches) {
if (match.winner != null && final mvps = match.mvp;
_group.members.any((m) => m.id == match.winner?.id)) { for (final mvpPlayer in mvps) {
print(match.winner); if (_group.members.any((m) => m.id == mvpPlayer.id)) {
bestPlayerCounts.update( mvpCounts.update(mvpPlayer, (value) => value + 1, ifAbsent: () => 1);
match.winner!, }
(value) => value + 1,
ifAbsent: () => 1,
);
} }
} }
// Sort players by win count final sortedMvps = mvpCounts.entries.toList()
final sortedPlayers = bestPlayerCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value)); ..sort((a, b) => b.value.compareTo(a.value));
// Get the best player if (sortedMvps.isEmpty) {
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-'; 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( Match(
name: 'Skeleton Match', name: 'Skeleton Match',
game: Game( game: Game(
name: '', name: 'Skeleton Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: 'This is a skeleton game description.',
color: GameColor.blue, color: GameColor.blue,
icon: '', icon: '',
), ),
group: Group( group: Group(
name: 'Skeleton Group', name: 'Skeleton Group',
description: '', description: 'This is a skeleton group description.',
members: [ members: [
Player(name: 'Skeleton Player 1', description: ''), Player(name: 'Skeleton Player 1', description: ''),
Player(name: 'Skeleton Player 2', 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); final matchIndex = recentMatches.indexWhere((match) => match.id == matchId);
if (matchIndex != -1) { if (matchIndex != -1) {
setState(() { setState(() {
recentMatches[matchIndex].winner = winner; // TODO: fix
//recentMatches[matchIndex].winner = winner;
}); });
} }
} }

View File

@@ -277,7 +277,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
game: tempGame, game: tempGame,
winner: widget.matchToEdit!.winner,
createdAt: widget.matchToEdit!.createdAt, createdAt: widget.matchToEdit!.createdAt,
endedAt: widget.matchToEdit!.endedAt, endedAt: widget.matchToEdit!.endedAt,
notes: widget.matchToEdit!.notes, notes: widget.matchToEdit!.notes,
@@ -314,9 +313,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
matchId: widget.matchToEdit!.id, matchId: widget.matchToEdit!.id,
playerId: player.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, text: loc.enter_results,
icon: Icons.emoji_events, icon: Icons.emoji_events,
onPressed: () async { onPressed: () async {
match.winner = await Navigator.push( await Navigator.push(
context, context,
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, fullscreenDialog: true,
@@ -237,54 +237,29 @@ class _MatchDetailViewState extends State<MatchDetailView> {
} }
/// Returns the widget to be displayed in the result [InfoTile] /// Returns the widget to be displayed in the result [InfoTile]
/// TODO: Update when score logic is overhauled
Widget getResultWidget(AppLocalizations loc) { Widget getResultWidget(AppLocalizations loc) {
if (isSingleRowResult()) { if (isSingleRowResult()) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getResultRow(loc), children: getSingleResultRow(loc),
); );
} else { } else {
return Column( return getScoreResultWidget(loc);
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,
),
),
],
),
],
);
} }
} }
/// Returns the result row for single winner/loser rulesets or a placeholder /// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet /// if no result is entered yet
/// TODO: Update when score logic is overhauled List<Widget> getSingleResultRow(AppLocalizations loc) {
List<Widget> getResultRow(AppLocalizations loc) { // Single Winner
if (match.winner != null && match.game.ruleset == Ruleset.singleWinner) { if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) {
return [ return [
Text( Text(
loc.winner, loc.winner,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
), ),
Text( Text(
match.winner!.name, match.mvp.first.name,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -292,6 +267,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
), ),
]; ];
// Single Loser
} else if (match.game.ruleset == Ruleset.singleLoser) { } else if (match.game.ruleset == Ruleset.singleLoser) {
return [ return [
Text( Text(
@@ -299,7 +275,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
), ),
Text( Text(
match.winner!.name, match.mvp.first.name,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -307,6 +283,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
), ),
), ),
]; ];
// No result entered yet
} else { } else {
return [ return [
Text( 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 // Returns if the result can be displayed in a single row
bool isSingleRowResult() { bool isSingleRowResult() {
return match.game.ruleset == Ruleset.singleWinner || 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/db/database.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.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/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.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 /// A view that allows selecting and saving the winner of a match
/// [match]: The match for which the winner is to be selected /// [match]: The match for which the winner is to be selected
/// [onWinnerChanged]: Optional callback invoked when the winner is changed /// [onWinnerChanged]: Optional callback invoked when the winner is changed
const MatchResultView({ const MatchResultView({super.key, required this.match, this.onWinnerChanged});
super.key,
required this.match,
this.ruleset = Ruleset.singleWinner,
this.onWinnerChanged,
});
/// The match for which the winner is to be selected /// The match for which the winner is to be selected
final Match match; 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 /// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
@@ -38,6 +30,8 @@ class MatchResultView extends StatefulWidget {
class _MatchResultViewState extends State<MatchResultView> { class _MatchResultViewState extends State<MatchResultView> {
late final AppDatabase db; late final AppDatabase db;
late final Ruleset ruleset;
/// List of all players who participated in the match /// List of all players who participated in the match
late final List<Player> allPlayers; late final List<Player> allPlayers;
@@ -51,6 +45,8 @@ class _MatchResultViewState extends State<MatchResultView> {
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
ruleset = widget.match.game.ruleset;
allPlayers = widget.match.players; allPlayers = widget.match.players;
allPlayers.sort((a, b) => a.name.compareTo(b.name)); allPlayers.sort((a, b) => a.name.compareTo(b.name));
@@ -59,13 +55,17 @@ class _MatchResultViewState extends State<MatchResultView> {
(index) => TextEditingController(), (index) => TextEditingController(),
); );
if (widget.match.winner != null) { if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) { if (rulesetSupportsWinnerSelection()) {
_selectedPlayer = allPlayers.firstWhere( _selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.winner!.id, (p) => p.id == widget.match.mvp.first.id,
); );
} else if (rulesetSupportsScoreEntry()) { } 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(); super.initState();
@@ -154,7 +154,6 @@ class _MatchResultViewState extends State<MatchResultView> {
child: ListView.separated( child: ListView.separated(
itemCount: allPlayers.length, itemCount: allPlayers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
print(allPlayers[index].name);
return ScoreListTile( return ScoreListTile(
text: allPlayers[index].name, text: allPlayers[index].name,
controller: controller[index], controller: controller[index],
@@ -176,6 +175,11 @@ class _MatchResultViewState extends State<MatchResultView> {
text: loc.save_changes, text: loc.save_changes,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
onPressed: () async { onPressed: () async {
final ending = DateTime.now();
await db.matchDao.updateMatchEndedAt(
matchId: widget.match.id,
endedAt: ending,
);
await _handleSaving(); await _handleSaving();
if (!context.mounted) return; if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer); Navigator.of(context).pop(_selectedPlayer);
@@ -190,12 +194,12 @@ class _MatchResultViewState extends State<MatchResultView> {
/// Handles saving or removing the winner in the database /// Handles saving or removing the winner in the database
/// based on the current selection. /// based on the current selection.
Future<void> _handleSaving() async { Future<void> _handleSaving() async {
if (widget.ruleset == Ruleset.singleWinner) { if (ruleset == Ruleset.singleWinner) {
await _handleWinner(); await _handleWinner();
} else if (widget.ruleset == Ruleset.singleLoser) { } else if (ruleset == Ruleset.singleLoser) {
await _handleLoser(); await _handleLoser();
} else if (widget.ruleset == Ruleset.lowestScore || } else if (ruleset == Ruleset.lowestScore ||
widget.ruleset == Ruleset.highestScore) { ruleset == Ruleset.highestScore) {
await _handleScores(); await _handleScores();
} }
@@ -204,9 +208,9 @@ class _MatchResultViewState extends State<MatchResultView> {
Future<bool> _handleWinner() async { Future<bool> _handleWinner() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
await db.scoreEntryDao.removeWinner(matchId: widget.match.id); return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else { } else {
await db.scoreEntryDao.setWinner( return await db.scoreEntryDao.setWinner(
matchId: widget.match.id, matchId: widget.match.id,
playerId: _selectedPlayer!.id, playerId: _selectedPlayer!.id,
); );
@@ -215,33 +219,33 @@ class _MatchResultViewState extends State<MatchResultView> {
Future<bool> _handleLoser() async { Future<bool> _handleLoser() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
/// TODO: Update when score logic is overhauled return await db.scoreEntryDao.removeLooser(matchId: widget.match.id);
return false;
} else { } else {
/// TODO: Update when score logic is overhauled return await db.scoreEntryDao.setLooser(
return false; matchId: widget.match.id,
playerId: _selectedPlayer!.id,
);
} }
} }
/// Handles saving the scores for each player in the database. /// 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++) { for (int i = 0; i < allPlayers.length; i++) {
var text = controller[i].text; var text = controller[i].text;
if (text.isEmpty) { if (text.isEmpty) {
text = '0'; text = '0';
} }
final score = int.parse(text); final score = int.parse(text);
await db.playerMatchDao.updatePlayerScore( await db.scoreEntryDao.addScore(
matchId: widget.match.id, matchId: widget.match.id,
playerId: allPlayers[i].id, playerId: allPlayers[i].id,
newScore: score, entry: ScoreEntry(roundNumber: 0, score: score, change: 0),
); );
} }
return false;
} }
String getTitleForRuleset(AppLocalizations loc) { String getTitleForRuleset(AppLocalizations loc) {
switch (widget.ruleset) { switch (ruleset) {
case Ruleset.singleWinner: case Ruleset.singleWinner:
return loc.select_winner; return loc.select_winner;
case Ruleset.singleLoser: case Ruleset.singleLoser:
@@ -252,12 +256,10 @@ class _MatchResultViewState extends State<MatchResultView> {
} }
bool rulesetSupportsWinnerSelection() { bool rulesetSupportsWinnerSelection() {
return widget.ruleset == Ruleset.singleWinner || return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser;
widget.ruleset == Ruleset.singleLoser;
} }
bool rulesetSupportsScoreEntry() { bool rulesetSupportsScoreEntry() {
return widget.ruleset == Ruleset.lowestScore || return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
widget.ruleset == Ruleset.highestScore;
} }
} }

View File

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

View File

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

View File

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

View File

@@ -71,20 +71,7 @@ class DataTransferService {
'gameId': m.game.id, 'gameId': m.game.id,
'groupId': m.group?.id, 'groupId': m.group?.id,
'playerIds': m.players.map((p) => p.id).toList(), 'playerIds': m.players.map((p) => p.id).toList(),
'scores': m.scores.map( 'scores': m.scores,
(playerId, scores) => MapEntry(
playerId,
scores
.map(
(s) => {
'roundNumber': s.roundNumber,
'score': s.score,
'change': s.change,
},
)
.toList(),
),
),
'notes': m.notes, 'notes': m.notes,
}, },
) )

View File

@@ -63,7 +63,6 @@ void main() {
game: testGame, game: testGame,
group: testGroup1, group: testGroup1,
players: [testPlayer4, testPlayer5], players: [testPlayer4, testPlayer5],
winner: testPlayer4,
notes: '', notes: '',
); );
testMatch2 = Match( testMatch2 = Match(
@@ -71,20 +70,19 @@ void main() {
game: testGame, game: testGame,
group: testGroup2, group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3], players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer2,
notes: '', notes: '',
); );
testMatchOnlyPlayers = Match( testMatchOnlyPlayers = Match(
name: 'Test Match with Players', name: 'Test Match with Players',
game: testGame, game: testGame,
players: [testPlayer1, testPlayer2, testPlayer3], players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer3,
notes: '', notes: '',
); );
testMatchOnlyGroup = Match( testMatchOnlyGroup = Match(
name: 'Test Match with Group', name: 'Test Match with Group',
game: testGame, game: testGame,
group: testGroup2, group: testGroup2,
players: testGroup2.members,
notes: '', notes: '',
); );
}); });
@@ -289,8 +287,8 @@ void main() {
matchId: testMatch1.id, matchId: testMatch1.id,
); );
expect(fetchedMatch.winner, isNotNull); expect(fetchedMatch.mvp, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer4.id); expect(fetchedMatch.mvp.first.id, testPlayer4.id);
}); });
test('Setting a winner works correctly', () async { test('Setting a winner works correctly', () async {
@@ -304,8 +302,8 @@ void main() {
final fetchedMatch = await database.matchDao.getMatchById( final fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id, matchId: testMatch1.id,
); );
expect(fetchedMatch.winner, isNotNull); expect(fetchedMatch.mvp, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer5.id); expect(fetchedMatch.mvp.first.id, testPlayer5.id);
}); });
test( test(

View File

@@ -343,8 +343,16 @@ void main() {
// Verifies that teams with overlapping members are independent. // Verifies that teams with overlapping members are independent.
test('Teams with overlapping members are independent', () async { test('Teams with overlapping members are independent', () async {
// Create two matches since player_match has primary key {playerId, matchId} // Create two matches since player_match has primary key {playerId, matchId}
final match1 = Match(name: 'Match 1', game: testGame1, notes: ''); final match1 = Match(
final match2 = Match(name: 'Match 2', game: testGame2, notes: ''); 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: match1);
await database.matchDao.addMatch(match: match2); await database.matchDao.addMatch(match: match2);

View File

@@ -58,6 +58,7 @@ void main() {
testMatchOnlyGroup = Match( testMatchOnlyGroup = Match(
name: 'Test Match with Group', name: 'Test Match with Group',
game: testGame, game: testGame,
players: testGroup.members,
group: testGroup, group: testGroup,
notes: '', notes: '',
); );

View File

@@ -231,8 +231,8 @@ void main() {
); );
expect(scores.length, 2); expect(scores.length, 2);
expect(scores[testPlayer1.id]!.length, 2); expect(scores[testPlayer1.id]!, isNotNull);
expect(scores[testPlayer2.id]!.length, 1); expect(scores[testPlayer2.id]!, isNotNull);
}); });
test('getAllMatchScores() with no scores saved', () async { test('getAllMatchScores() with no scores saved', () async {

View File

@@ -64,14 +64,8 @@ void main() {
players: [testPlayer1, testPlayer2], players: [testPlayer1, testPlayer2],
notes: 'Test notes', notes: 'Test notes',
scores: { scores: {
testPlayer1.id: [ testPlayer1.id: ScoreEntry(roundNumber: 1, score: 10, change: 10),
ScoreEntry(roundNumber: 1, score: 10, change: 10), testPlayer2.id: ScoreEntry(roundNumber: 1, score: 15, change: 15),
ScoreEntry(roundNumber: 2, score: 20, change: 10),
],
testPlayer2.id: [
ScoreEntry(roundNumber: 1, score: 15, change: 15),
ScoreEntry(roundNumber: 2, score: 25, change: 10),
],
}, },
); );
}); });