chessground 8.0.1
chessground: ^8.0.1 copied to clipboard
Chess board UI developed for lichess.org. It has no chess logic inside so it can be used for chess variants.
import 'dart:async';
import 'dart:math';
import 'package:board_example/board_editor_page.dart';
import 'package:flutter/material.dart';
import 'package:chessground/chessground.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:dartchess/dartchess.dart';
import 'board_theme.dart';
import 'board_thumbnails.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chessground Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
home: const HomePage(title: 'Chessground Demo'),
);
}
}
String pieceShiftMethodLabel(PieceShiftMethod method) {
switch (method) {
case PieceShiftMethod.drag:
return 'Drag';
case PieceShiftMethod.tapTwoSquares:
return 'Tap two squares';
case PieceShiftMethod.either:
return 'Either';
}
}
enum Mode {
botPlay,
inputMove,
freePlay,
}
const screenPadding = 16.0;
const screenPortraitSplitter = screenPadding / 2;
const screenLandscapeSplitter = screenPadding;
const buttonHeight = 50.0;
const buttonsSplitter = screenPadding;
const smallButtonsSplitter = screenPadding / 2;
class HomePage extends StatefulWidget {
const HomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
Position position = Chess.initial;
Side orientation = Side.white;
String fen = kInitialBoardFEN;
Move? lastMove;
NormalMove? promotionMove;
Move? premove;
ValidMoves validMoves = IMap(const {});
Side sideToMove = Side.white;
PieceSet pieceSet = PieceSet.gioco;
PieceShiftMethod pieceShiftMethod = PieceShiftMethod.either;
DragTargetKind dragTargetKind = DragTargetKind.circle;
BoardTheme boardTheme = BoardTheme.brown;
bool drawMode = true;
bool pieceAnimation = true;
bool dragMagnify = true;
bool testDropMoves = false;
Mode playMode = Mode.botPlay;
Position? lastPos;
ISet<Shape> shapes = ISet();
bool showBorder = false;
Position get initialPosition => testDropMoves
? Crazyhouse.initial.copyWith(
// Fill initial pockets for easier testing
pockets: Pockets.empty
.increment(Side.white, Role.pawn)
.increment(Side.white, Role.knight)
.increment(Side.white, Role.bishop)
.increment(Side.white, Role.rook)
.increment(Side.white, Role.queen))
: Chess.initial;
@override
Widget build(BuildContext context) {
Widget _buildNewRoundButton() => FilledButton.icon(
icon: const Icon(Icons.refresh_rounded),
label: const Text('New Round'),
onPressed: () {
setState(() {
position = initialPosition;
fen = position.fen;
validMoves = makeLegalMoves(position);
lastMove = null;
lastPos = null;
});
});
Widget _buildUndoButton() => FilledButton.icon(
icon: const Icon(Icons.undo_rounded),
label: const Text('Undo'),
onPressed: lastPos != null
? () => setState(() {
position = lastPos!;
fen = position.fen;
validMoves = makeLegalMoves(position);
lastPos = null;
})
: null,
);
Widget _buildControlButtons() => SizedBox(
height: buttonHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: _buildNewRoundButton(),
),
if (playMode == Mode.freePlay)
const SizedBox(width: buttonsSplitter),
if (playMode == Mode.freePlay)
Expanded(
child: _buildUndoButton(),
),
],
),
);
Widget _buildSettingsButton({
required String label,
required String value,
required VoidCallback onPressed,
}) =>
ElevatedButton(
child: Column(
children: [
Text(label,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
)),
Text(value,
style: const TextStyle(
fontSize: 12,
)),
],
),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 18.0,
vertical: 4,
),
),
onPressed: onPressed,
);
final settingsWidgets = ListView(
children: [
ExpansionTile(
title: const Text('Settings'),
initiallyExpanded: true,
shape: const RoundedRectangleBorder(),
minTileHeight: 0,
children: [
Wrap(
spacing: smallButtonsSplitter,
runSpacing: smallButtonsSplitter,
alignment: WrapAlignment.spaceBetween,
children: [
_buildSettingsButton(
label: 'Magnify drag',
value: dragMagnify ? 'ON' : 'OFF',
onPressed: () {
setState(() {
dragMagnify = !dragMagnify;
});
}),
_buildSettingsButton(
label: 'Drag target',
value: dragTargetKind.name,
onPressed: () => _showChoicesPicker<DragTargetKind>(
context,
choices: DragTargetKind.values,
selectedItem: dragTargetKind,
labelBuilder: (t) => Text(t.name),
onSelectedItemChanged: (DragTargetKind value) {
setState(() {
dragTargetKind = value;
});
},
),
),
_buildSettingsButton(
label: 'Orientation',
value: orientation.name,
onPressed: () {
setState(() {
orientation = orientation.opposite;
});
},
),
_buildSettingsButton(
label: 'Show border',
value: showBorder ? 'ON' : 'OFF',
onPressed: () {
setState(() {
showBorder = !showBorder;
});
},
),
_buildSettingsButton(
label: 'Piece set',
value: pieceSet.label,
onPressed: () => _showChoicesPicker<PieceSet>(
context,
choices: PieceSet.values,
selectedItem: pieceSet,
labelBuilder: (t) => Text(t.label),
onSelectedItemChanged: (PieceSet? value) {
setState(() {
if (value != null) {
pieceSet = value;
}
});
},
),
),
_buildSettingsButton(
label: 'Board theme',
value: boardTheme.label,
onPressed: () => _showChoicesPicker<BoardTheme>(
context,
choices: BoardTheme.values,
selectedItem: boardTheme,
labelBuilder: (t) => Text(t.label),
onSelectedItemChanged: (BoardTheme? value) {
setState(() {
if (value != null) {
boardTheme = value;
}
});
},
),
),
_buildSettingsButton(
label: 'Piece animation',
value: pieceAnimation ? 'ON' : 'OFF',
onPressed: () {
setState(() {
pieceAnimation = !pieceAnimation;
});
},
),
_buildSettingsButton(
label: 'Piece Shift',
value: pieceShiftMethodLabel(pieceShiftMethod),
onPressed: () => _showChoicesPicker<PieceShiftMethod>(
context,
choices: PieceShiftMethod.values,
selectedItem: pieceShiftMethod,
labelBuilder: (t) => Text(pieceShiftMethodLabel(t)),
onSelectedItemChanged: (PieceShiftMethod? value) {
setState(() {
if (value != null) {
pieceShiftMethod = value;
}
});
},
),
),
_buildSettingsButton(
label: 'Test Drop Moves',
value: testDropMoves ? 'ON' : 'OFF',
onPressed: () {
setState(() {
testDropMoves = !testDropMoves;
position = initialPosition;
});
}),
],
),
],
),
],
);
final inputMoveWidgets = TextField(
decoration: const InputDecoration(
labelText: 'Enter move in UCI format',
border: OutlineInputBorder(),
),
onSubmitted: (String value) {
final move = NormalMove.fromUci(value);
_playMove(move);
_tryPlayPremove();
},
);
final settings = ChessboardSettings(
pieceAssets: pieceSet.assets,
colorScheme: boardTheme.colors,
border: showBorder
? BoardBorder(
width: 16.0,
color: _darken(boardTheme.colors.darkSquare, 0.2),
)
: null,
enableCoordinates: true,
animationDuration:
pieceAnimation ? const Duration(milliseconds: 200) : Duration.zero,
dragFeedbackScale: dragMagnify ? 2.0 : 1.0,
dragTargetKind: dragTargetKind,
drawShape: DrawShapeOptions(
enable: drawMode,
onCompleteShape: _onCompleteShape,
onClearShapes: () {
setState(() {
shapes = ISet();
});
},
),
pieceShiftMethod: pieceShiftMethod,
autoQueenPromotionOnPremove: false,
pieceOrientationBehavior: playMode == Mode.freePlay
? PieceOrientationBehavior.opponentUpsideDown
: PieceOrientationBehavior.facingUser,
enableDropMoves: testDropMoves,
);
Widget _buildChessBoardWidget() => Center(
child: LayoutBuilder(
builder: (context, constraints) {
final chessboard = Chessboard(
size: min(constraints.maxWidth, constraints.maxHeight),
settings: settings,
orientation: orientation,
fen: fen,
lastMove: lastMove,
game: GameData(
playerSide:
(playMode == Mode.botPlay || playMode == Mode.inputMove)
? PlayerSide.white
: (position.turn == Side.white
? PlayerSide.white
: PlayerSide.black),
validMoves: validMoves,
sideToMove:
position.turn == Side.white ? Side.white : Side.black,
isCheck: position.isCheck,
promotionMove: promotionMove,
onMove: playMode == Mode.botPlay
? _onUserMoveAgainstBot
: _playMove,
onPromotionSelection: _onPromotionSelection,
premovable: (
onSetPremove: _onSetPremove,
premove: premove,
),
),
shapes: shapes.isNotEmpty ? shapes : null,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (testDropMoves)
CrazyhouseMenu(
position: position,
side: Side.black,
pieceSet: pieceSet,
squareSize: chessboard.squareSize,
settings: settings,
),
chessboard,
if (testDropMoves)
CrazyhouseMenu(
position: position,
side: Side.white,
pieceSet: pieceSet,
squareSize: chessboard.squareSize,
settings: settings,
),
],
);
},
),
);
Widget _buildPortrait() => Padding(
padding: const EdgeInsets.only(
bottom: screenPadding,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildChessBoardWidget(),
if (playMode == Mode.inputMove)
const SizedBox(height: screenPortraitSplitter),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: screenPadding,
),
child: playMode == Mode.inputMove
? inputMoveWidgets
: settingsWidgets,
),
),
if (playMode != Mode.inputMove)
const SizedBox(height: screenPortraitSplitter),
if (playMode != Mode.inputMove)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: screenPadding,
),
child: _buildControlButtons(),
),
],
),
);
Widget _buildLandscape() => Padding(
padding: const EdgeInsets.all(screenPadding),
child: Row(
children: [
Expanded(
child: _buildChessBoardWidget(),
),
const SizedBox(width: screenLandscapeSplitter),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: playMode == Mode.inputMove
? inputMoveWidgets
: settingsWidgets,
),
if (playMode != Mode.inputMove)
const SizedBox(height: screenPortraitSplitter),
if (playMode != Mode.inputMove) _buildControlButtons(),
],
),
)
],
),
);
return Scaffold(
primary: MediaQuery.of(context).orientation == Orientation.portrait,
appBar: AppBar(
title: switch (playMode) {
Mode.botPlay => const Text('Random Bot'),
Mode.inputMove => const Text('Enter opponent move'),
Mode.freePlay => const Text('Free Play'),
}),
drawer: Drawer(
child: ListView(
children: [
ListTile(
title: const Text('Random Bot'),
onTap: () {
setState(() {
playMode = Mode.botPlay;
});
if (position.turn == Side.black) {
_playBlackMove();
}
Navigator.pop(context);
},
),
ListTile(
title: const Text('Enter opponent move'),
onTap: () {
setState(() {
playMode = Mode.inputMove;
});
Navigator.pop(context);
},
),
ListTile(
title: const Text('Free Play'),
onTap: () {
setState(() {
playMode = Mode.freePlay;
});
Navigator.pop(context);
},
),
ListTile(
title: const Text('Board Editor'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BoardEditorPage(),
),
);
},
),
ListTile(
title: const Text('Board Thumbnails'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BoardThumbnailsPage(),
),
);
},
),
],
)),
body: OrientationBuilder(
builder: (context, orientation) => orientation == Orientation.portrait
? _buildPortrait()
: _buildLandscape(),
),
);
}
void _tryPlayPremove() {
if (premove != null) {
Timer.run(() {
_playMove(premove!, isPremove: true);
});
}
}
void _onCompleteShape(Shape shape) {
if (shapes.any((element) => element == shape)) {
setState(() {
shapes = shapes.remove(shape);
});
return;
} else {
setState(() {
shapes = shapes.add(shape);
});
}
}
void _showChoicesPicker<T extends Enum>(
BuildContext context, {
required List<T> choices,
required T selectedItem,
required Widget Function(T choice) labelBuilder,
required void Function(T choice) onSelectedItemChanged,
}) {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
contentPadding: const EdgeInsets.only(top: 12),
scrollable: true,
content: RadioGroup<T>(
groupValue: selectedItem,
onChanged: (value) {
if (value != null) onSelectedItemChanged(value);
Navigator.of(context).pop();
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: choices.map((value) {
return RadioListTile<T>(
title: labelBuilder(value),
value: value,
);
}).toList(growable: false),
),
),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
@override
void initState() {
validMoves = makeLegalMoves(position);
super.initState();
}
void _onSetPremove(Move? move) {
setState(() {
premove = move;
});
}
void _onPromotionSelection(Role? role) {
if (role == null) {
_onPromotionCancel();
} else if (promotionMove != null) {
if (playMode == Mode.botPlay) {
_onUserMoveAgainstBot(promotionMove!.withPromotion(role));
} else {
_playMove(promotionMove!.withPromotion(role));
}
}
}
void _onPromotionCancel() {
setState(() {
promotionMove = null;
});
}
void _playMove(Move move, {bool? viaDragAndDrop, bool? isPremove}) {
lastPos = position;
if (move is NormalMove && isPromotionPawnMove(move)) {
setState(() {
promotionMove = move;
});
} else if (position.isLegal(move)) {
setState(() {
position = position.playUnchecked(move);
lastMove = move;
fen = position.fen;
validMoves = makeLegalMoves(position);
promotionMove = null;
if (isPremove == true) {
premove = null;
}
});
}
}
void _onUserMoveAgainstBot(Move move, {viaDragAndDrop}) async {
lastPos = position;
if (move is NormalMove && isPromotionPawnMove(move)) {
setState(() {
promotionMove = move;
});
} else {
setState(() {
position = position.playUnchecked(move);
lastMove = move;
fen = position.fen;
validMoves = IMap(const {});
promotionMove = null;
});
await _playBlackMove();
_tryPlayPremove();
}
}
Future<void> _playBlackMove() async {
Future.delayed(const Duration(milliseconds: 100)).then((value) {
setState(() {});
});
if (position.isGameOver) return;
final random = Random();
await Future.delayed(Duration(milliseconds: random.nextInt(1000) + 500));
final allMoves = [
for (final entry in position.legalMoves.entries)
for (final dest in entry.value.squares)
NormalMove(from: entry.key, to: dest)
];
if (allMoves.isNotEmpty) {
NormalMove mv = (allMoves..shuffle()).first;
// Auto promote to a random non-pawn role
if (isPromotionPawnMove(mv)) {
final potentialRoles =
Role.values.where((role) => role != Role.pawn).toList();
final role = potentialRoles[random.nextInt(potentialRoles.length)];
mv = mv.withPromotion(role);
}
setState(() {
position = position.playUnchecked(mv);
lastMove =
NormalMove(from: mv.from, to: mv.to, promotion: mv.promotion);
fen = position.fen;
validMoves = makeLegalMoves(position);
});
lastPos = position;
}
}
bool isPromotionPawnMove(NormalMove move) {
return move.promotion == null &&
position.board.roleAt(move.from) == Role.pawn &&
((move.to.rank == Rank.first && position.turn == Side.black) ||
(move.to.rank == Rank.eighth && position.turn == Side.white));
}
}
Color _darken(Color c, [double amount = .1]) {
assert(amount >= 0 && amount <= 1);
return Color.lerp(c, const Color(0xFF000000), amount) ?? c;
}
class CrazyhouseMenu extends StatelessWidget {
const CrazyhouseMenu({
super.key,
required this.side,
required this.pieceSet,
required this.squareSize,
required this.settings,
required this.position,
});
final Side side;
final PieceSet pieceSet;
final double squareSize;
final Position position;
final ChessboardSettings settings;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: Role.values.where((role) => role != Role.king).map(
(role) {
final piece = Piece(role: role, color: side);
final hasPieceInPocket =
(position.pockets?.of(side, role) ?? 0) > 0;
return IgnorePointer(
ignoring: !hasPieceInPocket,
child: Draggable(
data: piece,
feedback: PieceDragFeedback(
scale: settings.dragFeedbackScale,
squareSize: squareSize,
piece: piece,
pieceAssets: pieceSet.assets,
),
child: PieceWidget(
piece: piece,
size: squareSize,
pieceAssets: pieceSet.assets,
opacity: hasPieceInPocket
? null
: const AlwaysStoppedAnimation(0.3),
),
),
);
},
).toList(),
),
);
}
}