First version of retrieving a gamesession from the db

This commit is contained in:
2025-08-21 22:30:13 +02:00
parent 4901e27b90
commit 7a420e909a
28 changed files with 868 additions and 1175 deletions

View File

@@ -1,10 +1,39 @@
import 'package:cabo_counter/data/db/database.dart';
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
import 'package:cabo_counter/data/dto/game_session.dart';
import 'package:cabo_counter/data/dto/player.dart';
import 'package:cabo_counter/data/dto/round.dart';
import 'package:drift/drift.dart';
part 'game_session_dao.g.dart';
@DriftAccessor(tables: [])
@DriftAccessor(tables: [GameSessionTable])
class GameSessionDao extends DatabaseAccessor<AppDatabase>
with _$GameSessionDaoMixin {
GameSessionDao(super.db);
/// Retrieves a game session by its ID.
Future<GameSession> getGameSession(String id) async {
final query = select(gameSessionTable)..where((tbl) => tbl.id.equals(id));
final gameSessionResult = await query.getSingle();
List<Player> playerList = await db.playerDao.getPlayersByGameId(id);
List<Round> roundList = await db.roundsDao.getRoundsByGameId(id);
GameSession gameSession = GameSession(
id: gameSessionResult.id,
createdAt: gameSessionResult.createdAt,
gameTitle: gameSessionResult.gameTitle,
players: playerList.map((player) => player.name).toList(),
pointLimit: gameSessionResult.pointLimit,
caboPenalty: gameSessionResult.caboPenalty,
isPointsLimitEnabled: gameSessionResult.isPointsLimitEnabled,
isGameFinished: gameSessionResult.isGameFinished,
winner: gameSessionResult.winner ?? '',
roundNumber: gameSessionResult.roundNumber,
playerScores: playerList.map((player) => player.totalScore).toList(),
roundList: roundList);
return gameSession;
}
}

View File

@@ -3,4 +3,7 @@
part of 'game_session_dao.dart';
// ignore_for_file: type=lint
mixin _$GameSessionDaoMixin on DatabaseAccessor<AppDatabase> {}
mixin _$GameSessionDaoMixin on DatabaseAccessor<AppDatabase> {
$GameSessionTableTable get gameSessionTable =>
attachedDatabase.gameSessionTable;
}

View File

@@ -0,0 +1,38 @@
import 'package:cabo_counter/data/db/database.dart';
import 'package:cabo_counter/data/db/tables/player_table.dart';
import 'package:cabo_counter/data/dto/player.dart';
import 'package:drift/drift.dart';
part 'player_dao.g.dart';
@DriftAccessor(tables: [PlayerTable])
class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
PlayerDao(super.db);
/// Retrieves all players from a game by gameId
Future<List<Player>> getPlayersByGameId(String gameId) async {
final query = select(playerTable)
..where((tbl) => tbl.gameId.equals(gameId));
final playerResults = await query.get();
return playerResults.map((row) {
return Player(
playerId: row.playerId,
gameId: row.gameId,
name: row.name,
position: row.position,
totalScore: row.totalScore,
);
}).toList()
..sort((a, b) => a.position.compareTo(b.position));
}
/// Retrieves a players position by its id
Future<int> getPositionByPlayerId(String playerId) async {
final query = select(playerTable)
..where((tbl) => tbl.playerId.equals(playerId));
final result = await query.getSingle();
return result.position;
}
}

View File

@@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayerDaoMixin on DatabaseAccessor<AppDatabase> {
$GameSessionTableTable get gameSessionTable =>
attachedDatabase.gameSessionTable;
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
}

View File

@@ -1,10 +0,0 @@
import 'package:cabo_counter/data/db/database.dart';
import 'package:drift/drift.dart';
part 'player_scores_dao.g.dart';
@DriftAccessor(tables: [])
class PlayerScoresDao extends DatabaseAccessor<AppDatabase>
with _$PlayerScoresDaoMixin {
PlayerScoresDao(super.db);
}

