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
302 changes: 302 additions & 0 deletions lib/src/view/play/challenge_odd_bots_screen.dart
Original file line number Diff line number Diff line change
@@ -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<String, List<_BotFen>> _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),
),
),
],
),
);
}
}
17 changes: 8 additions & 9 deletions lib/src/view/play/online_bots_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<IList<User>>((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),
);
});
Expand Down Expand Up @@ -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: () {
Expand Down
9 changes: 8 additions & 1 deletion lib/src/view/user/user_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
);
},
),
Expand Down
Loading