Skip to content

Commit

Permalink
feat: reset game status (#425)
Browse files Browse the repository at this point in the history
* feat: update game status when resetting

* fix: add WordSelectionPage back in

* test: add tests

* feat: manually dismiss reset modal

* fix: add period to end of sentence

* test: add BoardInfoRepository tests

* chore: remove status check from BottomBar

* chore: remove resetComplete BoardStatus

* Update test/crossword/bloc/crossword_bloc_test.dart

Co-authored-by: Hugo Walbecq <hugo@verygood.ventures>

* fix: error handling and styling

* test: update description

---------

Co-authored-by: Hugo Walbecq <hugo@verygood.ventures>
  • Loading branch information
marwfair and B0berman authored May 8, 2024
1 parent 0a6d784 commit 123b533
Show file tree
Hide file tree
Showing 13 changed files with 546 additions and 36 deletions.
38 changes: 38 additions & 0 deletions lib/crossword/bloc/crossword_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
on<BoardLoadingInformationRequested>(_onBoardLoadingInformationRequested);
on<LoadedSectionsSuspended>(_onLoadedSectionsSuspended);
on<BoardSectionLoaded>(_onBoardSectionLoaded);
on<GameStatusRequested>(_onGameStatusRequested);
on<BoardStatusResumed>(_onBoardStatusResumed);
on<MascotDropped>(_onMascotDropped);
}

Expand Down Expand Up @@ -203,6 +205,42 @@ class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
}
}

Future<void> _onGameStatusRequested(
GameStatusRequested event,
Emitter<CrosswordState> emit,
) async {
return emit.forEach(
_boardInfoRepository.getGameStatus(),
onData: (status) {
if (status == GameStatus.resetInProgress) {
return state.copyWith(
gameStatus: status,
boardStatus: BoardStatus.resetInProgress,
);
}

return state.copyWith(gameStatus: status);
},
onError: (error, stackTrace) {
addError(error, stackTrace);
return state.copyWith(
status: CrosswordStatus.failure,
);
},
);
}

void _onBoardStatusResumed(
BoardStatusResumed event,
Emitter<CrosswordState> emit,
) {
emit(
state.copyWith(
boardStatus: BoardStatus.inProgress,
),
);
}

