Conversation
There was a problem hiding this comment.
Pull request overview
This pull request introduces an offline "Play vs Computer" mode that allows users to play chess games against a local Stockfish engine without requiring an internet connection. The implementation includes comprehensive game state management, hint system, takeback functionality, and game persistence.
Changes:
- Implements offline computer game screen with UI controls for starting games, configuring Stockfish level (1-12), choosing sides, and managing casual/rated modes
- Adds game controller with engine integration for move generation, hints computation using multi-PV evaluation, and game state management
- Introduces game storage layer for persisting ongoing games and user preferences
- Refactors MaterialDifferenceDisplay widget into a reusable component
- Enhances engine work system with Elo-based multiPV and search time scaling for realistic difficulty levels
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
test/view/offline_computer/offline_computer_game_screen_test.dart |
Comprehensive widget tests covering game flow, hints, casual mode, and saved game restoration |
test/model/engine/fake_stockfish.dart |
Adds LegalMoveFakeStockfish and MultiPvFakeStockfish for testing game progression and hints |
test/model/engine/evaluation_service_test.dart |
Updates tests to reflect new Elo-based multiPV and search time scaling |
lib/src/view/offline_computer/offline_computer_game_screen.dart |
Main game screen with board, player info, controls, and dialogs for game setup and results |
lib/src/model/offline_computer/offline_computer_game_controller.dart |
Game state controller managing moves, engine responses, hints computation, and takebacks |
lib/src/model/offline_computer/offline_computer_game_storage.dart |
JSON-based persistence for saving and loading ongoing games |
lib/src/model/offline_computer/offline_computer_game_preferences.dart |
User preferences for Stockfish level, side choice, and casual mode |
lib/src/model/game/offline_computer_game.dart |
Game model with 12 Stockfish levels mapping to Elo ratings (1320-3190) |
lib/src/widgets/material_diff.dart |
Extracted reusable widget for displaying material difference and captured pieces |
lib/src/view/game/game_player.dart |
Refactored to use extracted MaterialDifferenceDisplay widget |
lib/src/model/engine/work.dart |
Enhanced MoveWork with Elo-scaled multiPV (2-10) and search times (150-5000ms) for realistic strength levels |
lib/src/model/engine/evaluation_service.dart |
Removed redundant _isDisposed checks for cleaner code |
lib/src/model/engine/evaluation_mixin.dart |
Made EvalWork.path nullable to support contexts without position trees |
lib/src/view/play/play_menu.dart |
Adds "Play against computer" menu item |
lib/src/styles/lichess_icons.dart |
Minor whitespace cleanup |
lib/src/model/settings/preferences_storage.dart |
Adds offlineComputerGame preference category |
CLAUDE.md |
Documents Dart 3.10+ dot shorthand syntax conventions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| context: context, | ||
| builder: (context) => YesNoDialog( | ||
| title: Text(context.l10n.mobileAreYouSure), | ||
| content: const Text('No worries, your game will be saved.'), |
There was a problem hiding this comment.
The text 'No worries, your game will be saved.' is hardcoded and should use a localized string from context.l10n for internationalization consistency.
| content: const Text('No worries, your game will be saved.'), | |
| content: Text(context.l10n.mobileGameWillBeSaved), |
| const SizedBox(height: 16), | ||
| SwitchListTile.adaptive( | ||
| title: Text(context.l10n.casual), | ||
| subtitle: const Text('Allow takebacks and hints'), |
There was a problem hiding this comment.
The text 'Allow takebacks and hints' is hardcoded and should use a localized string from context.l10n for internationalization consistency.
| subtitle: const Text('Allow takebacks and hints'), | |
| subtitle: Text(context.l10n.allowTakebacksAndHints), |
| @override | ||
| void runBuild() { | ||
| _evaluationService = ref.read(evaluationServiceProvider); | ||
| _evaluationService = ref.watch(evaluationServiceProvider); |
There was a problem hiding this comment.
Changing from ref.read to ref.watch means the mixin will now rebuild when the evaluationServiceProvider changes. This could cause unnecessary rebuilds if the service is mutated, as watching a provider typically means you want to rebuild when it changes. The original ref.read was likely intentional to avoid this. Please verify this change is necessary and doesn't cause performance issues or unexpected rebuilds.
| _evaluationService = ref.watch(evaluationServiceProvider); | |
| _evaluationService = ref.read(evaluationServiceProvider); |
| mainAxisSize: .max, | ||
| mainAxisAlignment: .start, |
There was a problem hiding this comment.
The dot shorthand syntax (.max and .start) is not used elsewhere in the codebase. For consistency, use the full form MainAxisSize.max and MainAxisAlignment.start instead. While the CLAUDE.md document describes this syntax, the existing codebase consistently uses the full form throughout (see lib/src/view/game/game_player.dart:83, lib/src/view/analysis/analysis_layout.dart:208, and many other files).
There was a problem hiding this comment.
we'd rather update the code little by little to use this new syntax
| // Verify side selection options (label is "Side") | ||
| expect(find.text('Side'), findsOneWidget); | ||
| expect(find.text('White'), findsOneWidget); | ||
| expect(find.text('Random side'), findsOneWidget); |
There was a problem hiding this comment.
The test expects the text 'Random side' but the UI code uses context.l10n.randomColor which, based on existing usage in lib/src/model/common/game.dart:11, would return a different localized string. The test should use the actual localized string that will be rendered, not a hardcoded value.
| expect(find.text('Random side'), findsOneWidget); | |
| expect(find.text('Random color'), findsOneWidget); |
| ref.read(_isBoardFlippedProvider.notifier).toggle(); | ||
| }, | ||
| ), | ||
| BottomSheetAction(makeLabel: (context) => const Text('New game'), onPressed: onNewGame), |
There was a problem hiding this comment.
The text 'New game' is hardcoded and should use a localized string from context.l10n. This is inconsistent with other menu items which properly use localization (e.g., flipBoard, analysis, resign).
| BottomSheetAction(makeLabel: (context) => const Text('New game'), onPressed: onNewGame), | |
| BottomSheetAction( | |
| makeLabel: (context) => Text(context.l10n.newGame), | |
| onPressed: onNewGame, | |
| ), |
| content: Text(subtitle), | ||
| actions: [ | ||
| TextButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.close)), | ||
| TextButton(onPressed: onNewGame, child: const Text('New game')), |
There was a problem hiding this comment.
The text 'New game' is hardcoded and should use a localized string from context.l10n. This is inconsistent with other UI text which properly uses localization (e.g., close, victory, draw).
| TextButton(onPressed: onNewGame, child: const Text('New game')), | |
| TextButton(onPressed: onNewGame, child: Text(context.l10n.newGame)), |
Co-authored-by: Copilot <[email protected]>
This pull request introduces the offline "Play vs Computer" mode, allowing users to play chess games against a local Stockfish engine.