Создание клона пасьянса во Flutter (без игрового движка)

После создания тральщика во Flutter мы теперь создадим еще одну классическую игру во Flutter: Soltaire. Опять же, здесь не используется игровой движок, и это чистый код Flutter. Если вы уже знаете, как работает Solitaire, и хорошо разбираетесь в Flutter, перейдите к концу статьи и найдите ссылку на Github.

Что такое пасьянс?

Пасьянс - карточная игра, конечная цель которой состоит в том, чтобы поместить все карты в колоды четырех мастей (верхние правые колоды) в порядке Туз - ›Король. Вначале у нас есть семь столбцов карточек, каждая из которых содержит от одной до семи карточек соответственно. Остальные карты находятся в колоде в верхнем левом углу.

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

Интересный факт: пасьянс был фактически представлен в Windows, чтобы люди привыкли к новым жестам, таким как перетаскивание мышью.

Начиная

Наша конечная цель будет такой:

Давайте посмотрим, что нам нужно сделать:

  1. Создайте базовую модель карты
  2. Создайте 52 карты, случайным образом разделите их по столбцам и положите остальные обратно в колоду.
  3. Создание виджета карты
  4. Создайте виджет столбца карточек
  5. Создайте семь столбцов карточек
  6. Создайте четыре колоды костюмов
  7. Создайте оставшуюся колоду карт

В этой статье будет немного больше кода 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 - это имя виджета карты, который мы изучим дальше.

Создание виджета карты

Наша карта нужна нам, чтобы:

  1. Возьмите модель PlayingCard и отобразите карту соответственно
  2. Быть перетаскиваемым
  3. Если перетащить, перенесите все карты под ним как прикрепленные карты.
  4. Переведите по оси 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(),
);

Теперь нам нужно собрать саму карту, у которой есть два случая:

  1. Карточка обращена вниз

Если карта обращена вниз, нам не нужно делать ее перетаскиваемой, и мы можем просто сделать простую обратную сторону карты.

Container(
    height: 60.0,
    width: 40.0,
    decoration: BoxDecoration(
      color: Colors.blue,
      border: Border.all(color: Colors.black),
      borderRadius: BorderRadius.circular(8.0),
    ),
  )

2. Карточка обращена вверх

Это более сложный случай. Нам нужно:

  1. Покажите пройденную игральную карту.
  2. Сделайте его перетаскиваемым (и прикрепите все карты под картой, если они есть).
  3. Приложите все данные, чтобы другой 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 и комментировать любые отзывы об этой статье.

Некоторые из моих других статей:





Не стесняйтесь проверить и другие мои профили: