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
30 changes: 22 additions & 8 deletions lib/src/model/board_editor/board_editor_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,34 @@ import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/chess960.dart';

part 'board_editor_controller.freezed.dart';

typedef BoardEditorControllerParams = ({Variant initialVariant, String? initialFen});

/// A provider for [BoardEditorController].
final boardEditorControllerProvider = NotifierProvider.autoDispose
.family<BoardEditorController, BoardEditorState, String?>(
.family<BoardEditorController, BoardEditorState, BoardEditorControllerParams?>(
BoardEditorController.new,
name: 'BoardEditorControllerProvider',
);

class BoardEditorController extends Notifier<BoardEditorState> {
BoardEditorController(this.initialFen);
BoardEditorController(this.params);

final String? initialFen;
final BoardEditorControllerParams? params;

@override
BoardEditorState build() {
final setup = Setup.parseFen(initialFen ?? kInitialFEN);
final position = Chess.fromSetup(setup, ignoreImpossibleCheck: true);
final pieces = readFen(initialFen ?? kInitialFEN).lock;
final variant = params?.initialVariant ?? Variant.standard;
final fen =
params?.initialFen ??
(variant == Variant.chess960 ? randomChess960Position() : variant.initialPosition).fen;
final setup = Setup.parseFen(fen);
final position = Position.setupPosition(variant.rule, setup, ignoreImpossibleCheck: true);
final pieces = readFen(fen).lock;

final castlingRights = IMap({
CastlingRight.whiteKing: position.castles.rookOf(Side.white, CastlingSide.king) != null,
Expand All @@ -33,6 +41,7 @@ class BoardEditorController extends Notifier<BoardEditorState> {
return BoardEditorState(
orientation: Side.white,
sideToPlay: setup.turn,
variant: variant,
pieces: pieces,
castlingRights: castlingRights,
editorPointerMode: EditorPointerMode.drag,
Expand Down Expand Up @@ -131,6 +140,10 @@ class BoardEditorController extends Notifier<BoardEditorState> {
state = state.copyWith(enPassantSquare: state.enPassantSquare == square ? null : square);
}

void setVariant(Variant variant) {
state = state.copyWith(variant: variant);
}

void _updatePosition(IMap<Square, Piece> pieces) {
state = state.copyWith(
pieces: pieces,
Expand Down Expand Up @@ -175,6 +188,7 @@ sealed class BoardEditorState with _$BoardEditorState {
const factory BoardEditorState({
required Side orientation,
required Side sideToPlay,
required Variant variant,
required IMap<Square, Piece> pieces,
required IMap<CastlingRight, bool> castlingRights,
required EditorPointerMode editorPointerMode,
Expand Down Expand Up @@ -261,9 +275,9 @@ sealed class BoardEditorState with _$BoardEditorState {
/// Returns `null` if the position is invalid.
String? get pgn {
try {
final position = Chess.fromSetup(Setup.parseFen(fen));
final position = Position.setupPosition(variant.rule, Setup.parseFen(fen));
return PgnGame(
headers: {'FEN': position.fen},
headers: {'FEN': position.fen, 'Variant': variant.label},
moves: PgnNode<PgnNodeData>(),
comments: [],
).makePgn();
Expand Down
9 changes: 9 additions & 0 deletions lib/src/model/common/chess.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ enum Variant {
final String label;
final IconData icon;

bool sideCanCastle(Side side) {
if (this == Variant.racingKings) return false;
if (this == Variant.antichess) return false;
if (side == Side.white && this == Variant.horde) return false;
return true;
}

bool get hasEnPassant => this != Variant.racingKings;

bool get isReadSupported => readSupportedVariants.contains(this);

bool get isPlaySupported => playSupportedVariants.contains(this);
Expand Down
10 changes: 8 additions & 2 deletions lib/src/model/common/chess960.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import 'package:dartchess/dartchess.dart';

final _random = Random.secure();

Position randomChess960Position() {
final rank8 = _positions[_random.nextInt(_positions.length)];
Position chess960Position(int index) {
if (index < 0 || index >= _positions.length) {
throw ArgumentError('Index must be between 0 and ${_positions.length - 1}');
}

final rank8 = _positions[index];

return Chess.fromSetup(
Setup.parseFen('$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()} w KQkq - 0 1'),
);
}

Position randomChess960Position() => chess960Position(_random.nextInt(_positions.length));

// https://github.com/lichess-org/scalachess/blob/bd139c6dc1acdc8fff08c46e412f784d49a16578/core/src/main/scala/variant/Chess960.scala#L49
final _positions = [
'bbqnnrkr',
Expand Down
34 changes: 9 additions & 25 deletions lib/src/view/analysis/analysis_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart';
import 'package:lichess_mobile/src/widgets/bottom_bar.dart';
import 'package:lichess_mobile/src/widgets/buttons.dart';
import 'package:lichess_mobile/src/widgets/feedback.dart';
import 'package:lichess_mobile/src/widgets/misc.dart';
import 'package:lichess_mobile/src/widgets/platform_context_menu_button.dart';
import 'package:lichess_mobile/src/widgets/user.dart';
import 'package:lichess_mobile/src/widgets/variant_app_bar_title.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -193,7 +193,7 @@ class _AnalysisScreenState extends ConsumerState<_AnalysisScreen>
resizeToAvoidBottomInset: false,
appBar: AppBar(
centerTitle: false,
title: _Title(variant: value.variant),
title: VariantAppBarTitle(variant: value.variant, title: context.l10n.analysis),
actions: appBarActions,
),
body: _Body(options: widget.options, controller: _tabController),
Expand All @@ -207,7 +207,7 @@ class _AnalysisScreenState extends ConsumerState<_AnalysisScreen>
resizeToAvoidBottomInset: false,
appBar: AppBar(
centerTitle: false,
title: const _Title(variant: Variant.standard),
title: VariantAppBarTitle(variant: Variant.standard, title: context.l10n.analysis),
actions: appBarActions,
),
body: const Center(child: CircularProgressIndicator.adaptive()),
Expand Down Expand Up @@ -275,25 +275,6 @@ class _AnalysisMenu extends ConsumerWidget {
}
}

class _Title extends StatelessWidget {
const _Title({required this.variant});

final Variant variant;

static const excludedIcons = [Variant.standard, Variant.fromPosition];

@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!excludedIcons.contains(variant)) ...[Icon(variant.icon), const SizedBox(width: 5.0)],
Flexible(child: AppBarTitleText(context.l10n.analysis)),
],
);
}
}

class _Body extends ConsumerWidget {
const _Body({required this.options, required this.controller});

Expand Down Expand Up @@ -677,9 +658,12 @@ class _BottomBar extends ConsumerWidget {
makeLabel: (context) => Text(context.l10n.boardEditor),
onPressed: () {
final boardFen = analysisState.currentPosition.fen;
Navigator.of(
context,
).push(BoardEditorScreen.buildRoute(context, initialFen: boardFen));
Navigator.of(context).push(
BoardEditorScreen.buildRoute(context, (
initialVariant: analysisState.variant,
initialFen: boardFen,
)),
);
},
),
if (analysisState.isComputerAnalysisAllowed)
Expand Down
102 changes: 57 additions & 45 deletions lib/src/view/board_editor/board_editor_filters.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:dartchess/dartchess.dart' hide Position;
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart';
Expand All @@ -7,15 +8,19 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart';

class BoardEditorFilters extends ConsumerWidget {
const BoardEditorFilters({required this.initialFen, super.key});
const BoardEditorFilters({required this.params, super.key});

final String? initialFen;
final BoardEditorControllerParams? params;

@override
Widget build(BuildContext context, WidgetRef ref) {
final editorController = boardEditorControllerProvider(initialFen);
final editorController = boardEditorControllerProvider(params);
final editorState = ref.watch(editorController);

final castlingSide = Side.values
.where((side) => editorState.variant.sideCanCastle(side))
.toIList();

return BottomSheetScrollableContainer(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
children: [
Expand All @@ -36,53 +41,60 @@ class BoardEditorFilters extends ConsumerWidget {
}).toList(),
),
),
Padding(
padding: Styles.bodySectionPadding,
child: Text(context.l10n.castling, style: Styles.title),
),
...Side.values.map((side) {
return Padding(
padding: Styles.horizontalBodyPadding,
child: Row(
spacing: 8.0,
children: [
SizedBox(
width: 100.0,
child: Text(
side == Side.white ? context.l10n.white : context.l10n.black,
maxLines: 1,
overflow: TextOverflow.ellipsis,
if (castlingSide.isNotEmpty) ...[
Padding(
padding: Styles.bodySectionPadding,
child: Text(context.l10n.castling, style: Styles.title),
),
...Side.values.where((side) => editorState.variant.sideCanCastle(side)).map((side) {
return Padding(
padding: Styles.horizontalBodyPadding,
child: Row(
spacing: 8.0,
children: [
SizedBox(
width: 100.0,
child: Text(
side == Side.white ? context.l10n.white : context.l10n.black,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
...[CastlingSide.king, CastlingSide.queen].map((castlingSide) {
return ChoiceChip(
label: Text(castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O'),
selected: editorState.isCastlingAllowed(side, castlingSide),
onSelected: (selected) {
ref.read(editorController.notifier).setCastling(side, castlingSide, selected);
},
);
}),
],
),
);
}),
if (editorState.enPassantOptions.isNotEmpty) ...[
...[CastlingSide.king, CastlingSide.queen].map((castlingSide) {
return ChoiceChip(
label: Text(castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O'),
selected: editorState.isCastlingAllowed(side, castlingSide),
onSelected: (selected) {
ref
.read(editorController.notifier)
.setCastling(side, castlingSide, selected);
},
);
}),
],
),
);
}),
],
if (editorState.variant.hasEnPassant && editorState.enPassantOptions.isNotEmpty) ...[
const Padding(
padding: Styles.bodySectionPadding,
child: Text('En passant', style: Styles.subtitle),
),
Wrap(
spacing: 8.0,
children: editorState.enPassantOptions.squares.map((square) {
return ChoiceChip(
label: Text(square.name),
selected: editorState.enPassantSquare == square,
onSelected: (selected) {
ref.read(editorController.notifier).toggleEnPassantSquare(square);
},
);
}).toList(),
Padding(
padding: Styles.horizontalBodyPadding,
child: Wrap(
spacing: 8.0,
children: editorState.enPassantOptions.squares.map((square) {
return ChoiceChip(
label: Text(square.name),
selected: editorState.enPassantSquare == square,
onSelected: (selected) {
ref.read(editorController.notifier).toggleEnPassantSquare(square);
},
);
}).toList(),
),
),
],
],
Expand Down
Loading