Узнайте, как добавить играбельную шахматную партию в приложение чата Flutter.

Введение

Шахматы — игра стара как мир — и все же большинство людей не согласны с тем, идет ли король вправо или влево.

Шахматы перешли от традиционной доски к нашим экранам. Он довольно часто появляется в чат-приложениях, поскольку большинство людей знают хотя бы основы игры.

Потратив несколько лет на то, чтобы переходить от одного длинного турнира к другому, я определенно знаю, куда идет король, и к тому же я работаю в Stream. Эта интуиция открывает некоторые интересные возможности.

Добавление дополнительных функций в чат — будь то игры, такие как Messenger или визуальные эффекты, такие как в iMessage, — улучшает удержание пользователей и делает общение в чате более запоминающимся.

В этой статье мы будем использовать Stream Chat Flutter SDK в качестве базового интерфейса чата, в который мы можем добавить игру в шахматы. SDK позволяет легко добавлять собственные вложения в чат в приложении.

Готовый? Давайте кодировать.

Планирование нашего приложения

В большинстве реализаций шахматных игр в чате в приложении интерфейс довольно сложен: обычно он состоит из рисования новой шахматной доски каждый раз, когда игрок делает ход. Из-за этого канал заполняется шахматными досками, а уже отправленные сообщения становятся довольно труднодоступными.

По этой причине в этом руководстве основное внимание уделяется построению шахматной доски с игрой, в которую можно играть с помощью одного сообщения в чате. Это делает игру неотъемлемой частью интерфейса чата и позволяет избежать загромождения канала шахматными досками.

Вот конечный результат того, что вы собираетесь построить:

На что следует обратить внимание:

  1. На нижней панели ввода сообщений есть значок игры, с помощью которого можно начать шахматную партию.
  2. Щелчок по значку добавляет шахматную партию в качестве вложения к сообщению, которое мы отображаем с помощью пользовательского эскиза.
  3. Ранее отправленное пользовательское вложение отображается как полноценная шахматная доска с интерактивностью после отправки сообщения.

Начинающийся

Начнем с создания базовой реализации Stream Chat. Здесь пакет stream_chat_flutter используется для простого добавления функций чата. Наряду с этим для создания шахматных досок используется пакет flutter_chess_board.

К вашему pubspec.yaml должны быть добавлены следующие новые зависимости:

dependencies:
  stream_chat_flutter: ^3.5.1
  flutter_chess_board: ^1.0.1

⚠️ Во время чтения этой статьи могут быть обновленные версии этих пакетов. Если вы хотите следовать точно, будет безопаснее использовать версии, упомянутые выше. Когда вы ознакомитесь со всем кодом, вы можете запустить flutter pub old, чтобы увидеть, какие пакеты требуют обновления.

Откройте main.dart и настройте чат:

  1. Инициализируйте Stream Chat Flutter, подключите пользователя и следите за изменениями канала.
  2. Отображение сообщений канала с помощью виджета MessageListView.
  3. Добавьте виджет MessageInput, чтобы иметь возможность отправлять сообщения на вышеупомянутый канал.

Вот как это выглядит в коде:

Это дает один канал с AppBar, MessageListView под ним, отображающим сообщения в канале, а также MessageInput внизу для отправки/редактирования сообщений:

Отправка вложения шахматной доски

Теперь, когда базовый пользовательский интерфейс чата настроен, можно интегрировать предполагаемую функциональность Chess.

Моделирование шахматной доски

Чтобы создать наше пользовательское вложение шахматной доски, нам потребуется создать пользовательское вложение для отображения шахматной доски.

Нам также необходимо создать специальное вложение сообщения, которое будет включать данные о текущей игре и ориентации доски, чтобы цвета фигур были назначены правильно.

Чтобы загрузить игру в шахматы, мы будем использовать формат нотации Форсайта-Эдвардса (FEN) для хранения сведений о текущей игре в шахматы. FEN хранит всю необходимую информацию о любой позиции на доске, поэтому мы можем инициализировать шахматную доску в заданной позиции.

Например, это стандартное обозначение начальной позиции:

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

Не вдаваясь в подробности (реализация их не требует), вот несколько вещей, которые вы должны знать:

  • FEN делит позиции на обычные восемь рядов шахматной доски.
  • Строчные буквы обозначают черные фигуры, а прописные буквы обозначают белые фигуры.
  • «w» указывает на ход белых.
  • В конце строки ASCII есть счетчик полного и полухода.

Класс Chess в пакетах flutter_chess_board дает нам .fengetter, который конвертирует текущую игру в формат FEN. Вы можете преобразовать это обратно в объект Chess, используя Chess.fromFEN().

Наряду с этим необходимо сохранить идентификатор пользователя, играющего белыми фигурами, чтобы можно было отобразить правильную ориентацию доски. Если вам нужно, вы также можете сохранить игрока, играющего черными фигурами, однако эта реализация держит его открытым, чтобы код не ломался для групповых каналов, которые могут иметь более двух участников.

