From fd6a78360aa3d89e5e1e7ee4efd5dd25638a965d Mon Sep 17 00:00:00 2001 From: Noah <78898963+HaonRekcef@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:38:59 +0100 Subject: [PATCH] challenge oddbots --- .../view/play/challenge_odd_bots_screen.dart | 302 ++++++++++++++++++ lib/src/view/play/online_bots_screen.dart | 17 +- lib/src/view/user/user_screen.dart | 9 +- 3 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 lib/src/view/play/challenge_odd_bots_screen.dart diff --git a/lib/src/view/play/challenge_odd_bots_screen.dart b/lib/src/view/play/challenge_odd_bots_screen.dart new file mode 100644 index 0000000000..296a27651f --- /dev/null +++ b/lib/src/view/play/challenge_odd_bots_screen.dart @@ -0,0 +1,302 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_layout_grid/flutter_layout_grid.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +class ChallengeOddBotsScreen extends StatelessWidget { + const ChallengeOddBotsScreen(this.bot); + + final LightUser bot; + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.challengeChallengesX(bot.name))), + body: _ChallengeBody(bot), + ); + } +} + +class _ChallengeBody extends ConsumerStatefulWidget { + const _ChallengeBody(this.bot); + + final LightUser bot; + + @override + ConsumerState<_ChallengeBody> createState() => _ChallengeBodyState(); +} + +class _BotFen { + final String fen; + final Side side; + + _BotFen({required this.fen, required this.side}); +} + +final Map> _botFens = { + 'leelaknightodds': [ + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKB1R w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R1BQKBNR w KQkq', side: Side.black), + ], + 'leelaqueenodds': [ + _BotFen(fen: 'rnb1kbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB1KBNR w KQkq', side: Side.black), + ], + 'leelaqueenforknight': [ + _BotFen(fen: 'r1bqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB1KBNR w KQkq', side: Side.white), + _BotFen(fen: 'r1bqkbnr/pppppppp/8/8/8/8/8/PPPPPPPP/RNB1KBNR w KQkq', side: Side.black), + ], + 'leelarookodds': [ + _BotFen(fen: '1nbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQk', side: Side.white), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBNR w Kkq', side: Side.black), + ], + 'leelapieceodds': [ + _BotFen(fen: 'r1bqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: 'rn1qk1nr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: '1nbqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQk', side: Side.white), + _BotFen(fen: '1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQ', side: Side.white), + _BotFen(fen: 'r2qk1nr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: '2bqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQk', side: Side.white), + _BotFen(fen: '1n1qk1nr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQk', side: Side.white), + _BotFen(fen: 'rnb1kb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: 'r2qk2r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: '1nb1kbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQk', side: Side.white), + _BotFen(fen: 'r1b1kb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: 'rn2k1nr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq', side: Side.white), + _BotFen(fen: '1nb1kb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQk', side: Side.white), + _BotFen(fen: '1nb1kbn1/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQ', side: Side.white), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R1BQKB1R w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RN1QK1NR w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKB1R w Kkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1 w kq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R2QK1NR w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/2BQKB1R w Kkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1N1QK1NR w Kkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB1KB1R w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R2QK2R w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NB1KBNR w Kkq - 0 1', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R1B1KB1R w KQkq - 0 1', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RN2K1NR w KQkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NB1KB1R w Kkq', side: Side.black), + _BotFen(fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NB1KBN1 w kq', side: Side.black), + ], +}; +final oddBots = _botFens.keys; + +class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { + String? fen; + SideChoice sideChoice = SideChoice.white; + + @override + Widget build(BuildContext context) { + final preferences = ref.watch(challengePreferencesProvider); + + //special bots have a shorter range of time controls, to prevent an error of the slider we need to check if the time stored in the preferences is within the range of the slider + int seconds = + (preferences.clock.time.inSeconds < 60 || preferences.clock.time.inSeconds > 15 * 60) + ? 300 + : preferences.clock.time.inSeconds; + int incrementSeconds = + preferences.clock.increment.inSeconds > 10 ? 10 : preferences.clock.increment.inSeconds; + + return Center( + child: ListView( + shrinkWrap: true, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.sectionBottomPadding + : Styles.verticalBodyPadding, + children: [ + Builder( + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text.rich( + TextSpan( + text: '${context.l10n.minutesPerSide}: ', + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: clockLabelInMinutes(seconds), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: seconds, + values: List.generate(15, (i) => (i + 1) * 60), + labelBuilder: clockLabelInMinutes, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + seconds = value.toInt(); + }); + } + : null, + onChangeEnd: (num value) { + setState(() { + seconds = value.toInt(); + }); + }, + ), + ); + }, + ); + }, + ), + Builder( + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text.rich( + TextSpan( + text: '${context.l10n.incrementInSeconds}: ', + children: [ + TextSpan( + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: incrementSeconds.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: incrementSeconds, + values: List.generate(11, (i) => i), + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + incrementSeconds = value.toInt(); + }); + } + : null, + onChangeEnd: (num value) { + setState(() { + incrementSeconds = value.toInt(); + }); + }, + ), + ); + }, + ); + }, + ), + LayoutBuilder( + builder: (context, constraints) { + final crossAxisCount = + constraints.maxWidth > 600 + ? 4 + : constraints.maxWidth > 450 + ? 3 + : 2; + const sidePadding = 16.0; + const double borderWidth = 3.0; + final boardWidth = + (constraints.maxWidth - + (sidePadding * (crossAxisCount - 1)) - + (2 * sidePadding) - + (2 * borderWidth * crossAxisCount)) / + crossAxisCount; + const borderRadius = 4.0 + borderWidth; + + final userBotFens = _botFens[widget.bot.name.toLowerCase()] ?? []; + final rowCount = (userBotFens.length / crossAxisCount).ceil(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: sidePadding), + child: LayoutGrid( + columnSizes: List.generate(crossAxisCount, (_) => 1.fr), + rowSizes: List.generate(rowCount, (_) => auto), + rowGap: 16, + columnGap: sidePadding, + children: + userBotFens.map((botFen) { + return GestureDetector( + onTap: () { + setState(() { + fen = botFen.fen; + sideChoice = + botFen.side == Side.white ? SideChoice.white : SideChoice.black; + }); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: + fen == botFen.fen + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + width: borderWidth, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + child: BoardThumbnail( + size: boardWidth, + orientation: botFen.side, + fen: botFen.fen, + ), + ), + ); + }).toList(), + ), + ); + }, + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: FatButton( + semanticsLabel: context.l10n.challengeChallengeToPlay, + onPressed: + fen != null + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + challenge: ChallengeRequest( + destUser: widget.bot, + variant: Variant.fromPosition, + timeControl: ChallengeTimeControlType.clock, + clock: ( + time: Duration(seconds: seconds), + increment: Duration(seconds: incrementSeconds), + ), + rated: false, + sideChoice: sideChoice, + initialFen: fen, + ), + ); + }, + ); + } + : null, + child: Text(context.l10n.challengeChallengeToPlay, style: Styles.bold), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 3961253ee8..06eb8c900c 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/play/challenge_odd_bots_screen.dart'; import 'package:lichess_mobile/src/view/play/create_challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -22,16 +23,9 @@ import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:linkify/linkify.dart'; import 'package:url_launcher/url_launcher.dart'; -// TODO(#796): remove when Leela featured bots special challenges are ready -// https://github.com/lichess-org/mobile/issues/796 -const _disabledBots = {'leelaknightodds', 'leelaqueenodds', 'leelaqueenforknight', 'leelarookodds'}; - final _onlineBotsProvider = FutureProvider.autoDispose>((ref) async { return ref.withClientCacheFor( - (client) => UserRepository(client).getOnlineBots().then( - (bots) => - bots.whereNot((bot) => _disabledBots.contains(bot.id.value.toLowerCase())).toIList(), - ), + (client) => UserRepository(client).getOnlineBots().then((bots) => bots.toIList()), const Duration(hours: 5), ); }); @@ -129,10 +123,15 @@ class _Body extends ConsumerWidget { ); return; } + final isOddBot = oddBots.contains(bot.lightUser.name.toLowerCase()); pushPlatformRoute( context, title: context.l10n.challengeChallengesX(bot.lightUser.name), - builder: (context) => CreateChallengeScreen(bot.lightUser), + builder: + (context) => + isOddBot + ? ChallengeOddBotsScreen(bot.lightUser) + : CreateChallengeScreen(bot.lightUser), ); }, onLongPress: () { diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 416ed1a3d9..ac0ae44952 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/play/challenge_odd_bots_screen.dart'; import 'package:lichess_mobile/src/view/play/create_challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; @@ -116,9 +117,15 @@ class _UserProfileListView extends ConsumerWidget { title: Text(context.l10n.challengeChallengeToPlay), leading: const Icon(LichessIcons.crossed_swords), onTap: () { + final isOddBot = oddBots.contains(user.lightUser.name.toLowerCase()); pushPlatformRoute( context, - builder: (context) => CreateChallengeScreen(user.lightUser), + title: context.l10n.challengeChallengesX(user.lightUser.name), + builder: + (context) => + isOddBot + ? ChallengeOddBotsScreen(user.lightUser) + : CreateChallengeScreen(user.lightUser), ); }, ),