chessground 10.0.2 copy "chessground: ^10.0.2" to clipboard
chessground: ^10.0.2 copied to clipboard

Chess board UI developed for lichess.org. It has no chess logic inside so it can be used for chess variants.

example/lib/main.dart

import 'dart:async';
import 'dart:math';
import 'package:board_example/atomic_game_page.dart';
import 'package:board_example/board_editor_page.dart';
import 'package:flutter/material.dart';
import 'package:chessground/chessground.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;
  Move? lastMove;
  PieceSet pieceSet = PieceSet.gioco;
  late ChessboardController _controller;
  late final ValueNotifier<bool> _canUndo;
  late final ValueNotifier<Position> _positionNotifier;
  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;
  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) {
    final newRoundButton = FilledButton.icon(
      icon: const Icon(Icons.refresh_rounded),
      label: const Text('New Round'),
      onPressed: () {
        position = initialPosition;
        lastPos = null;
        lastMove = null;
        _canUndo.value = false;
        _positionNotifier.value = position;
        _controller.updatePosition(_buildGame(), animate: false, resetPremove: true);
      },
    );

    final controlButtons = SizedBox(
      height: buttonHeight,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(child: newRoundButton),
          if (playMode == Mode.freePlay) const SizedBox(width: buttonsSplitter),
          if (playMode == Mode.freePlay)
            Expanded(
              child: ValueListenableBuilder<bool>(
                valueListenable: _canUndo,
                builder: (context, canUndo, _) => FilledButton.icon(
                  icon: const Icon(Icons.undo_rounded),
                  label: const Text('Undo'),
                  onPressed: canUndo
                      ? () {
                          position = lastPos!;
                          lastPos = null;
                          lastMove = null;
                          _canUndo.value = false;
                          _positionNotifier.value = position;
                          _controller.updatePosition(_buildGame(), animate: false, resetPremove: true);
                        }
                      : null,
                ),
              ),
            ),
        ],
      ),
    );

    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: [
                SettingsButton(
                    label: 'Magnify drag',
                    value: dragMagnify ? 'ON' : 'OFF',
                    onPressed: () {
                      setState(() {
                        dragMagnify = !dragMagnify;
                      });
                    }),
                SettingsButton(
                  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;
                      });
                    },
                  ),
                ),
                SettingsButton(
                  label: 'Orientation',
                  value: orientation.name,
                  onPressed: () {
                    setState(() {
                      orientation = orientation.opposite;
                    });
                  },
                ),
                SettingsButton(
                  label: 'Show border',
                  value: showBorder ? 'ON' : 'OFF',
                  onPressed: () {
                    setState(() {
                      showBorder = !showBorder;
                    });
                  },
                ),
                SettingsButton(
                  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;
                        }
                      });
                    },
                  ),
                ),
                SettingsButton(
                  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;
                        }
                      });
                    },
                  ),
                ),
                SettingsButton(
                  label: 'Piece animation',
                  value: pieceAnimation ? 'ON' : 'OFF',
                  onPressed: () {
                    setState(() {
                      pieceAnimation = !pieceAnimation;
                    });
                  },
                ),
                SettingsButton(
                  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;
                        }
                      });
                    },
                  ),
                ),
                SettingsButton(
                  label: 'Test Crazyhouse drop moves',
                  value: testDropMoves ? 'ON' : 'OFF',
                  onPressed: () {
                    testDropMoves = !testDropMoves;
                    position = initialPosition;
                    lastPos = null;
                    lastMove = null;
                    _canUndo.value = false;
                    _positionNotifier.value = position;
                    _controller.updatePosition(_buildGame(), animate: false, resetPremove: true);
                    setState(() {}); // Show/hide CrazyhouseMenu.
                  },
                ),
              ],
            ),
          ],
        ),
      ],
    );

    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),
      enableDrops: testDropMoves,
      pieceShiftMethod: pieceShiftMethod,
      autoQueenPromotionOnPremove: false,
      pieceOrientationBehavior: playMode == Mode.freePlay
          ? PieceOrientationBehavior.opponentUpsideDown
          : PieceOrientationBehavior.facingUser,
    );

    final Widget chessboardWidget = Center(
      child: LayoutBuilder(
        builder: (context, constraints) {
          final chessboard = Chessboard(
            controller: _controller,
            size: min(constraints.maxWidth, constraints.maxHeight),
            settings: settings,
            orientation: orientation,
            onMove:
                playMode == Mode.botPlay ? _onUserMoveAgainstBot : _playMove,
          );

          return Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              if (testDropMoves)
                ValueListenableBuilder<Position>(
                  valueListenable: _positionNotifier,
                  builder: (context, pos, _) => CrazyhouseMenu(
                    position: pos,
                    side: Side.black,
                    pieceSet: pieceSet,
                    squareSize: chessboard.squareSize,
                    settings: settings,
                  ),
                ),
              chessboard,
              if (testDropMoves)
                ValueListenableBuilder<Position>(
                  valueListenable: _positionNotifier,
                  builder: (context, pos, _) => CrazyhouseMenu(
                    position: pos,
                    side: Side.white,
                    pieceSet: pieceSet,
                    squareSize: chessboard.squareSize,
                    settings: settings,
                  ),
                ),
            ],
          );
        },
      ),
    );

    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: () {
              playMode = Mode.botPlay;
              _controller.updatePosition(_buildGame());
              setState(() {}); // Update app bar title and button visibility.
              if (position.turn == Side.black) {
                _playBlackMove();
              }
              Navigator.pop(context);
            },
          ),
          ListTile(
            title: const Text('Enter opponent move'),
            onTap: () {
              playMode = Mode.inputMove;
              _controller.updatePosition(_buildGame());
              setState(() {}); // Update app bar title and button visibility.
              Navigator.pop(context);
            },
          ),
          ListTile(
            title: const Text('Free Play'),
            onTap: () {
              playMode = Mode.freePlay;
              _controller.updatePosition(_buildGame());
              setState(() {}); // Update app bar title and button visibility.
              Navigator.pop(context);
            },
          ),
          ListTile(
            title: const Text('Atomic Chess'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const AtomicGamePage(),
                ),
              );
            },
          ),
          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
              ? Padding(
                  padding: const EdgeInsets.only(
                    bottom: screenPadding,
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      chessboardWidget,
                      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: controlButtons,
                        ),
                    ],
                  ),
                )
              : Padding(
                  padding: const EdgeInsets.all(screenPadding),
                  child: Row(
                    children: [
                      Expanded(
                        child: chessboardWidget,
                      ),
                      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) controlButtons,
                          ],
                        ),
                      )
                    ],
                  ),
                )),
    );
  }

  void _tryPlayPremove() {
    final move = _controller.premove;
    if (move != null) {
      _controller.premove = null;
      Timer.run(() async {
        if (move is NormalMove && _isPromotionPawnMove(move)) {
          _controller.pendingPromotion = move;
          _controller.updatePosition(_buildGame());
        } else {
          _playMove(move);
          if (playMode == Mode.botPlay) {
            await _playBlackMove();
            _tryPlayPremove();
          }
        }
      });
    }
  }

  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() {
    super.initState();
    _controller = ChessboardController(game: _buildGame());
    _canUndo = ValueNotifier(false);
    _positionNotifier = ValueNotifier(position);
  }

  @override
  void dispose() {
    _controller.dispose();
    _canUndo.dispose();
    _positionNotifier.dispose();
    super.dispose();
  }

  GameData _buildGame() {
    return GameData(
      fen: position.fen,
      lastMove: lastMove,
      playerSide: (playMode == Mode.botPlay || playMode == Mode.inputMove)
          ? PlayerSide.white
          : (position.turn == Side.white ? PlayerSide.white : PlayerSide.black),
      validMoves: makeLegalMoves(position),
      validDropSquares:
          testDropMoves ? position.legalDrops.squares.toSet() : null,
      sideToMove: position.turn == Side.white ? Side.white : Side.black,
      kingSquareInCheck:
          position.isCheck ? position.board.kingOf(position.turn) : null,
    );
  }

  void _playMove(Move move, {bool? viaDragAndDrop}) {
    if (position.isLegal(move)) {
      lastPos = position;
      position = position.playUnchecked(move);
      lastMove = move;
      _controller.updatePosition(_buildGame());
      _canUndo.value = true;
      _positionNotifier.value = position;
    }
  }

  void _onUserMoveAgainstBot(Move move, {bool? viaDragAndDrop}) async {
    if (position.isLegal(move)) {
      lastPos = position;
      position = position.playUnchecked(move);
      lastMove = move;
      _controller.updatePosition(_buildGame());
      _canUndo.value = true;
      _positionNotifier.value = position;
      await _playBlackMove();
      _tryPlayPremove();
    }
  }

  Future<void> _playBlackMove() async {
    if (position.isGameOver) return;

    final random = Random();
    await Future.delayed(Duration(milliseconds: random.nextInt(1000) + 500));
    if (!mounted) return;

    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);
      }

      position = position.playUnchecked(mv);
      lastPos = position;
      lastMove = NormalMove(from: mv.from, to: mv.to, promotion: mv.promotion);
      _controller.updatePosition(_buildGame());
      _canUndo.value = true;
      _positionNotifier.value = 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 SettingsButton extends StatelessWidget {
  const SettingsButton({
    super.key,
    required this.label,
    required this.value,
    required this.onPressed,
  });

  final String label;
  final String value;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return 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,
    );
  }
}

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(
                dragAnchorStrategy: pointerDragAnchorStrategy,
                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(),
      ),
    );
  }
}
20
likes
160
points
2.36k
downloads

Documentation

API reference

Publisher

verified publisherlichess.org

Weekly Downloads

Chess board UI developed for lichess.org. It has no chess logic inside so it can be used for chess variants.

Repository (GitHub)
View/report issues

Funding

Consider supporting this project:

lichess.org

License

GPL-3.0 (license)

Dependencies

dartchess, flutter, meta

More

Packages that depend on chessground