void _onMascotDropped(
MascotDropped event,
Emitter<CrosswordState> emit,
Expand Down
14 changes: 14 additions & 0 deletions lib/crossword/bloc/crossword_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ class BoardLoadingInformationRequested extends CrosswordEvent {
List<Object?> get props => [];
}

class GameStatusRequested extends CrosswordEvent {
const GameStatusRequested();

@override
List<Object?> get props => [];
}

class BoardStatusResumed extends CrosswordEvent {
const BoardStatusResumed();

@override
List<Object?> get props => [];
}

class MascotDropped extends CrosswordEvent {
const MascotDropped();

Expand Down
15 changes: 15 additions & 0 deletions lib/crossword/bloc/crossword_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ enum WordStatus {
invalid,
}

enum BoardStatus {
inProgress,
resetInProgress,
}

class WordSelection extends Equatable {
WordSelection({
required this.section,
Expand Down Expand Up @@ -41,6 +46,8 @@ class WordSelection extends Equatable {
class CrosswordState extends Equatable {
const CrosswordState({
this.status = CrosswordStatus.initial,
this.gameStatus = GameStatus.inProgress,
this.boardStatus = BoardStatus.inProgress,
this.sectionSize = 0,
this.sections = const {},
this.selectedWord,
Expand All @@ -49,6 +56,8 @@ class CrosswordState extends Equatable {
});

final CrosswordStatus status;
final GameStatus gameStatus;
final BoardStatus boardStatus;
final int sectionSize;
final Map<(int, int), BoardSection> sections;
final WordSelection? selectedWord;
Expand All @@ -57,6 +66,8 @@ class CrosswordState extends Equatable {

CrosswordState copyWith({
CrosswordStatus? status,
GameStatus? gameStatus,
BoardStatus? boardStatus,
int? sectionSize,
Map<(int, int), BoardSection>? sections,
WordSelection? selectedWord,
Expand All @@ -65,6 +76,8 @@ class CrosswordState extends Equatable {
}) {
return CrosswordState(
status: status ?? this.status,
gameStatus: gameStatus ?? this.gameStatus,
boardStatus: boardStatus ?? this.boardStatus,
sectionSize: sectionSize ?? this.sectionSize,
sections: sections ?? this.sections,
selectedWord: selectedWord ?? this.selectedWord,
Expand All @@ -86,6 +99,8 @@ class CrosswordState extends Equatable {
@override
List<Object?> get props => [
status,
gameStatus,
boardStatus,
sectionSize,
sections,
selectedWord,
Expand Down
109 changes: 103 additions & 6 deletions lib/crossword/view/crossword_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:game_domain/game_domain.dart';
import 'package:io_crossword/audio/audio.dart';
import 'package:io_crossword/bottom_bar/bottom_bar.dart';
import 'package:io_crossword/crossword/crossword.dart' hide WordSelected;
import 'package:io_crossword/crossword/crossword.dart'
hide WordSelected, WordUnselected;
import 'package:io_crossword/crossword2/crossword2.dart';
import 'package:io_crossword/drawer/drawer.dart';
import 'package:io_crossword/end_game/end_game.dart';
import 'package:io_crossword/how_to_play/how_to_play.dart';
import 'package:io_crossword/l10n/l10n.dart';
import 'package:io_crossword/player/player.dart';
Expand Down Expand Up @@ -107,7 +109,14 @@ class CrosswordView extends StatelessWidget {
);
},
),
body: BlocBuilder<CrosswordBloc, CrosswordState>(
body: BlocConsumer<CrosswordBloc, CrosswordState>(
listenWhen: (previous, current) =>
previous.gameStatus != current.gameStatus,
listener: (context, state) {
if (state.gameStatus == GameStatus.resetInProgress) {
context.read<WordSelectionBloc>().add(const WordUnselected());
}
},
buildWhen: (previous, current) =>
previous.status != current.status ||
previous.mascotVisible != current.mascotVisible,
Expand Down Expand Up @@ -146,18 +155,106 @@ class LoadedBoardView extends StatelessWidget {

@override
Widget build(BuildContext context) {
return const DefaultWordInputController(
final boardStatus = context.select(
(CrosswordBloc bloc) => bloc.state.boardStatus,
);

return DefaultWordInputController(
child: Stack(
children: [
Crossword2View(),
WordSelectionPage(),
BottomBar(),
const Crossword2View(),
const WordSelectionPage(),
if (boardStatus != BoardStatus.resetInProgress)
const BottomBar()
else
const ColoredBox(
color: Color(0x88000000),
child: Center(
child: ResetDialogContent(),
),
),
],
),
);
}
}

class ResetDialogContent extends StatelessWidget {
@visibleForTesting
const ResetDialogContent({super.key});

@override
Widget build(BuildContext context) {
final l10n = context.l10n;

return IoPhysicalModel(
child: Card(
child: SizedBox(
width: 340,
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Text(
l10n.resetDialogTitle,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
Text(
l10n.resetDialogSubtitle,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
const _BottomActions(),
],
),
),
),
),
);
}
}

class _BottomActions extends StatelessWidget {
const _BottomActions();

@override
Widget build(BuildContext context) {
final l10n = context.l10n;

final gameStatus = context.select(
(CrosswordBloc bloc) => bloc.state.gameStatus,
);

final resetInProgress = gameStatus == GameStatus.resetInProgress;

return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: OutlinedButton(
onPressed: () => EndGameCheck.openDialog(context),
child: Text(l10n.exitButtonLabel),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: resetInProgress
? null
: () => context
.read<CrosswordBloc>()
.add(const BoardStatusResumed()),
child: Text(l10n.keepPlayingButtonLabel),
),
),
],
);
}
}

class MascotAnimation extends StatefulWidget {
@visibleForTesting
const MascotAnimation(this.mascot, {super.key});
Expand Down
20 changes: 16 additions & 4 deletions lib/how_to_play/view/how_to_play_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,14 @@ class _HowToPlaySmall extends StatelessWidget {
child: SingleChildScrollView(
child: HowToPlayContent(
mascot: mascot,
onDonePressed: () =>
context.flow<GameIntroStatus>().complete(),
onDonePressed: () {
context
.read<HowToPlayCubit>()
.updateStatus(HowToPlayStatus.pickingUp);
context
.read<AudioController>()
.playSfx(Assets.music.startButton1);
},
),
),
),
Expand Down Expand Up @@ -166,8 +172,14 @@ class _HowToPlayLarge extends StatelessWidget {
children: [
HowToPlayContent(
mascot: mascot,
onDonePressed: () =>
context.flow<GameIntroStatus>().complete(),
onDonePressed: () {
context
.read<HowToPlayCubit>()
.updateStatus(HowToPlayStatus.pickingUp);
context
.read<AudioController>()
.playSfx(Assets.music.startButton1);
},
),
const SizedBox(height: 40),
OutlinedButton(
Expand Down
16 changes: 16 additions & 0 deletions lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -402,5 +402,21 @@
"doneButtonLabel": "Done",
"@doneButtonLabel": {
"description": "The label for the done button."
},
"resetDialogTitle": "WOW! We've just completed the 2024 I/O Crossword, but the game isn't over!",
"@resetDialogTitle": {
"description": "The title of the game completed modal."
},
"resetDialogSubtitle": "Hang on while we reset the board so you can keep building that streak.\n\nWhen the board has been regenerated, you'll be able to continue playing.",
"@resetDialogSubtitle": {
"description": "The subtitle of the game completed modal."
},
"exitButtonLabel": "Exit",
"@exitButtonLabel": {
"description": "The label for the exit button."
},
"keepPlayingButtonLabel": "Keep playing",
"@keepPlayingButtonLabel": {
"description": "The label for the keep playing button."
}
}
39 changes: 39 additions & 0 deletions packages/board_info_repository/lib/src/board_info_repository.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:rxdart/rxdart.dart';

/// {@template game_status}
/// The status of the game.
/// {@endtemplate}
enum GameStatus {
/// The game is in progress.
inProgress('in_progress'),

/// The game is completed and in the process of resetting.
resetInProgress('reset_in_progress');

/// {@macro game_status}
const GameStatus(this.value);

/// The String value of the status.
final String value;

/// Converts the [value] to a [GameStatus].
static GameStatus fromString(String value) => values.firstWhere(
(element) => element.value == value,
orElse: () => inProgress,
);
}

/// {@template board_info_exception}
/// An exception to throw when there is an error fetching the board info.
/// {@endtemplate}
Expand Down Expand Up @@ -111,4 +134,20 @@ class BoardInfoRepository {

return _hintsEnabled!.stream;
}

/// Returns the status of the game.
Stream<GameStatus> getGameStatus() {
try {
return boardInfoCollection
.where('type', isEqualTo: 'game_status')
.snapshots()
.map(
(event) => GameStatus.fromString(
event.docs.first.data()['value'] as String,
),
);
} catch (error, stackStrace) {
throw BoardInfoException(error, stackStrace);
}
}
}
Loading

0 comments on commit 123b533

Please sign in to comment.