Вот класс модели для вложения, которое должно быть добавлено к сообщению:

import 'package:flutter_chess_board/flutter_chess_board.dart';
class ChessAttachment {
  final Chess game;
  final String whiteUserId;
ChessAttachment({required this.game, required this.whiteUserId});
factory ChessAttachment.fromJson(Map<String, dynamic> json) {
    return ChessAttachment(
      game: Chess.fromFEN(json['game'] as String),
      whiteUserId: json['white_user_id'] as String,
    );
  }
Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'game': game.fen,
      'white_user_id': whiteUserId,
    };
  }
ChessAttachment copyWith({
    Chess? game,
    String? whiteUserId,
  }) {
    return ChessAttachment(
      game: game ?? this.game,
      whiteUserId: whiteUserId ?? this.whiteUserId,
    );
  }
}

Вы можете добавить к этому любые другие данные, которые могут вам понадобиться — несколько примеров включают:

  • Определение другого игрока или игроков
  • Добавление счета между игроками и т. д.

Настройка виджета MessageInput

Прежде чем мы создадим пользовательский интерфейс, необходимый для игры в шахматы, должен быть способ инициировать начало игры. Это примет форму кнопки в аргументе MessageInput actions. Давайте начнем с добавления этой кнопки действия:

GlobalKey<MessageInputState> _mipKey = GlobalKey();
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: const ChannelHeader(),
    body: Column(
      children: <Widget>[
        const Expanded(
          child: MessageListView(),
        ),
    MessageInput(
          key: _mipKey,
          attachmentLimit: 3,
          actions: [
           IconButton(
             onPressed: () {},
              icon: const Icon(Icons.videogame_asset_outlined),
              padding: const EdgeInsets.all(0),
              constraints: const BoxConstraints.tightFor(
                height: 24,
                width: 24,
              ),
              splashRadius: 24,
            ),
          ],
        ),
      ],
    ),
  );
}

Теперь ваш MessageInputWidget должен выглядеть так:

Добавить вложение в состояние MessageInput

Теперь есть кнопка для добавления вложения в файл MessageInput. Однако логика его добавления еще нуждается в реализации.

Создание нового объекта Chess для хранения нашей шахматной партии — хорошее начало. Вы можете получить текущий идентификатор пользователя, чтобы настроить игрока, играющего белым (вы можете изменить это в своей реализации, чтобы кто-то другой был белым или текущий игрок играл черным). Наконец, вы можете создать объект модели ChessAttachment, который будет использовать два вышеупомянутых объекта.

Последний раздел присоединил GlobalKey к MessageInput. Этот ключ теперь используется для доступа к текущему состоянию MessageInput и добавления вложения.

Вот код для этого шага:

IconButton(
  onPressed: () {
    var newGame = Chess();
    var userId = StreamChat.of(context).currentUser!.id;
    var attachment =
    ChessAttachment(game: newGame, whiteUserId: userId);
_mipKey.currentState?.addAttachment(
      Attachment(
        type: 'chess',
        uploadState: const UploadState.success(),
        extraData: attachment.toJson(),
      ),
    );
  },
  icon: const Icon(Icons.videogame_asset_outlined),
  padding: const EdgeInsets.all(0),
  constraints: const BoxConstraints.tightFor(
    height: 24,
    width: 24,
  ),
  splashRadius: 24,
),

Несмотря на то, что MessageInput теперь имеет вложение, это не предопределенный тип (изображение, видео, GIF и т. д.). Виджету нужно сообщить, как мы хотим, чтобы миниатюра для вложения отображалась.

Добавление пользовательской миниатюры шахматной доски

В предыдущем разделе основное внимание уделялось добавлению кнопки к MessageInput, которая позволяла добавлять вложение типа chess. Когда пользователь добавляет это вложение, должен быть предварительный просмотр этого вложения в файле MessageInput composer. Сама по себе шахматная доска была бы хорошим предварительным просмотром этого, чего и пытается достичь этот раздел.

MessageInput содержит свойство attachmentThumbnailBuilders, которому предоставляется Map, где ключи — это типы вложений, а значения — методы построителя вложений.

Добавляя свойство attachmentThumbnailBuilders и определяя построитель для типа вложения chess, код выглядит так:

MessageInput(
  key: _mipKey,
  attachmentLimit: 3,
  actions: [
    IconButton(
      onPressed: () {
        var newGame = Chess();
        var userId = StreamChat.of(context).currentUser!.id;
        var attachment =
            ChessAttachment(game: newGame, whiteUserId: userId);
_mipKey.currentState?.addAttachment(
          Attachment(
            type: 'chess',
            uploadState: const UploadState.success(),
            extraData: attachment.toJson(),
          ),
        );
      },
      icon: const Icon(Icons.videogame_asset_outlined),
    ),
  ],
  attachmentThumbnailBuilders: {
    'chess': (context, attachment) {
      return SizedBox(
        height: 75,
        width: 75,
        child: ChessBoard(
          controller: ChessBoardController(),
        ),
      );
    },
  },
),