View File

@@ -1,6 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_scores_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayerScoresDaoMixin on DatabaseAccessor<AppDatabase> {}

View File

@@ -1,9 +0,0 @@
import 'package:cabo_counter/data/db/database.dart';
import 'package:drift/drift.dart';
part 'players_dao.g.dart';
@DriftAccessor(tables: [])
class PlayersDao extends DatabaseAccessor<AppDatabase> with _$PlayersDaoMixin {
PlayersDao(super.db);
}

View File

@@ -1,6 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'players_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayersDaoMixin on DatabaseAccessor<AppDatabase> {}

View File

@@ -1,10 +1,51 @@
import 'package:cabo_counter/data/db/database.dart';
import 'package:cabo_counter/data/db/tables/round_scores_table.dart';
import 'package:cabo_counter/data/dto/round_score.dart';
import 'package:drift/drift.dart';
part 'round_scores_dao.g.dart';
@DriftAccessor(tables: [])
@DriftAccessor(tables: [RoundScoresTable])
class RoundScoresDao extends DatabaseAccessor<AppDatabase>
with _$RoundScoresDaoMixin {
RoundScoresDao(super.db);
/// Retrieves all scores for a specific round by its ID.
Future<List<RoundScore>> getRoundScoresByRoundId(String roundId) async {
final query = select(roundScoresTable)
..where((tbl) => tbl.roundId.equals(roundId));
final result = await query.get();
// Get positions for each player
final scoresWithPosition = await Future.wait(result.map((row) async {
final position = await db.playerDao.getPositionByPlayerId(row.playerId);
return MapEntry(row, position);
}));
// Sort rows by position
scoresWithPosition.sort((a, b) => a.value.compareTo(b.value));
return scoresWithPosition.map((entry) {
final row = entry.key;
return RoundScore(
roundId: roundId,
playerId: row.playerId,
score: row.score,
scoreUpdate: row.scoreUpdate,
);
}).toList();
}
Future<List<int>> getScoresByRoundId(String roundId) async {
List<RoundScore> roundScores = await getRoundScoresByRoundId(roundId);
return roundScores.map((score) => score.score).toList();
}
Future<List<int>> getScoreUpdatesByRoundId(String roundId) async {
List<RoundScore> roundScores = await getRoundScoresByRoundId(roundId);
return roundScores.map((score) => score.scoreUpdate).toList();
}
}

View File

@@ -3,4 +3,10 @@
part of 'round_scores_dao.dart';
// ignore_for_file: type=lint
mixin _$RoundScoresDaoMixin on DatabaseAccessor<AppDatabase> {}
mixin _$RoundScoresDaoMixin on DatabaseAccessor<AppDatabase> {
$GameSessionTableTable get gameSessionTable =>
attachedDatabase.gameSessionTable;
$RoundsTableTable get roundsTable => attachedDatabase.roundsTable;
$RoundScoresTableTable get roundScoresTable =>
attachedDatabase.roundScoresTable;
}

View File

@@ -1,9 +1,39 @@
import 'package:cabo_counter/data/db/database.dart';
import 'package:cabo_counter/data/db/tables/rounds_table.dart';
import 'package:cabo_counter/data/dto/round.dart';
import 'package:drift/drift.dart';
part 'rounds_dao.g.dart';
@DriftAccessor(tables: [])
@DriftAccessor(tables: [RoundsTable])
class RoundsDao extends DatabaseAccessor<AppDatabase> with _$RoundsDaoMixin {
RoundsDao(super.db);
/// Retrieves all rounds for a specific game session by its ID.
Future<List<Round>> getRoundsByGameId(String gameId) async {
final query = select(roundsTable)
..where((tbl) => tbl.gameId.equals(gameId));
final roundResult = await query.get();
final roundList = await Future.wait(
roundResult.map((row) async {
final scores = await db.roundScoresDao.getScoresByRoundId(row.roundId);
final roundScores =
await db.roundScoresDao.getScoreUpdatesByRoundId(row.roundId);
return Round(
roundId: row.roundId,
gameId: row.gameId,
roundNum: row.roundNumber,
caboPlayerIndex: row.caboPlayerIndex,
kamikazePlayerIndex: row.kamikazePlayerIndex,
scores: scores,
scoreUpdates: roundScores,
);
}),
);
return roundList;
}
}

