Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ef64fb5
Added hint button, need to add functionality.
ItsDang Dec 5, 2024
052ffcd
Switch hint icon from help to info to distinguish from view the solut…
ItsDang Dec 5, 2024
03a2ff0
Switch hint button to hint function.
ItsDang Dec 5, 2024
8f9075b
Working on hint function, now logs the correct move in log.
ItsDang Dec 5, 2024
283357c
It's not pretty, but got circle around the solution to square.
ItsDang Dec 6, 2024
9f0e32e
Refactored common steps from toggleHint() and skipmove().
ItsDang Dec 6, 2024
5747cd8
Fixed the button toggle and reset on move.
ItsDang Dec 6, 2024
b63c16c
Spacing
ItsDang Dec 6, 2024
3b19f0b
got the possible hint move squares
ItsDang Dec 10, 2024
55b2bc0
Got to circle the possible squares to move to.
ItsDang Dec 12, 2024
6e3b41f
Highlight hint button when selected.
ItsDang Dec 12, 2024
31484e0
Removed unused developer import and adding trailing ,
ItsDang Dec 12, 2024
b47a068
Formatting for puzzle screen
ItsDang Dec 12, 2024
bcff029
Adding last trailing commas
ItsDang Dec 12, 2024
5e820e7
1 more trailing , and format
ItsDang Dec 12, 2024
e33565d
Merge branch 'lichess-org:main' into puzzle-show-hint
ItsDang Dec 12, 2024
cf61b62
Merge branch 'main' into puzzle-hints-2
CloudyDino Feb 20, 2025
84d7997
dart format
CloudyDino Feb 20, 2025
123c7ec
Don't have rated puzzle when hint is used
CloudyDino Feb 16, 2025
0e2a9e7
Remove showHint and just use hintMove
CloudyDino Feb 16, 2025
c1a4359
Rename hintPossibleMoves to hintSquares
CloudyDino Feb 16, 2025
3e84fce
Only circle hint move
CloudyDino Feb 18, 2025
d14ff79
Change hintMove to hintSquare
CloudyDino Feb 18, 2025
c1d8af5
Use _viewSolutionCompleter.future
CloudyDino Feb 19, 2025
8e77780
Rename solutionMove to _nextSolutionMove and make private getter for …
CloudyDino Feb 20, 2025
c4ad866
Add PuzzleScreen test for hints
CloudyDino Feb 20, 2025
7417bf5
Try to check if hint is set
CloudyDino Feb 20, 2025
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
29 changes: 25 additions & 4 deletions lib/src/model/puzzle/puzzle_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class PuzzleController extends _$PuzzleController {
late Branch _gameTree;
Timer? _firstMoveTimer;
Timer? _viewSolutionTimer;

// on streak, we pre-load the next puzzle to avoid a delay when the user
// completes the current one
FutureResult<PuzzleContext?>? _nextPuzzleFuture;
Expand All @@ -43,6 +44,7 @@ class PuzzleController extends _$PuzzleController {

Future<PuzzleService> get _service =>
ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength);

@override
PuzzleState build(PuzzleContext initialContext, {PuzzleStreak? initialStreak}) {
final evaluationService = ref.read(evaluationServiceProvider);
Expand Down Expand Up @@ -100,6 +102,7 @@ class PuzzleController extends _$PuzzleController {
currentPath: UciPath.empty,
node: _gameTree.view,
pov: _gameTree.nodeAt(initialPath).position.ply.isEven ? Side.white : Side.black,
hintShown: false,
resultSent: false,
isChangingDifficulty: false,
isLocalEvalEnabled: false,
Expand All @@ -119,6 +122,7 @@ class PuzzleController extends _$PuzzleController {
_addMove(move);

if (state.mode == PuzzleMode.play) {
state = state.copyWith(hintSquare: null);
final nodeList = _gameTree.branchesOn(state.currentPath).toList();
final movesToTest = nodeList.sublist(state.initialPath.size).map((e) => e.sanMove);

Expand Down Expand Up @@ -195,12 +199,18 @@ class PuzzleController extends _$PuzzleController {
});
}

void toggleHint() {
if (state.hintSquare == null) {
state = state.copyWith(hintShown: true, hintSquare: state._nextSolutionMove.from);
} else {
state = state.copyWith(hintSquare: null);
}
}

void skipMove() {
if (state.streak != null) {
state = state.copyWith.streak!(hasSkipped: true);
final moveIndex = state.currentPath.size - state.initialPath.size;
final solution = state.puzzle.puzzle.solution[moveIndex];
onUserMove(NormalMove.fromUci(solution));
onUserMove(state._nextSolutionMove);
}
}

Expand Down Expand Up @@ -311,7 +321,10 @@ class PuzzleController extends _$PuzzleController {
solution: PuzzleSolution(
id: state.puzzle.puzzle.id,
win: state.result == PuzzleResult.win,
rated: initialContext.userId != null && ref.read(puzzlePreferencesProvider).rated,
rated:
initialContext.userId != null &&
!state.hintShown &&
ref.read(puzzlePreferencesProvider).rated,
),
);

Expand Down Expand Up @@ -511,6 +524,8 @@ class PuzzleState with _$PuzzleState {
NormalMove? promotionMove,
PuzzleResult? result,
PuzzleFeedback? feedback,
required bool hintShown,
Square? hintSquare,
required bool isLocalEvalEnabled,
required bool resultSent,
required bool isChangingDifficulty,
Expand All @@ -531,9 +546,15 @@ class PuzzleState with _$PuzzleState {
EvaluationContext(variant: Variant.standard, initialPosition: initialPosition);

Position get position => node.position;

String get fen => node.position.fen;

bool get canGoNext => mode == PuzzleMode.view && node.children.isNotEmpty;

bool get canGoBack => mode == PuzzleMode.view && currentPath.size > initialPath.size;

NormalMove get _nextSolutionMove =>
NormalMove.fromUci(puzzle.puzzle.solution[currentPath.size - initialPath.size]);

IMap<Square, ISet<Square>> get validMoves => makeLegalMoves(position);
}
18 changes: 18 additions & 0 deletions lib/src/view/puzzle/puzzle_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ class _Body extends ConsumerWidget {
dest: evalBestMove.to,
),
])
: puzzleState.hintSquare != null
? ISet([Circle(color: ShapeColor.green.color, orig: puzzleState.hintSquare!)])
: null,
engineGauge:
puzzleState.isEngineEnabled
Expand Down Expand Up @@ -404,6 +406,22 @@ class _BottomBarState extends ConsumerState<_BottomBar> {
!isDailyPuzzle &&
puzzleState.mode != PuzzleMode.view)
_DifficultySelector(initialPuzzleContext: widget.initialPuzzleContext),
if (puzzleState.mode != PuzzleMode.view)
FutureBuilder(
future: _viewSolutionCompleter.future,
builder: (context, snapshot) {
return BottomBarButton(
icon: Icons.info,
label: context.l10n.getAHint,
showLabel: true,
highlighted: puzzleState.hintSquare != null,
onTap:
snapshot.connectionState == ConnectionState.done
? () => ref.read(ctrlProvider.notifier).toggleHint()
: null,
);
},
),
if (puzzleState.mode != PuzzleMode.view)
FutureBuilder(
future: _viewSolutionCompleter.future,
Expand Down
114 changes: 108 additions & 6 deletions test/view/puzzle/puzzle_screen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/testing.dart';
import 'package:lichess_mobile/src/model/account/account_preferences.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart';
Expand Down Expand Up @@ -200,7 +201,7 @@ void main() {

const orientation = Side.black;

// await for first move to be played
// wait for first move to be played
await tester.pump(const Duration(milliseconds: 1500));

// in play mode we don't see the continue button
Expand Down Expand Up @@ -497,24 +498,125 @@ void main() {
// wait for the puzzle to load
await tester.pump(const Duration(milliseconds: 200));

// await for first move to be played and view solution button to appear
// wait for first move to be played and view solution button to appear
await tester.pump(const Duration(seconds: 5));

// view solution
expect(find.byIcon(Icons.help), findsOneWidget);
await tester.tap(find.byIcon(Icons.help));

// wait for solution replay animation to finish
await tester.pump(const Duration(seconds: 1));

// check puzzle was saved as isRatedPreference
final captured = verify(saveDBReq).captured;
final captured = verify(saveDBReq).captured.map((e) => e as PuzzleBatch).toList();
expect(captured.length, 2);
expect((captured[1] as PuzzleBatch).solved.length, 0);
expect((captured[0] as PuzzleBatch).solved.length, 1);
expect((captured[0] as PuzzleBatch).solved[0].rated, isRatedPreference);
expect(captured[0].solved, [
PuzzleSolution(id: puzzle2.puzzle.id, win: false, rated: isRatedPreference),
]);
expect(captured[1].solved.length, 0);
},
);
}

testWidgets('puzzle rating is saved correctly when hint is used', variant: kPlatformVariant, (
WidgetTester tester,
) async {
final mockClient = MockClient((request) {
if (request.url.path == '/api/puzzle/batch/mix') {
return mockResponse(batchOf1, 200);
}
return mockResponse('', 404);
});

final app = await makeTestProviderScopeApp(
tester,
home: PuzzleScreen(
angle: const PuzzleTheme(PuzzleThemeKey.mix),
puzzleId: puzzle2.puzzle.id,
),
overrides: [
lichessClientProvider.overrideWith((ref) {
return LichessClient(mockClient, ref);
}),
puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage),
puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage),
puzzlePreferencesProvider.overrideWith(() => MockPuzzlePreferences(true)),
],
userSession: fakeSession,
);

Future<void> saveDBReq() => mockBatchStorage.save(
userId: fakeSession.user.id,
angle: const PuzzleTheme(PuzzleThemeKey.mix),
data: captureAny(named: 'data'),
);
when(saveDBReq).thenAnswer((_) async {});
when(
() => mockBatchStorage.fetch(
userId: fakeSession.user.id,
angle: const PuzzleTheme(PuzzleThemeKey.mix),
),
).thenAnswer((_) async => batch);

when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {});

await tester.pumpWidget(app);

// wait for the puzzle to load
await tester.pump(const Duration(milliseconds: 200));

// wait for first move to be played and hint/view solution buttons to appear
await tester.pump(const Duration(seconds: 5));

// check possible hint widgets before hint is set
final customPaintWidgetsBefore = find.byType(CustomPaint).evaluate().toSet();

// get hint and wait for it to show
expect(find.byIcon(Icons.info), findsOneWidget);
await tester.tap(find.byIcon(Icons.info));
await tester.pump(const Duration(milliseconds: 100));

// check hint is set
final customPaintWidgetsAfter = find.byType(CustomPaint).evaluate();
expect(customPaintWidgetsAfter.length, customPaintWidgetsBefore.length + 1);
final diff = customPaintWidgetsAfter.toSet().difference(customPaintWidgetsBefore);
expect(diff.length, 1);
expect((diff.first.widget as CustomPaint).painter.runtimeType.toString(), '_CirclePainter');

// view solution
expect(find.byIcon(Icons.help), findsOneWidget);
await tester.tap(find.byIcon(Icons.help));

// wait for solution replay animation to finish
await tester.pump(const Duration(seconds: 1));

// go to next puzzle
expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget);
await tester.tap(find.byIcon(CupertinoIcons.play_arrow_solid));

// wait for the puzzle to load
await tester.pump(const Duration(milliseconds: 200));

// wait for first move to be played and hint/view solution buttons to appear
await tester.pump(const Duration(seconds: 5));

// view solution
expect(find.byIcon(Icons.help), findsOneWidget);
await tester.tap(find.byIcon(Icons.help));

// wait for solution replay animation to finish
await tester.pump(const Duration(seconds: 1));

// check first puzzle was unrated due to hint
// and following puzzles are still rated when not using the hint
final captured = verify(saveDBReq).captured.map((e) => e as PuzzleBatch).toList();
expect(captured.length, 4);
expect(captured[0].solved, [PuzzleSolution(id: puzzle2.puzzle.id, win: false, rated: false)]);
expect(captured[1].solved.length, 0);
expect(captured[2].solved, [PuzzleSolution(id: puzzle.puzzle.id, win: false, rated: true)]);
expect(captured[3].solved.length, 0);
});
});
}

Expand Down