Skip to content
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
39 changes: 32 additions & 7 deletions lib/src/model/clock/chess_clock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:flutter/foundation.dart';
const _emergencyDelay = Duration(seconds: 20);
const _tickDelay = Duration(milliseconds: 100);

typedef _EmergencyState = ({bool shouldTriggerEmergencyCallback, DateTime? nextEmergency});

/// A chess clock.
class ChessClock {
ChessClock({
Expand All @@ -32,8 +34,14 @@ class ChessClock {
Timer? _startDelayTimer;
DateTime? _lastStarted;
final _stopwatch = clock.stopwatch();
bool _shouldPlayEmergencyFeedback = true;
DateTime? _nextEmergency;
_EmergencyState _whiteEmergencyState = (
shouldTriggerEmergencyCallback: true,
nextEmergency: null,
);
_EmergencyState _blackEmergencyState = (
shouldTriggerEmergencyCallback: true,
nextEmergency: null,
);

final ValueNotifier<Duration> _whiteTime;
final ValueNotifier<Duration> _blackTime;
Expand All @@ -55,6 +63,17 @@ class ChessClock {
/// Returns the current active side.
Side get activeSide => _activeSide;

set _activeSideEmergencyState(_EmergencyState state) {
if (_activeSide == Side.white) {
_whiteEmergencyState = state;
} else {
_blackEmergencyState = state;
}
}

_EmergencyState get _activeSideEmergencyState =>
_activeSide == Side.white ? _whiteEmergencyState : _blackEmergencyState;

/// Sets the time for either side.
void setTimes({Duration? whiteTime, Duration? blackTime}) {
if (whiteTime != null) {
Expand Down Expand Up @@ -171,13 +190,19 @@ class ChessClock {
final timeLeft = _activeTime.value;
if (emergencyThreshold != null &&
timeLeft <= emergencyThreshold! &&
_shouldPlayEmergencyFeedback &&
(_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) {
_shouldPlayEmergencyFeedback = false;
_nextEmergency = clock.now().add(_emergencyDelay);
_activeSideEmergencyState.shouldTriggerEmergencyCallback &&
(_activeSideEmergencyState.nextEmergency == null ||
_activeSideEmergencyState.nextEmergency!.isBefore(clock.now()))) {
_activeSideEmergencyState = (
shouldTriggerEmergencyCallback: false,
nextEmergency: clock.now().add(_emergencyDelay),
);
onEmergency?.call(_activeSide);
} else if (emergencyThreshold != null && timeLeft > emergencyThreshold! * 1.5) {
_shouldPlayEmergencyFeedback = true;
_activeSideEmergencyState = (
shouldTriggerEmergencyCallback: true,
nextEmergency: _activeSideEmergencyState.nextEmergency,
);
}
}
}
29 changes: 29 additions & 0 deletions test/model/clock/chess_clock_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,33 @@ void main() {
expect(onEmergencyCount, 1);
});
});

// This bug was caused by the "have we played the emergency sound" flag not being tracked separately per side.
test('Regression tests for issue #1626', () {
fakeAsync((async) {
int onEmergencyCount = 0;
final clock = ChessClock(
whiteTime: const Duration(seconds: 60),
blackTime: const Duration(seconds: 60),
emergencyThreshold: const Duration(seconds: 30),
onEmergency: (_) {
onEmergencyCount++;
},
);
clock.start();
async.elapse(const Duration(seconds: 30));
expect(onEmergencyCount, 1);

// Switch to black. We're above 1.5x the emergency threshold, so this used to reset the flag (for both sides).
clock.startSide(Side.black);
// There's an internal 20s cooldown for the emergency callback, so wait for that.
async.elapse(const Duration(seconds: 20));
expect(onEmergencyCount, 1);

// Switch back to white, this used to incorrectly play the sound again
clock.startSide(Side.white);
async.elapse(const Duration(milliseconds: 100));
expect(onEmergencyCount, 1);
});
});
}