View File

@@ -3,4 +3,8 @@
part of 'rounds_dao.dart';
// ignore_for_file: type=lint
mixin _$RoundsDaoMixin on DatabaseAccessor<AppDatabase> {}
mixin _$RoundsDaoMixin on DatabaseAccessor<AppDatabase> {
$GameSessionTableTable get gameSessionTable =>
attachedDatabase.gameSessionTable;
$RoundsTableTable get roundsTable => attachedDatabase.roundsTable;
}

View File

@@ -1,6 +1,9 @@
import 'package:cabo_counter/data/db/dao/game_session_dao.dart';
import 'package:cabo_counter/data/db/dao/player_dao.dart';
import 'package:cabo_counter/data/db/dao/round_scores_dao.dart';
import 'package:cabo_counter/data/db/dao/rounds_dao.dart';
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
import 'package:cabo_counter/data/db/tables/player_scores_table.dart';
import 'package:cabo_counter/data/db/tables/players_table.dart';
import 'package:cabo_counter/data/db/tables/player_table.dart';
import 'package:cabo_counter/data/db/tables/round_scores_table.dart';
import 'package:cabo_counter/data/db/tables/rounds_table.dart';
import 'package:drift/drift.dart';
@@ -9,13 +12,9 @@ import 'package:path_provider/path_provider.dart';
part 'database.g.dart';
@DriftDatabase(tables: [
GameSessionTable,
PlayerScoresTable,
PlayersTable,
RoundScoresTable,
RoundsTable
])
@DriftDatabase(
tables: [GameSessionTable, PlayerTable, RoundScoresTable, RoundsTable],
daos: [GameSessionDao, PlayerDao, RoundsDao, RoundScoresDao])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
import 'package:cabo_counter/data/db/tables/players_table.dart';
import 'package:drift/drift.dart';
class PlayerScoresTable extends Table {
TextColumn get roundId =>
text().references(GameSessionTable, #id, onDelete: KeyAction.cascade)();
TextColumn get playerName => text().references(PlayersTable, #name)();
IntColumn get totalScore => integer()();
@override
Set<Column<Object>> get primaryKey => {roundId, playerName};
}

View File

@@ -1,9 +1,12 @@
import 'package:cabo_counter/data/db/tables/game_session_table.dart';
import 'package:drift/drift.dart';
class PlayersTable extends Table {
class PlayerTable extends Table {
TextColumn get playerId =>
text().references(GameSessionTable, #id, onDelete: KeyAction.cascade)();
TextColumn get gameId =>
text().references(GameSessionTable, #id, onDelete: KeyAction.cascade)();
IntColumn get totalScore => integer()();
IntColumn get position => integer()();
TextColumn get name => text()();
}

View File

@@ -3,11 +3,11 @@ import 'package:drift/drift.dart';
class RoundScoresTable extends Table {
TextColumn get roundId =>
text().references(RoundsTable, #id, onDelete: KeyAction.cascade)();
TextColumn get playerName => text()();
text().references(RoundsTable, #roundId, onDelete: KeyAction.cascade)();
TextColumn get playerId => text()();
IntColumn get score => integer()();
IntColumn get scoreUpdate => integer()();
@override
Set<Column<Object>> get primaryKey => {roundId, playerName};
Set<Column<Object>> get primaryKey => {roundId, playerId};
}

View File

@@ -6,7 +6,8 @@ class RoundsTable extends Table {
TextColumn get gameId =>
text().references(GameSessionTable, #id, onDelete: KeyAction.cascade)();
IntColumn get roundNumber => integer()();
TextColumn get kamikazePlayer => text().nullable()();
IntColumn get caboPlayerIndex => integer()();
IntColumn get kamikazePlayerIndex => integer().nullable()();
@override
Set<Column<Object>> get primaryKey => {roundId};

View File

@@ -20,21 +20,25 @@ class GameSession extends ChangeNotifier {
final int pointLimit;
final int caboPenalty;
final bool isPointsLimitEnabled;
bool isGameFinished = false;
String winner = '';
int roundNumber = 1;
late List<int> playerScores;
List<Round> roundList = [];
bool isGameFinished;
String winner;
int roundNumber;
List<int> playerScores;
List<Round> roundList;
GameSession({
required this.id,
required this.createdAt,
required this.gameTitle,
required this.players,
required this.pointLimit,
required this.caboPenalty,
required this.isPointsLimitEnabled,
}) {
GameSession(
{required this.id,
required this.createdAt,
required this.gameTitle,
required this.players,
required this.pointLimit,
required this.caboPenalty,
required this.isPointsLimitEnabled,
this.isGameFinished = false,
this.winner = '',
this.roundNumber = 1,
this.playerScores = const [],
this.roundList = const []}) {
playerScores = List.filled(players.length, 0);
}
@@ -207,6 +211,8 @@ class GameSession extends ChangeNotifier {
int? kamikazePlayerIndex,
]) {
Round newRound = Round(
roundId: const Uuid().v1(),
gameId: id,
roundNum: roundNum,
caboPlayerIndex: caboPlayerIndex,
kamikazePlayerIndex: kamikazePlayerIndex,

34
lib/data/dto/player.dart Normal file
View File

@@ -0,0 +1,34 @@
class Player {
final String playerId;
final String gameId;
final String name;
final int position;
int totalScore;
Player(
{required this.playerId,
required this.gameId,
required this.name,
required this.position,
this.totalScore = 0});
@override
String toString() {
return '(playerId: $playerId, gameId: $gameId, name: $name, position: $position)';
}
Map<String, dynamic> toJson() => {
'playerId': playerId,
'gameId': gameId,
'name': name,
'position': position,
'totalScore': totalScore
};
Player.fromJson(Map<String, dynamic> json)
: playerId = json['playerId'],
gameId = json['gameId'],
name = json['name'],
position = json['position'],
totalScore = json['totalScore'];
}

View File

@@ -6,6 +6,8 @@
/// [kamikazePlayerIndex] is the index of the player who got kamikaze. If no one got
/// kamikaze, this value is null.
class Round {
final String roundId;
final String gameId;
final int roundNum;
final int caboPlayerIndex;
final int? kamikazePlayerIndex;
@@ -13,6 +15,8 @@ class Round {
final List<int> scoreUpdates;
Round({
required this.roundId,
required this.gameId,
required this.roundNum,
required this.caboPlayerIndex,
this.kamikazePlayerIndex,
@@ -29,6 +33,8 @@ class Round {
/// Converts the Round object to a JSON map.
Map<String, dynamic> toJson() => {
'roundId': roundId,
'gameId': gameId,
'roundNum': roundNum,
'caboPlayerIndex': caboPlayerIndex,
'kamikazePlayerIndex': kamikazePlayerIndex,
@@ -38,7 +44,9 @@ class Round {
/// Creates a Round object from a JSON map.
Round.fromJson(Map<String, dynamic> json)
: roundNum = json['roundNum'],
: roundId = json['roundId'],
gameId = json['gameId'],
roundNum = json['roundNum'],
caboPlayerIndex = json['caboPlayerIndex'],
kamikazePlayerIndex = json['kamikazePlayerIndex'],
scores = List<int>.from(json['scores']),

View File

@@ -0,0 +1,12 @@
class RoundScore {
final String roundId;
final String playerId;
final int score;
final int scoreUpdate;
RoundScore(
{required this.roundId,
required this.playerId,
required this.score,
required this.scoreUpdate});
}

View File

@@ -63,7 +63,7 @@ import 'app_localizations_en.dart';
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
@@ -86,16 +86,16 @@ abstract class AppLocalizations {
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en')
Locale('en'),
];
/// No description provided for @app_name.
@@ -487,7 +487,11 @@ abstract class AppLocalizations {
/// In de, this message translates to:
/// **'{playerCount, plural, =1{{names} hat exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommt deshalb {bonusPoints} Punkte abgezogen!} other{{names} haben exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommen deshalb jeweils {bonusPoints} Punkte abgezogen!}}'**
String bonus_points_message(
int playerCount, String names, int pointLimit, int bonusPoints);
int playerCount,
String names,
int pointLimit,
int bonusPoints,
);
/// No description provided for @end_of_game_title.
///
@@ -775,8 +779,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View File

@@ -214,7 +214,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String bonus_points_message(
int playerCount, String names, int pointLimit, int bonusPoints) {
int playerCount,
String names,
int pointLimit,
int bonusPoints,
) {
String _temp0 = intl.Intl.pluralLogic(
playerCount,
locale: localeName,

View File

@@ -211,7 +211,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String bonus_points_message(
int playerCount, String names, int pointLimit, int bonusPoints) {
int playerCount,
String names,
int pointLimit,
int bonusPoints,
) {
String _temp0 = intl.Intl.pluralLogic(
playerCount,
locale: localeName,

View File

@@ -458,14 +458,14 @@ class _CreateGameViewState extends State<CreateGameView> {
bool isPointsLimitEnabled = gameMode == GameMode.pointLimit;
GameSession gameSession = GameSession(
id: id,
createdAt: DateTime.now(),
gameTitle: _gameTitleTextController.text,
players: players,
pointLimit: ConfigService.getPointLimit(),
caboPenalty: ConfigService.getCaboPenalty(),
isPointsLimitEnabled: isPointsLimitEnabled,
);
id: id,
createdAt: DateTime.now(),
gameTitle: _gameTitleTextController.text,
players: players,
pointLimit: ConfigService.getPointLimit(),
caboPenalty: ConfigService.getCaboPenalty(),
isPointsLimitEnabled: isPointsLimitEnabled,
isGameFinished: false);
gameManager.addGameSession(gameSession);
final session = gameManager.getGameSessionById(id) ?? gameSession;

View File

@@ -9,14 +9,14 @@ void main() {
setUp(() {
session = GameSession(
id: '1',
createdAt: testDate,
gameTitle: testTitle,
players: testPlayers,
pointLimit: 100,
caboPenalty: 5,
isPointsLimitEnabled: true,
);
id: '1',
createdAt: testDate,
gameTitle: testTitle,
players: testPlayers,
pointLimit: 100,
caboPenalty: 5,
isPointsLimitEnabled: true,
isGameFinished: false);
});
group('Initialization & JSON', () {

View File

@@ -11,6 +11,8 @@ void main() {
setUp(() {
round = Round(
roundId: 'testRoundId',
gameId: 'testGameId',
roundNum: testRoundNum,
caboPlayerIndex: testCaboPlayerIndex,
kamikazePlayerIndex: testKamikazePlayerIndex,
@@ -30,6 +32,8 @@ void main() {
test('Constructor with null kamikazePlayerIndex', () {
final roundWithoutKamikaze = Round(
roundId: 'testRoundId',
gameId: 'testGameId',
roundNum: testRoundNum,
caboPlayerIndex: testCaboPlayerIndex,
kamikazePlayerIndex: null,
@@ -98,6 +102,8 @@ void main() {
test('toString() with null kamikazePlayerIndex', () {
final roundWithoutKamikaze = Round(
roundId: 'testRoundId',
gameId: 'testGameId',
roundNum: testRoundNum,
caboPlayerIndex: testCaboPlayerIndex,
kamikazePlayerIndex: null,