Skip to content
Open
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
151 changes: 121 additions & 30 deletions lib/src/model/clock/clock_tool_controller.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:clock/clock.dart' as clock;
import 'package:dartchess/dartchess.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -17,6 +18,23 @@ enum ClockSide {
Side get chessClockSide => this == ClockSide.top ? Side.black : Side.white;
}

enum ClockTimeControlType {
increment,
simpleDelay,
bronsteinDelay;

String get label => switch (this) {
increment => 'Increment',
simpleDelay => 'Simple delay',
bronsteinDelay => 'Bronstein delay',
};

String get clockValueLabel => switch (this) {
increment => 'Increment',
simpleDelay || bronsteinDelay => 'Delay',
};
}

/// A provider for [ClockToolController].
final clockToolControllerProvider = NotifierProvider.autoDispose<ClockToolController, ClockState>(
ClockToolController.new,
Expand All @@ -30,13 +48,18 @@ class ClockToolController extends Notifier<ClockState> {
ClockSide.bottom: false,
};
late Duration _emergencyThreshold;
DateTime? _delayStartedAt;
Duration? _delayDuration;
Duration? _pausedDelayRemaining;
Duration? _activeTurnStartedWithTime;

@override
ClockState build() {
const time = Duration(minutes: 10);
const increment = Duration.zero;
_emergencyThreshold = _calculateEmergencyThreshold(time);
const options = ClockOptions(
type: ClockTimeControlType.increment,
topTime: time,
bottomTime: time,
topIncrement: increment,
Expand Down Expand Up @@ -91,6 +114,8 @@ class ClockToolController extends Notifier<ClockState> {

void onTap(ClockSide playerType) {
final started = state.activeSide != null;
final wasRunning = _clock.isRunning;
final thinkTime = wasRunning ? _clock.stop() : null;
state = state.copyWith(
activeSide: playerType.opposite,
topMoves: playerType == ClockSide.top && started ? state.topMoves + 1 : state.topMoves,
Expand All @@ -99,22 +124,8 @@ class ClockToolController extends Notifier<ClockState> {
: state.bottomMoves,
);
ref.read(soundServiceProvider).play(Sound.clock);
_clock.incTime(
playerType.chessClockSide,
playerType == ClockSide.top ? state.options.topIncrement : state.options.bottomIncrement,
);
// Start the countdown only if either this is not a zero-start clock
// or the new active side has already made at least one move.
final Duration initialOfNewActive = playerType.opposite == ClockSide.top
? state.options.topTime
: state.options.bottomTime;
final bool hasNewActiveMoved =
(playerType.opposite == ClockSide.top ? state.topMoves : state.bottomMoves) > 0;
if (initialOfNewActive.inMilliseconds != 0 || hasNewActiveMoved) {
_clock.startSide(playerType.opposite.chessClockSide);
} else {
_clock.stop();
}
_applyMoveBonus(playerType, wasRunning ? thinkTime : null, _activeTurnStartedWithTime);
_startActiveSide(playerType.opposite);
}

void updateDuration(ClockSide playerType, Duration duration) {
Expand All @@ -129,7 +140,7 @@ class ClockToolController extends Notifier<ClockState> {
}

void updateOptions(TimeIncrement timeIncrement) {
final options = ClockOptions.fromTimeIncrement(timeIncrement);
final options = ClockOptions.fromTimeIncrement(timeIncrement, type: state.options.type);
_emergencyThreshold = _calculateEmergencyThreshold(Duration(seconds: timeIncrement.time));
_hasPlayedLowTimeSound[ClockSide.top] = false;
_hasPlayedLowTimeSound[ClockSide.bottom] = false;
Expand All @@ -143,6 +154,7 @@ class ClockToolController extends Notifier<ClockState> {

void updateOptionsCustom(TimeIncrement clock, ClockSide player) {
final options = ClockOptions(
type: state.options.type,
topTime: player == ClockSide.top ? Duration(seconds: clock.time) : state.options.topTime,
bottomTime: player == ClockSide.bottom
? Duration(seconds: clock.time)
Expand All @@ -164,6 +176,11 @@ class ClockToolController extends Notifier<ClockState> {
);
}

void updateClockType(ClockTimeControlType type) {
state = state.copyWith(options: state.options.copyWith(type: type));
reset();
}

void reset() {
state = state.copyWith(
activeSide: null,
Expand All @@ -176,27 +193,19 @@ class ClockToolController extends Notifier<ClockState> {
);
_clock.stop();
_clock.setTimes(blackTime: state.options.topTime, whiteTime: state.options.bottomTime);
_activeTurnStartedWithTime = null;
// Reset low time sound flags for both players
_hasPlayedLowTimeSound[ClockSide.top] = false;
_hasPlayedLowTimeSound[ClockSide.bottom] = false;
}

void start(ClockSide playerType) {
final Duration initialOfStartingPlayer = playerType.opposite == ClockSide.top
? state.options.topTime
: state.options.bottomTime;
state = state.copyWith(activeSide: playerType.opposite);
// If the new active side starts at zero time, do not start their countdown yet.
// We only begin decreasing a side's clock after that side has completed at least one move.
// This makes 0+increment modes usable.
if (initialOfStartingPlayer.inMilliseconds == 0) {
_clock.stop();
} else {
_clock.startSide(playerType.opposite.chessClockSide);
}
_startActiveSide(playerType.opposite);
}

void pause() {
_pausedDelayRemaining = _remainingDelay();
_clock.stop();
state = state.copyWith(paused: true);
}
Expand All @@ -213,28 +222,105 @@ class ClockToolController extends Notifier<ClockState> {
: state.bottomMoves > 0;

if (active != null && (initialOfActive.inMilliseconds != 0 || hasActiveMoved)) {
_clock.start();
final delay = _pausedDelayRemaining ?? _delayFor(active);
_markDelay(delay);
_clock.start(delay: delay);
}
_pausedDelayRemaining = null;
state = state.copyWith(paused: false);
}

void toggleOrientation(ClockOrientation clockOrientation) {
state = state.copyWith(clockOrientation: clockOrientation);
}

Duration? _startActiveSide(ClockSide activeSide) {
// Start the countdown only if either this is not a zero-start clock
// or the active side has already made at least one move.
// This makes 0+increment modes usable.
final Duration initialOfActive = activeSide == ClockSide.top
? state.options.topTime
: state.options.bottomTime;
final bool hasActiveMoved = activeSide == ClockSide.top
? state.topMoves > 0
: state.bottomMoves > 0;
if (initialOfActive.inMilliseconds != 0 || hasActiveMoved) {
final delay = _delayFor(activeSide);
_markDelay(delay);
_activeTurnStartedWithTime = state.getDuration(activeSide).value;
_clock.startSide(activeSide.chessClockSide, delay: delay);
return null;
} else {
_markDelay(null);
_activeTurnStartedWithTime = null;
return _clock.stop();
}
}

Duration? _delayFor(ClockSide activeSide) {
return state.options.type == ClockTimeControlType.simpleDelay
? state.options.getIncrementDuration(activeSide)
: null;
}

void _applyMoveBonus(ClockSide playerType, Duration? thinkTime, Duration? turnStartedWithTime) {
final increment = state.options.getIncrementDuration(playerType);
final bonus = switch (state.options.type) {
ClockTimeControlType.increment => increment,
ClockTimeControlType.simpleDelay => Duration.zero,
ClockTimeControlType.bronsteinDelay =>
thinkTime != null && thinkTime > Duration.zero
? _minDuration(thinkTime, increment)
: Duration.zero,
};
if (bonus > Duration.zero) {
_clock.incTime(playerType.chessClockSide, bonus);
if (state.options.type == ClockTimeControlType.bronsteinDelay &&
turnStartedWithTime != null &&
state.getDuration(playerType).value > turnStartedWithTime) {
_clock.setTime(playerType.chessClockSide, turnStartedWithTime);
}
}
}

void _markDelay(Duration? delay) {
if (delay != null && delay > Duration.zero) {
_delayStartedAt = clock.clock.now();
_delayDuration = delay;
} else {
_delayStartedAt = null;
_delayDuration = null;
}
}

Duration? _remainingDelay() {
final startedAt = _delayStartedAt;
final duration = _delayDuration;
if (startedAt == null || duration == null) return null;
final remaining = duration - clock.clock.now().difference(startedAt);
return remaining > Duration.zero ? remaining : null;
}
}

Duration _minDuration(Duration a, Duration b) => a < b ? a : b;

@freezed
sealed class ClockOptions with _$ClockOptions {
const ClockOptions._();

const factory ClockOptions({
required ClockTimeControlType type,
required Duration topTime,
required Duration bottomTime,
required Duration topIncrement,
required Duration bottomIncrement,
}) = _ClockOptions;

factory ClockOptions.fromTimeIncrement(TimeIncrement timeIncrement) => ClockOptions(
factory ClockOptions.fromTimeIncrement(
TimeIncrement timeIncrement, {
ClockTimeControlType type = ClockTimeControlType.increment,
}) => ClockOptions(
type: type,
topTime: Duration(seconds: timeIncrement.time),
bottomTime: Duration(seconds: timeIncrement.time),
topIncrement: Duration(seconds: timeIncrement.increment),
Expand All @@ -245,6 +331,7 @@ sealed class ClockOptions with _$ClockOptions {
TimeIncrement playerTop,
TimeIncrement playerBottom,
) => ClockOptions(
type: ClockTimeControlType.increment,
topTime: Duration(seconds: playerTop.time),
bottomTime: Duration(seconds: playerBottom.time),
topIncrement: Duration(seconds: playerTop.increment),
Expand All @@ -255,6 +342,10 @@ sealed class ClockOptions with _$ClockOptions {
return playerType == ClockSide.top ? topIncrement.inSeconds : bottomIncrement.inSeconds;
}

Duration getIncrementDuration(ClockSide playerType) {
return playerType == ClockSide.top ? topIncrement : bottomIncrement;
}

bool hasIncrement(ClockSide playerType) {
return getIncrement(playerType) > 0;
}
Expand Down
19 changes: 13 additions & 6 deletions lib/src/model/over_the_board/over_the_board_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ class OverTheBoardPreferencesNotifier extends Notifier<OverTheBoardPrefs>
OverTheBoardPrefs get defaults => OverTheBoardPrefs.defaults;

@override
OverTheBoardPrefs fromJson(Map<String, dynamic> json) => OverTheBoardPrefs.fromJson(json);
OverTheBoardPrefs fromJson(Map<String, dynamic> json) {
final migratedJson = Map<String, dynamic>.of(json);
if (migratedJson['timeControlType'] == 'realTime' ||
migratedJson['timeControlType'] == 'increment') {
migratedJson['timeControlType'] = 'clock';
}
return OverTheBoardPrefs.fromJson(migratedJson);
}

@override
OverTheBoardPrefs build() {
Expand All @@ -49,13 +56,13 @@ class OverTheBoardPreferencesNotifier extends Notifier<OverTheBoardPrefs>
}

enum TimeControlType {
realTime,
clock,
unlimited;

String label(AppLocalizations l10n) {
switch (this) {
case TimeControlType.realTime:
return l10n.realTime;
case TimeControlType.clock:
return l10n.clock;
case TimeControlType.unlimited:
return l10n.unlimited;
}
Expand All @@ -71,14 +78,14 @@ sealed class OverTheBoardPrefs with _$OverTheBoardPrefs implements Serializable
const factory OverTheBoardPrefs({
required bool flipPiecesAfterMove,
required bool symmetricPieces,
@Default(TimeControlType.realTime) TimeControlType timeControlType,
@Default(TimeControlType.clock) TimeControlType timeControlType,
@Default(OverTheBoardPrefs._defaultTimeIncrement) TimeIncrement timeIncrement,
}) = _OverTheBoardPrefs;

static const defaults = OverTheBoardPrefs(
flipPiecesAfterMove: false,
symmetricPieces: false,
timeControlType: TimeControlType.realTime,
timeControlType: TimeControlType.clock,
timeIncrement: _defaultTimeIncrement,
);

Expand Down
11 changes: 7 additions & 4 deletions lib/src/view/clock/clock_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';
import 'package:lichess_mobile/src/model/settings/general_preferences.dart';
import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/view/play/time_control_modal.dart';
import 'package:lichess_mobile/src/view/clock/clock_tool_settings_modal.dart';

const _iconSize = 38.0;
const _kIconPadding = EdgeInsets.all(10.0);
Expand Down Expand Up @@ -67,13 +67,16 @@ class ClockSettings extends ConsumerWidget {
final options = ref.watch(
clockToolControllerProvider.select((value) => value.options),
);
return TimeControlModal(
excludeUltraBullet: true,
return ClockToolSettingsModal(
clockType: options.type,
timeIncrement: TimeIncrement(
options.bottomTime.inSeconds,
options.bottomIncrement.inSeconds,
),
onSelected: (choice) {
onClockTypeSelected: (type) {
ref.read(clockToolControllerProvider.notifier).updateClockType(type);
},
onTimeSelected: (choice) {
ref.read(clockToolControllerProvider.notifier).updateOptions(choice);
},
);
Expand Down
9 changes: 4 additions & 5 deletions lib/src/view/clock/clock_tool_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class _ClockTileState extends ConsumerState<ClockTile> with SingleTickerProvider
: () => showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => CustomClockSettings(
player: playerType,
clockType: clockState.options.type,
clock: playerType == ClockSide.top
? TimeIncrement.fromDurations(
clockState.options.topTime,
Expand All @@ -315,19 +315,18 @@ class _ClockTileState extends ConsumerState<ClockTile> with SingleTickerProvider
clockState.options.bottomTime,
clockState.options.bottomIncrement,
),
onSubmit: (ClockSide player, TimeIncrement clock) {
Navigator.of(context).pop();
onTimeSelected: (TimeIncrement clock) {
ref
.read(clockToolControllerProvider.notifier)
.updateOptionsCustom(clock, player);
.updateOptionsCustom(clock, playerType);
},
),
),
),
if (clockState.options.hasIncrement(playerType)) ...[
const SizedBox(width: 8),
Text(
'+${clockState.options.getIncrement(playerType)}',
'${clockState.options.type == ClockTimeControlType.increment ? '+' : 'd'}${clockState.options.getIncrement(playerType)}',
style: TextStyle(fontSize: 28, color: clockStyle.textColor),
),
],
Expand Down
Loading