Теперь, когда кнопка нажата, миниатюра отображается над TextField, которая показывает шахматную доску:

Отображение вложения Chessboard в сообщении

После предыдущих разделов сообщение отправляется с шахматным вложением, которое содержит текущую позицию на доске и метаданные игры. Теперь эти вложения необходимо отобразить в сообщении в виде полной шахматной доски.

MessageListView по умолчанию (очевидно) не поддерживает тип chessattachment. Чтобы добавить это, можно использовать логику, аналогичную MessageInput, но с вложениями вместо миниатюр. Однако здесь параметр customAttachmentBuilders используется, чтобы указать MessageListViewкак отображать пользовательское вложение.

Для этого вы можете получить вложение шахмат, проверив message.attachments. Поскольку данные шахматного вложения добавляются в параметр extraData вложения, его можно воссоздать с помощью ChessAttachment.fromJson(attachment.extraData).

Вы также можете создать экземпляр ChessBoardController, используя игру в файле ChessAttachment. Теперь здесь отображается текущая игровая позиция на доске. Кроме того, параметр whiteUserId помогает нам определить, какая сторона доски обращена к какому пользователю. Мы можем изменить ориентацию доски, если ID не совпадает:

return ChessBoard(
   controller: chessBoardController,
   boardOrientation: StreamChat.of(context).currentUser!.id ==   chessAttachment.whiteUserId ? PlayerColor.white : PlayerColor.black,
);

Общий код для этого:

MessageListView(
  messageBuilder: (context, details, list, defaultWidget) {
    return defaultWidget.copyWith(
      customAttachmentBuilders: {
        'chess': (context2, message, list) {
          var attachment = message.attachments
              .firstWhere((e) => e.type == 'chess');
          var chessAttachment =
          ChessAttachment.fromJson(attachment.extraData);
          var chessBoardController =
          ChessBoardController.fromGame(chessAttachment.game);
return ChessBoard(
            controller: chessBoardController,
            boardOrientation:
            StreamChat.of(context).currentUser!.id ==
                chessAttachment.whiteUserId
                ? PlayerColor.white
                : PlayerColor.black,
          );
        },
      },
    );
  },
),

Это теперь отображает шахматную доску как приложение к самому сообщению:

Главное, что следует отметить в этот момент, это то, что шахматная доска не реагирует ни на какие изменения — например, сделанные ходы не обновляют доску для другого игрока, так как ход никогда не отправляется. Давайте займемся этим дальше.

Следим за новыми ходами на доске

Текущий код может добавлять ChessAtachment к сообщению, отображать миниатюру и отображать вложение в виде шахматной доски в MessageListView. Теперь, когда пользователь делает ход, доске необходимо обновить вложение с новой позицией на доске, что также автоматически обновляет другое устройство.

Для этого ChessBoardController позволяет прослушивать обновления позиции на доске. Когда это произойдет, мы можем обновить сообщение новыми данными вложения.

MessageListView(
  messageBuilder: (context, details, list, defaultWidget) {
    return defaultWidget.copyWith(
      customAttachmentBuilders: {
        'chess': (context2, message, list) {
          var attachment = message.attachments
              .firstWhere((e) => e.type == 'chess');
          var chessAttachment =
          ChessAttachment.fromJson(attachment.extraData);
          var chessBoardController =
          ChessBoardController.fromGame(chessAttachment.game);
chessBoardController.addListener(
                () {
              StreamChannel.of(context).channel.updateMessage(
                message.copyWith(
                  attachments: [
                    attachment.copyWith(
                      uploadState: const UploadState.success(),
                      extraData: chessAttachment
                          .copyWith(
                        game: chessBoardController.game,
                      )
                          .toJson(),
                    ),
                  ],
                ),
              );
            },
          );
return ChessBoard(
            controller: chessBoardController,
            boardOrientation:
            StreamChat.of(context).currentUser!.id ==
                chessAttachment.whiteUserId
                ? PlayerColor.white
                : PlayerColor.black,
          );
        },
      },
    );
  },
),

Заключение

При реализации всего вышеперечисленного окончательный результат позволит двум игрокам играть друг с другом, не выходя из чата или не отправляя шахматные приглашения, которые не доходят до другого игрока.

Мы хотим, чтобы вы тратили время на повышение ELO, а не на разработку.

Чтобы найти полный рабочий код для этого проекта, вы можете перейти по ссылке GitHub ниже.

GitHub: https://github.com/deven98/stream_chess_demo

Первоначально опубликовано на https://getstream.io.