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

Fix puzzle storm and streak issue with invalidating provider #661

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions lib/src/model/puzzle/puzzle_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class PuzzleRepository {
(e) => PuzzleId(e),
),
),
timestamp: DateTime.now(),
);
},
);
Expand All @@ -119,6 +120,7 @@ class PuzzleRepository {
),
highscore: pick(json['high']).letOrNull(_stormHighScoreFromPick),
key: pick(json['key']).asStringOrNull(),
timestamp: DateTime.now(),
);
},
);
Expand Down Expand Up @@ -251,6 +253,9 @@ class PuzzleStreakResponse with _$PuzzleStreakResponse {
const factory PuzzleStreakResponse({
required Puzzle puzzle,
required Streak streak,
// This field is not returned by the API but it is used to force the
// [puzzleControllerProvider] to recompute
required DateTime timestamp,
}) = _PuzzleStreakResponse;
}

Expand All @@ -260,6 +265,9 @@ class PuzzleStormResponse with _$PuzzleStormResponse {
required IList<LitePuzzle> puzzles,
required String? key,
required PuzzleStormHighScore? highscore,
// This field is not returned by the API but it is used to force the
// stormControllerProvider to recompute
required DateTime timestamp,
}) = _PuzzleStormResponse;
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/model/puzzle/puzzle_streak.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class PuzzleStreak with _$PuzzleStreak {
required int index,
required bool hasSkipped,
required bool finished,
required DateTime timestamp,
}) = _PuzzleStreak;

PuzzleId? get nextId => streak.getOrNull(index + 1);
Expand Down
72 changes: 45 additions & 27 deletions lib/src/model/puzzle/storm_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,47 +28,49 @@ const startTime = Duration(minutes: 3);

@riverpod
class StormController extends _$StormController {
int _nextPuzzleIndex = 0;
int _moves = 0;
int _errors = 0;
final _history = <PuzzleHistoryEntry>[];
Timer? _firstMoveTimer;

@override
StormState build(IList<LitePuzzle> puzzles) {
StormState build(IList<LitePuzzle> puzzles, DateTime timestamp) {
final pov = Chess.fromSetup(Setup.parseFen(puzzles.first.fen));
final clock = StormClock();

ref.onDispose(() {
_firstMoveTimer?.cancel();
state.clock.dispose();
clock.dispose();
});

final pov = Chess.fromSetup(Setup.parseFen(puzzles.first.fen));
final newState = StormState(
firstMovePlayed: false,
runOver: false,
runStarted: false,
puzzle: puzzles[_nextPuzzleIndex],
puzzleIndex: 0,
puzzle: puzzles.first,
moves: 0,
errors: 0,
history: const IList.empty(),
position: pov,
pov: pov.turn.opposite,
moveIndex: -1,
numSolved: 0,
clock: StormClock(),
clock: clock,
combo: const StormCombo(current: 0, best: 0),
stats: null,
lastSolvedTime: null,
);
_nextPuzzleIndex += 1;

_firstMoveTimer = Timer(
const Duration(seconds: 1),
() => _addMove(
state.expectedMove!,
newState.expectedMove!,
ComboState.noChange,
runStarted: false,
userMove: false,
isFirstMove: true,
),
);
newState.clock.timeStream.listen((e) {
if (e.$1 == Duration.zero && state.clock.endAt == null) {
if (e.$1 == Duration.zero && clock.endAt == null) {
end();
}
});
Expand All @@ -80,7 +82,7 @@ class StormController extends _$StormController {
state.clock.start();
final expected = state.expectedMove;
_addMove(move, ComboState.noChange, runStarted: true, userMove: true);
_moves += 1;
state = state.copyWith(moves: state.moves + 1);
if (state.position.isGameOver || move == expected) {
final bonus = state.combo.bonus(getNext: true);
if (bonus != null) {
Expand All @@ -105,7 +107,7 @@ class StormController extends _$StormController {
userMove: false,
);
} else {
_errors += 1;
state = state.copyWith(errors: state.errors + 1);
ref.read(soundServiceProvider).play(Sound.error);
HapticFeedback.heavyImpact();
state.clock.subtractTime(malus);
Expand Down Expand Up @@ -178,9 +180,12 @@ class StormController extends _$StormController {
newComboCurrent = state.combo.current;
}

final int newPuzzleIndex = state.puzzleIndex + 1;

state = state.copyWith(
puzzle: puzzles[_nextPuzzleIndex],
position: Chess.fromSetup(Setup.parseFen(puzzles[_nextPuzzleIndex].fen)),
puzzleIndex: newPuzzleIndex,
puzzle: puzzles[newPuzzleIndex],
position: Chess.fromSetup(Setup.parseFen(puzzles[newPuzzleIndex].fen)),
moveIndex: -1,
numSolved: result ? state.numSolved + 1 : state.numSolved,
lastSolvedTime: DateTime.now(),
Expand All @@ -189,7 +194,6 @@ class StormController extends _$StormController {
best: math.max(state.combo.best, state.combo.current + 1),
),
);
_nextPuzzleIndex += 1;
await Future<void>.delayed(moveDelay);
_addMove(
state.expectedMove!,
Expand Down Expand Up @@ -241,13 +245,13 @@ class StormController extends _$StormController {
}

StormRunStats _getStats() {
final wins = _history.where((e) => e.win == true).toList();
final mean =
_history.sumBy((e) => e.solvingTime!.inSeconds) / _history.length;
final wins = state.history.where((e) => e.win == true).toList();
final mean = state.history.sumBy((e) => e.solvingTime!.inSeconds) /
state.history.length;
final threshold = mean * 1.5;
return StormRunStats(
moves: _moves,
errors: _errors,
moves: state.moves,
errors: state.errors,
score: wins.length,
comboBest: state.combo.best,
time: state.clock.endAt!,
Expand All @@ -257,8 +261,8 @@ class StormController extends _$StormController {
(maxRating, rating) => rating > maxRating ? rating : maxRating,
)
: 0,
history: _history.toIList(),
slowPuzzleIds: _history
history: state.history,
slowPuzzleIds: state.history
.where((e) => e.solvingTime!.inSeconds > threshold)
.map((e) => e.id)
.toIList(),
Expand All @@ -269,20 +273,25 @@ class StormController extends _$StormController {
final timeTaken = state.lastSolvedTime != null
? DateTime.now().difference(state.lastSolvedTime!)
: DateTime.now().difference(state.clock.startAt!);
_history.add(
PuzzleHistoryEntry.fromLitePuzzle(state.puzzle, success, timeTaken),
state = state.copyWith(
history: state.history.add(
PuzzleHistoryEntry.fromLitePuzzle(state.puzzle, success, timeTaken),
),
);
}

bool _isNextPuzzleAvailable() {
return _nextPuzzleIndex < puzzles.length;
return state.puzzleIndex + 1 < puzzles.length;
}
}

@freezed
class StormState with _$StormState {
const StormState._();
const factory StormState({
/// Index of the current puzzle being played
required int puzzleIndex,

/// Current puzzle being played
required LitePuzzle puzzle,

Expand All @@ -304,6 +313,15 @@ class StormState with _$StormState {
/// A combo object which has the current and best combo
required StormCombo combo,

/// Number of moves made in the run
required int moves,

/// Number of errors made in the run
required int errors,

/// The history of puzzles played in the run
required IList<PuzzleHistoryEntry> history,

/// Stats of the storm run. Only initialisd after the run ends
required StormRunStats? stats,

Expand Down
2 changes: 1 addition & 1 deletion lib/src/view/puzzle/storm_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class _Body extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final ctrlProvider = stormControllerProvider(data.puzzles);
final ctrlProvider = stormControllerProvider(data.puzzles, data.timestamp);
final puzzleState = ref.watch(ctrlProvider);
ref.listen(ctrlProvider.select((state) => state.runOver), (_, s) {
if (s) {
Expand Down
1 change: 1 addition & 0 deletions lib/src/view/puzzle/streak_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class _Load extends ConsumerWidget {
index: 0,
hasSkipped: false,
finished: false,
timestamp: data.timestamp,
),
);
},
Expand Down
1 change: 1 addition & 0 deletions test/view/puzzle/storm_screen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,5 @@ final mockStromRun = PuzzleStormResponse(
]),
key: null,
highscore: null,
timestamp: DateTime.now(),
);