Skip to content

Commit 92de7cf

Browse files
committed
Add game state restoration feature with undo functionality
1 parent b6e5723 commit 92de7cf

16 files changed

Lines changed: 289 additions & 67 deletions

lib/app/view/app.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,33 @@ class App extends StatelessWidget {
2020
required AppConfigRepository appConfigRepository,
2121
required UserRepository userRepository,
2222
required ScryfallRepository scryfallRepository,
23+
required PlayerRepository playerRepository,
2324
required User user,
2425
super.key,
2526
}) : _appConfigRepository = appConfigRepository,
2627
_userRepository = userRepository,
2728
_scryfallRepository = scryfallRepository,
2829
_firebaseDatabaseRepository = firebaseDatabaseRepository,
30+
_playerRepository = playerRepository,
2931
_user = user;
3032

3133
final FirebaseDatabaseRepository _firebaseDatabaseRepository;
3234
final ScryfallRepository _scryfallRepository;
3335
final AppConfigRepository _appConfigRepository;
36+
final PlayerRepository _playerRepository;
3437
final UserRepository _userRepository;
3538

3639
final User _user;
3740

3841
@override
3942
Widget build(BuildContext context) {
40-
final playerRepository = PlayerRepository();
41-
4243
return MultiRepositoryProvider(
4344
providers: [
4445
RepositoryProvider.value(value: _appConfigRepository),
4546
RepositoryProvider.value(value: _scryfallRepository),
4647
RepositoryProvider.value(value: _firebaseDatabaseRepository),
4748
RepositoryProvider.value(value: _userRepository),
48-
RepositoryProvider.value(value: playerRepository),
49+
RepositoryProvider.value(value: _playerRepository),
4950
RepositoryProvider.value(value: _user),
5051
],
5152
child: MultiBlocProvider(
@@ -59,7 +60,7 @@ class App extends StatelessWidget {
5960
),
6061
BlocProvider(
6162
create: (context) => GameBloc(
62-
playerRepository: playerRepository,
63+
playerRepository: _playerRepository,
6364
database: context.read<FirebaseDatabaseRepository>(),
6465
),
6566
),

lib/game/bloc/game_bloc.dart

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ part 'game_event.dart';
1212
part 'game_state.dart';
1313

1414
class GameBloc extends Bloc<GameEvent, GameState> {
15+
// ... existing constructor and fields ...
1516
GameBloc({
1617
required PlayerRepository playerRepository,
1718
required FirebaseDatabaseRepository database,
@@ -26,6 +27,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
2627
on<GameFinishEvent>(_onGameFinish);
2728
on<GameUpdateTimerEvent>(_onUpdateTimer);
2829
on<PlayerRepositoryUpdateEvent>(_repositoryUpdated);
30+
on<GameRestoreRequested>(_onGameRestoreRequested);
2931

3032
// Subscribe to player updates and emit them immediately
3133
_playerRepository.players.listen((players) {
@@ -189,24 +191,36 @@ class GameBloc extends Bloc<GameEvent, GameState> {
189191
emit(state.copyWith(elapsedSeconds: event.gameLength));
190192
}
191193

194+
Future<void> _onGameRestoreRequested(
195+
GameRestoreRequested event,
196+
Emitter<GameState> emit,
197+
) async {
198+
final restored = _playerRepository.restorePreviousGameState();
199+
if (restored) {
200+
// Emit restored state as running, clear winner/error
201+
emit(state.copyWith(
202+
status: GameStatus.running,
203+
playerList: _playerRepository.getPlayers(),
204+
winner: null,
205+
error: null,
206+
));
207+
// Dispatch TimerStartEvent to TimerBloc
208+
// (Assumes context or a callback is available; see note below)
209+
// context.read<TimerBloc>().add(const TimerStartEvent());
210+
// If context is not available here, trigger from UI after BlocListener
211+
}
212+
// else do nothing if no snapshot
213+
}
214+
192215
Future<void> _onGameFinish(
193216
GameFinishEvent event,
194217
Emitter<GameState> emit,
195218
) async {
196219
emit(state.copyWith(status: GameStatus.finished));
197220

198-
final updateWinner = event.winner.copyWith(
199-
placement: const Value(1),
200-
lifePoints: 0,
201-
state: PlayerModelState.eliminated,
202-
timeOfDeath: Value(DateTime.now().millisecondsSinceEpoch),
203-
);
204-
205-
_playerRepository.updatePlayer(updateWinner);
206-
207221
final gameModel = GameModel(
208222
roomId: await generateShortGameId(),
209-
winnerId: updateWinner.id,
223+
winnerId: event.winner.id,
210224
players: state.playerList,
211225
startTime: state.startTime ?? DateTime.now(),
212226
endTime: DateTime.now(),
@@ -228,8 +242,9 @@ class GameBloc extends Bloc<GameEvent, GameState> {
228242
// Then check for game-ending condition
229243
final alivePlayers =
230244
event.players.where((p) => p.state == PlayerModelState.active).toList();
231-
if (alivePlayers.length <= 1 && state.status == GameStatus.running) {
232-
add(GameFinishEvent(winner: alivePlayers.first));
245+
if (alivePlayers.isEmpty && state.status == GameStatus.running) {
246+
add(GameFinishEvent(
247+
winner: event.players.firstWhere((p) => p.placement == 1)));
233248
}
234249
}
235250

lib/game/bloc/game_event.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ abstract class GameEvent extends Equatable {
77
List<Object?> get props => [];
88
}
99

10+
/// Event to request restoring the previous game state (undo game over)
11+
class GameRestoreRequested extends GameEvent {
12+
const GameRestoreRequested();
13+
14+
@override
15+
List<Object?> get props => [];
16+
}
17+
1018
final class CreateGameEvent extends GameEvent {
1119
const CreateGameEvent({
1220
required this.numberOfPlayers,

lib/l10n/arb/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,14 @@
596596
"placeholders": {
597597
"error": {}
598598
}
599+
},
600+
"undoGameOverButtonLabel": "Undo / Restore",
601+
"@undoGameOverButtonLabel": {
602+
"description": "Button label for restoring the previous game state on the Game Over page"
603+
},
604+
"gameRestoredMessage": "Previous game restored!",
605+
"@gameRestoredMessage": {
606+
"description": "Snackbar message shown when a previous game state is successfully restored from the Game Over page"
599607
}
600608
}
601609

lib/l10n/arb/app_es.arb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,13 @@
1010
"gameModeTitle": "Modo de Juego",
1111
"statsTitle": "Tus Estadísticas",
1212
"matchHistoryLoadError": "Error al cargar el historial de partidas",
13-
"noMatchHistoryAvailable": "No hay historial de partidas disponible"
13+
"noMatchHistoryAvailable": "No hay historial de partidas disponible",
14+
"undoGameOverButtonLabel": "Deshacer / Restaurar",
15+
"@undoGameOverButtonLabel": {
16+
"description": "Etiqueta del botón para restaurar el estado previo del juego en la página de Fin de Juego"
17+
},
18+
"gameRestoredMessage": "¡Juego anterior restaurado!",
19+
"@gameRestoredMessage": {
20+
"description": "Mensaje de snackbar mostrado cuando se restaura correctamente un estado previo del juego desde la página de Fin de Juego"
21+
}
1422
}

lib/l10n/arb/app_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,18 @@ abstract class AppLocalizations {
688688
/// In en, this message translates to:
689689
/// **'Error: {error}'**
690690
String errorSnackbarMessage(Object error);
691+
692+
/// Button label for restoring the previous game state on the Game Over page
693+
///
694+
/// In en, this message translates to:
695+
/// **'Undo / Restore'**
696+
String get undoGameOverButtonLabel;
697+
698+
/// Snackbar message shown when a previous game state is successfully restored from the Game Over page
699+
///
700+
/// In en, this message translates to:
701+
/// **'Previous game restored!'**
702+
String get gameRestoredMessage;
691703
}
692704

693705
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {

lib/l10n/arb/app_localizations_en.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,10 @@ class AppLocalizationsEn extends AppLocalizations {
322322
String errorSnackbarMessage(Object error) {
323323
return 'Error: $error';
324324
}
325+
326+
@override
327+
String get undoGameOverButtonLabel => 'Undo / Restore';
328+
329+
@override
330+
String get gameRestoredMessage => 'Previous game restored!';
325331
}

lib/l10n/arb/app_localizations_es.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,10 @@ class AppLocalizationsEs extends AppLocalizations {
322322
String errorSnackbarMessage(Object error) {
323323
return 'Error: $error';
324324
}
325+
326+
@override
327+
String get undoGameOverButtonLabel => 'Deshacer / Restaurar';
328+
329+
@override
330+
String get gameRestoredMessage => '¡Juego anterior restaurado!';
325331
}

lib/life_counter/view/game_over_page.dart

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class GameOverView extends StatelessWidget {
4848

4949
final gameModel = context.watch<GameBloc>().state.gameModel;
5050
if (gameModel == null) return const CircularProgressIndicator();
51-
51+
// Restore/Undo button (only if canRestoreGame is true)
52+
final canRestoreGame = context.read<PlayerRepository>().canRestoreGame;
5253
return Scaffold(
5354
appBar: AppBar(
5455
title: Text(l10n.gameOverTitle),
@@ -66,44 +67,78 @@ class GameOverView extends StatelessWidget {
6667
final players = state.standings;
6768
final winner = players.first;
6869

69-
return CustomScrollView(
70-
slivers: [
71-
SliverPadding(
72-
padding: const EdgeInsets.all(16),
73-
sliver: SliverList(
74-
delegate: SliverChildListDelegate([
75-
Padding(
76-
padding: const EdgeInsets.all(16),
77-
child: Column(
78-
crossAxisAlignment: CrossAxisAlignment.start,
79-
children: [
80-
Text(
81-
l10n.matchOverview,
82-
style: Theme.of(context).textTheme.headlineSmall,
83-
),
84-
const SizedBox(height: 16),
85-
WinnerWidget(
86-
winner: winner,
87-
),
88-
],
89-
),
70+
return Column(
71+
children: [
72+
if (canRestoreGame)
73+
Padding(
74+
padding: const EdgeInsets.all(16.0),
75+
child: FilledButton.icon(
76+
icon: const Icon(Icons.undo, color: Colors.white),
77+
label: Text(l10n.undoGameOverButtonLabel),
78+
style: FilledButton.styleFrom(
79+
backgroundColor: Theme.of(context).colorScheme.primary,
80+
foregroundColor: Theme.of(context).colorScheme.onPrimary,
81+
minimumSize: const Size.fromHeight(48),
82+
textStyle: Theme.of(context).textTheme.titleMedium,
9083
),
91-
const SizedBox(height: 16),
92-
Row(
93-
crossAxisAlignment: CrossAxisAlignment.start,
94-
children: [
95-
DraggableStandingsWidget(
96-
gameOverState: state,
97-
),
98-
QuestionWidget(
99-
gameOverState: state,
100-
players: players,
84+
onPressed: () {
85+
context
86+
.read<GameBloc>()
87+
.add(const GameRestoreRequested());
88+
context.read<TimerBloc>().add(const TimerStartEvent());
89+
// Optionally show a snackbar or navigate
90+
ScaffoldMessenger.of(context).showSnackBar(
91+
SnackBar(content: Text(l10n.gameRestoredMessage)),
92+
);
93+
},
94+
),
95+
),
96+
Expanded(
97+
child: CustomScrollView(
98+
slivers: [
99+
SliverPadding(
100+
padding: const EdgeInsets.all(16),
101+
sliver: SliverList(
102+
delegate: SliverChildListDelegate(
103+
[
104+
Padding(
105+
padding: const EdgeInsets.all(16),
106+
child: Column(
107+
crossAxisAlignment: CrossAxisAlignment.start,
108+
children: [
109+
Text(
110+
l10n.matchOverview,
111+
style: Theme.of(context)
112+
.textTheme
113+
.headlineSmall,
114+
),
115+
const SizedBox(height: 16),
116+
WinnerWidget(
117+
winner: winner,
118+
),
119+
],
120+
),
121+
),
122+
const SizedBox(height: 16),
123+
Row(
124+
crossAxisAlignment: CrossAxisAlignment.start,
125+
children: [
126+
DraggableStandingsWidget(
127+
gameOverState: state,
128+
),
129+
QuestionWidget(
130+
gameOverState: state,
131+
players: players,
132+
),
133+
],
134+
),
135+
const SizedBox(height: 16),
136+
const ButtonsWidget(),
137+
],
101138
),
102-
],
139+
),
103140
),
104-
const SizedBox(height: 16),
105-
const ButtonsWidget(),
106-
]),
141+
],
107142
),
108143
),
109144
],

lib/life_counter/widgets/life_counter_widget.dart

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -216,18 +216,18 @@ class _DecrementLifeWidgetState extends State<DecrementLifeWidget> {
216216
onLongPressEnd: (_) => context.read<PlayerBloc>().add(
217217
const PlayerStopDecrement(),
218218
),
219-
child: Padding(
220-
padding: isPhone ? const EdgeInsets.all(8) : const EdgeInsets.all(64),
221-
child: AnimatedContainer(
222-
decoration: BoxDecoration(
223-
color:
224-
_isTapped ? Colors.white.withAlpha(32) : Colors.transparent,
225-
borderRadius: const BorderRadius.all(
226-
Radius.circular(20),
227-
),
219+
child: AnimatedContainer(
220+
decoration: BoxDecoration(
221+
color: _isTapped ? Colors.white.withAlpha(32) : Colors.transparent,
222+
borderRadius: const BorderRadius.all(
223+
Radius.circular(20),
228224
),
229-
duration: const Duration(milliseconds: 50),
230-
child: SizedBox.expand(
225+
),
226+
duration: const Duration(milliseconds: 50),
227+
child: SizedBox.expand(
228+
child: Padding(
229+
padding:
230+
isPhone ? const EdgeInsets.all(8) : const EdgeInsets.all(64),
231231
child: Row(
232232
children: [
233233
const FaIcon(FontAwesomeIcons.minus, size: 36),

0 commit comments

Comments
 (0)