Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reset game status #425

Merged
merged 18 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class App extends StatelessWidget {
create: (_) => CrosswordBloc(
crosswordRepository: crosswordRepository,
boardInfoRepository: boardInfoRepository,
),
)..add(const GameStatusRequested()),
),
BlocProvider(
create: (_) => PlayerBloc(
Expand Down
41 changes: 41 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);
}

final CrosswordRepository _crosswordRepository;
Expand Down Expand Up @@ -199,4 +201,43 @@ class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
);
}
}

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

return state.copyWith(gameStatus: status);
},
);
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(
const CrosswordState(
status: CrosswordStatus.failure,
),
);
}
}
marwfair marked this conversation as resolved.
Show resolved Hide resolved

void _onBoardStatusResumed(
BoardStatusResumed event,
Emitter<CrosswordState> emit,
) {
emit(
state.copyWith(
boardStatus: BoardStatus.inProgress,
),
);
}
}
14 changes: 14 additions & 0 deletions lib/crossword/bloc/crossword_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,17 @@ class BoardLoadingInformationRequested extends CrosswordEvent {
@override
List<Object?> get props => [];
}

class GameStatusRequested extends CrosswordEvent {
marwfair marked this conversation as resolved.
Show resolved Hide resolved
const GameStatusRequested();

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

class BoardStatusResumed extends CrosswordEvent {
const BoardStatusResumed();

@override
List<Object?> get props => [];
}
24 changes: 22 additions & 2 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,27 +46,35 @@ class WordSelection extends Equatable {
class CrosswordState extends Equatable {
const CrosswordState({
this.status = CrosswordStatus.initial,
this.gameStatus = GameStatus.inProgress,
this.boardStatus = BoardStatus.inProgress,
marwfair marked this conversation as resolved.
Show resolved Hide resolved
this.sectionSize = 0,
this.sections = const {},
this.selectedWord,
this.zoomLimit = 0.35,
});

final CrosswordStatus status;
final GameStatus gameStatus;
final BoardStatus boardStatus;
final int sectionSize;
final Map<(int, int), BoardSection> sections;
final WordSelection? selectedWord;
final double zoomLimit;

CrosswordState copyWith({
CrosswordStatus? status,
GameStatus? gameStatus,
BoardStatus? boardStatus,
marwfair marked this conversation as resolved.
Show resolved Hide resolved
int? sectionSize,
Map<(int, int), BoardSection>? sections,
WordSelection? selectedWord,
double? zoomLimit,
}) {
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 @@ -79,6 +92,13 @@ class CrosswordState extends Equatable {
}

@override
List<Object?> get props =>
[status, sectionSize, sections, selectedWord, zoomLimit];
List<Object?> get props => [
status,
gameStatus,
boardStatus,
sectionSize,
sections,
selectedWord,
zoomLimit,
];
}
120 changes: 114 additions & 6 deletions lib/crossword/view/crossword_page.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import 'package:api_client/api_client.dart';
import 'package:board_info_repository/board_info_repository.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/l10n/l10n.dart';
import 'package:io_crossword/player/player.dart';
import 'package:io_crossword/random_word_selection/random_word_selection.dart';
Expand Down Expand Up @@ -98,7 +101,12 @@ class CrosswordView extends StatelessWidget {
);
},
),
body: BlocBuilder<CrosswordBloc, CrosswordState>(
body: BlocConsumer<CrosswordBloc, CrosswordState>(
listener: (context, state) {
marwfair marked this conversation as resolved.
Show resolved Hide resolved
if (state.gameStatus == GameStatus.resetInProgress) {
context.read<WordSelectionBloc>().add(const WordUnselected());
}
},
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, state) {
switch (state.status) {
Expand Down Expand Up @@ -127,14 +135,114 @@ 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: IgnorePointer(
ignoring: resetInProgress,
child: OutlinedButton(
style: resetInProgress
? Theme.of(context).outlinedButtonTheme.style?.copyWith(
foregroundColor:
MaterialStateProperty.all<Color>(Colors.grey),
side: MaterialStateProperty.all<BorderSide>(
const BorderSide(color: Colors.grey),
),
shape: MaterialStateProperty.all<OutlinedBorder>(
const StadiumBorder(side: BorderSide(width: 2)),
),
)
: null,
onPressed: () =>
context.read<CrosswordBloc>().add(const BoardStatusResumed()),
child: Text(l10n.keepPlayingButtonLabel),
),
),
marwfair marked this conversation as resolved.
Show resolved Hide resolved
),
],
);
}
}
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."
}
}
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
Loading