Создание клона пасьянса во Flutter (без игрового движка)
После создания тральщика во Flutter мы теперь создадим еще одну классическую игру во Flutter: Soltaire. Опять же, здесь не используется игровой движок, и это чистый код Flutter. Если вы уже знаете, как работает Solitaire, и хорошо разбираетесь в Flutter, перейдите к концу статьи и найдите ссылку на Github.
Что такое пасьянс?
Пасьянс - карточная игра, конечная цель которой состоит в том, чтобы поместить все карты в колоды четырех мастей (верхние правые колоды) в порядке Туз - ›Король. Вначале у нас есть семь столбцов карточек, каждая из которых содержит от одной до семи карточек соответственно. Остальные карты находятся в колоде в верхнем левом углу.
В столбцах карточек должны быть карточки чередующихся цветов (красный, черный) и они должны быть в порядке. Мы можем перетаскивать одну или несколько карточек из одного столбца в другой.
Интересный факт: пасьянс был фактически представлен в Windows, чтобы люди привыкли к новым жестам, таким как перетаскивание мышью.
Начиная
Наша конечная цель будет такой:
Давайте посмотрим, что нам нужно сделать:
- Создайте базовую модель карты
- Создайте 52 карты, случайным образом разделите их по столбцам и положите остальные обратно в колоду.
- Создание виджета карты
- Создайте виджет столбца карточек
- Создайте семь столбцов карточек
- Создайте четыре колоды костюмов
- Создайте оставшуюся колоду карт
В этой статье будет немного больше кода Flutter, чем в статье о сапёрах, поскольку здесь логика проще, но компоненты немного сложнее.
Давайте код
Начнем с изготовления открытки.
Создание базовой модели карты
Сначала давайте создадим базовую модель данных для карты.
Игральная карта имеет масть (бубны / червы / пики / трефы) и тип (1,2,3,4… J, Q, K). Наряду с этим мы добавим две другие переменные для хранения, если эта карта перевернута и открывалась ли эта карта раньше.
Модель имеет три перечисления для хранения типа карты, масти и цвета. Он также имеет простой геттер для получения цвета карты (красный / черный).
Создавайте карточки и случайным образом помещайте их в столбцы карточек.
Сначала мы инициализируем все списки, в которых хранятся наши карты.
// Stores the cards on the seven columns List<PlayingCard> cardColumn1 = []; List<PlayingCard> cardColumn2 = []; List<PlayingCard> cardColumn3 = []; List<PlayingCard> cardColumn4 = []; List<PlayingCard> cardColumn5 = []; List<PlayingCard> cardColumn6 = []; List<PlayingCard> cardColumn7 = []; // Stores the remaining card deck List<PlayingCard> cardDeckClosed = []; List<PlayingCard> cardDeckOpened = []; // Stores the card in the final suit decks List<PlayingCard> finalHeartsDeck = []; List<PlayingCard> finalDiamondsDeck = []; List<PlayingCard> finalSpadesDeck = []; List<PlayingCard> finalClubsDeck = [];
(Вы также можете создать Список ‹Список ‹PlayingCard›› здесь)
Теперь инициализируем все 52 карты:
List<PlayingCard> allCards = []; // Add all cards to deck CardSuit.values.forEach((suit) { CardType.values.forEach((type) { allCards.add(PlayingCard( cardType: type, cardSuit: suit, faceUp: false, )); }); });
Теперь, когда игра начинается, в столбце 1 есть 1 карта, в столбце 2 - 2 карты и так далее. Всего у нас семь столбцов. Итак, мы произвольно выбираем 28 карт и добавляем их в соответствующие списки. Нам также нужно открыть и перевернуть последние карточки в каждом столбце.
Random random = Random(); // Add cards to columns and remaining to deck for (int i = 0; i < 28; i++) { int randomNumber = random.nextInt(allCards.length); if (i == 0) { PlayingCard card = allCards[randomNumber]; cardColumn1.add( card ..opened = true ..faceUp = true, ); allCards.removeAt(randomNumber); } else if (i > 0 && i < 3) { if (i == 2) { PlayingCard card = allCards[randomNumber]; cardColumn2.add( card ..opened = true ..faceUp = true, ); } else { cardColumn2.add(allCards[randomNumber]); } allCards.removeAt(randomNumber); } else if (i > 2 && i < 6) { if (i == 5) { PlayingCard card = allCards[randomNumber]; cardColumn3.add( card ..opened = true ..faceUp = true, ); } else { cardColumn3.add(allCards[randomNumber]); } allCards.removeAt(randomNumber); } else if (i > 5 && i < 10) { if (i == 9) { PlayingCard card = allCards[randomNumber]; cardColumn4.add( card ..opened = true ..faceUp = true, ); } else { cardColumn4.add(allCards[randomNumber]); } allCards.removeAt(randomNumber); } else if (i > 9 && i < 15) { if (i == 14) { PlayingCard card = allCards[randomNumber]; cardColumn5.add( card ..opened = true ..faceUp = true, ); } else { cardColumn5.add(allCards[randomNumber]); } allCards.removeAt(randomNumber); } else if (i > 14 && i < 21) { if (i == 20) { PlayingCard card = allCards[randomNumber]; cardColumn6.add( card ..opened = true ..faceUp = true, ); } else { cardColumn6.add(allCards[randomNumber]); } allCards.removeAt(randomNumber); } else { if (i == 27) { PlayingCard card = allCards[randomNumber]; cardColumn7.add( card ..opened = true ..faceUp = true, ); } else { cardColumn7.add(allCards[randomNumber]); } allCards.removeAt(randomNumber); } }
Затем мы просто добавляем оставшиеся карты в allCards
к списку оставшихся карт. Мы также открываем верхнюю карточку и добавляем ее в openedCards
, которую пользователь может перетащить в столбцы карточек.
cardDeckClosed = allCards; cardDeckOpened.add( cardDeckClosed.removeLast() ..opened = true ..faceUp = true, ); setState(() {});
Создание столбца карточек
Мы на мгновение нарушим порядок и изучим столбец карточек, прежде чем изучать сам виджет карточек.
Столбец карт - это стопка карт, каждая из которых сдвинута в направлении Y больше, чем предыдущая. Если бы не было перевода, у нас были бы все карточки друг на друга.
Столбец карт также должен иметь возможность принимать другие карты, которые перетаскиваются в него. Итак, нам также нужно использовать DragTarget
. Если вы не знакомы с Draggables и DragTargets, ознакомьтесь с моим подробным описанием их здесь.
Мы принимаем следующие параметры:
// List of cards in the stack final List<PlayingCard> cards; // Callback when card is added to the stack final CardAcceptCallback onCardsAdded; // The index of the list in the game final int columnIndex;
onCardAdded
- это обратный вызов для запуска функции, когда карточка перетаскивается из одного списка в другой. columnIndex
отмечает индекс столбца в семи созданных нами столбцах. Это полезно отметить, поскольку нам нужна информация о том, откуда взялась и ушла перетаскиваемая карта.
Из приведенной выше информации можно сделать вывод, что это должен быть Stack
, окруженный DragTarget
:
DragTarget<Map>( builder: (context, listOne, listTwo) { return Stack( children: widget.cards.map((card) { int index = widget.cards.indexOf(card); return TransformedCard( playingCard: card, transformIndex: index, attachedCards: widget.cards.sublist(index, widget.cards.length), columnIndex: widget.columnIndex, ); }).toList(), ); }, ),
Как мы уже говорили, CardColumn
должен иметь возможность принимать перетаскиваемые карты, но только если они соответствуют условиям игры, а именно противоположного цвета и по порядку. Мы кодируем логику в функции onWillAccept
нашего DragTarget
.
onWillAccept: (value) { // If empty, accept if (widget.cards.length == 0) { return true; } // Get dragged cards list List<PlayingCard> draggedCards = value["cards"]; PlayingCard firstCard = draggedCards.first; if (firstCard.cardColor == CardColor.red) { if (widget.cards.last.cardColor == CardColor.red) { return false; } int lastColumnCardIndex = CardType.values.indexOf(widget.cards.last.cardType); int firstDraggedCardIndex = CardType.values.indexOf(firstCard.cardType); if(lastColumnCardIndex != firstDraggedCardIndex + 1) { return false; } } else { if (widget.cards.last.cardColor == CardColor.black) { return false; } int lastColumnCardIndex = CardType.values.indexOf(widget.cards.last.cardType); int firstDraggedCardIndex = CardType.values.indexOf(firstCard.cardType); if(lastColumnCardIndex != firstDraggedCardIndex + 1) { return false; } } return true; },
Когда карта принимается, вызывается обратный вызов. Как мы скоро увидим, при перетаскивании карточек мы передаем данные перетаскиваемых карточек, а также столбец, из которого они были получены.
onAccept: (value) { widget.onCardsAdded( value["cards"], value["fromIndex"], ); },
TransformedCard
- это имя виджета карты, который мы изучим дальше.
Создание виджета карты
Наша карта нужна нам, чтобы:
- Возьмите модель
PlayingCard
и отобразите карту соответственно - Быть перетаскиваемым
- Если перетащить, перенесите все карты под ним как прикрепленные карты.
- Переведите по оси Y в зависимости от положения карты в столбце карты (вы можете реализовать это поведение в столбце карты, но я решил сделать это в самой карте).
В параметрах берем:
// The card model to display final PlayingCard playingCard; // The distance to translate the card in the Y axis (default: 15.0) final double transformDistance; // The index of the card in the card column final int transformIndex; // The index of the column in the seven card columns in the game. final int columnIndex; // Cards below the current card in the card column final List<PlayingCard> attachedCards;
Как мы обсуждали в разделе CardColumn
, нам нужно перевести наши карточки в направлении Y на основе их индекса в списке.
Итак, во-первых, у нас есть виджет Transform
как внешний виджет при создании карты.
return Transform( transform: Matrix4.identity() ..translate( 0.0, widget.transformIndex * widget.transformDistance, 0.0, ), child: _buildCard(), );
Теперь нам нужно собрать саму карту, у которой есть два случая:
- Карточка обращена вниз
Если карта обращена вниз, нам не нужно делать ее перетаскиваемой, и мы можем просто сделать простую обратную сторону карты.
Container( height: 60.0, width: 40.0, decoration: BoxDecoration( color: Colors.blue, border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(8.0), ), )
2. Карточка обращена вверх
Это более сложный случай. Нам нужно:
- Покажите пройденную игральную карту.
- Сделайте его перетаскиваемым (и прикрепите все карты под картой, если они есть).
- Приложите все данные, чтобы другой
DragTarget
мог получить карту.
Нам нужно сделать внешний виджет Draggable
:
Draggable<Map>( child: _buildFaceUpCard(), feedback: CardColumn( cards: widget.attachedCards, ), childWhenDragging: _buildFaceUpCard(), data: { "cards": widget.attachedCards, "fromIndex": widget.columnIndex, }, );
feedback
- это виджет, отображаемый при перетаскивании карточек, это еще один столбец карточек, поскольку если перетаскивать карточку в середине, то все карточки, расположенные ниже, также перетаскиваются вместе с ней. Итак, мы строим меньший CardColumn
из карты + карт под ней. (Да, у CardColumn
есть карты, каждая из которых имеет CardColumn
в качестве обратной связи, каждая из которых имеет карты. Это похоже на CardColumn-ception… без переполнения стека.)
Создание семи столбцов карточек
Мы просто используем Expanded
в строке для добавления столбцов карточек.
Создайте четыре колоды костюмов
Поскольку мы также можем перетаскивать карты в колоды мастей, нам нужно снова использовать DragTarget
, хотя и с немного другой логикой, поскольку карты должны приниматься в обратном порядке по сравнению с CardColumn
.
// The suit of the deck final CardSuit cardSuit; // The cards added to the deck final List<PlayingCard> cardsAdded; // Callback when card is added final CardAcceptCallback onCardAdded;
Нам не нужна колода карт, мы можем просто показать самую верхнюю карту. Если он пустой, строим пустую карту.
Опять же, это DragTarget
с TransformedCard
или пустым заполнителем внутри него.
Однако логика принятия (onWillAccept
) отличается, поскольку здесь мы принимаем 2 после 1, тогда как в CardColumn
мы поступили наоборот.
Создание оставшейся колоды карт
Остальные карты состоят из двух списков, оставшиеся карты открыты, а оставшиеся карты закрыты. Мы просто перекладываем карточки между двумя списками и показываем самый верхний. Мы используем InkWell
вокруг левой карты, чтобы при нажатии на карту мы перемещали верхнюю карту из закрытого списка в открытый. Правую карту также нужно перетаскивать.
Обработка перетаскивания карт
Как только карта или несколько карт перетаскиваются из одной CardColumn
в другую, нам нужно добавить карты в список карт для этого CardColumn
и удалить его из другой.
onCardsAdded: (cards, index) { setState(() { (newColumnName).addAll(cards); int length = oldColumnName.length; oldColumnName .removeRange(length - cards.length, length); _refreshList(index); }); },
Обработка условия победы
После каждого перетаскивания мы просто проверяем, составляет ли добавление карт в окончательных колодах мастей 52. Если да, мы объявляем выигрыш.
if (finalDiamondsDeck.length + finalHeartsDeck.length + finalClubsDeck.length + finalSpadesDeck.length == 52) { _handleWin(); }
И, наконец, наш результат:
Для полного кода:
Вот и все для этой статьи! Надеюсь, вам понравилось, и оставлю пару аплодисментов, если понравилось. Следуйте за мной, чтобы увидеть больше статей о Flutter и комментировать любые отзывы об этой статье.