Главная » Разработка для Windows Phone 7 » PhreeCell и колода карт

0

Изначально я не думал реализовывать в своем пасьянсе PhreeCell какие-либо дополнительные возможности, кроме необходимых для игры. Моя жена – она играет во FreeCell для Windows, и у нее пасьянс сходится практически всегда – абсолютно безапелляционно заявила, что PhreeCell необходимы еще две функции. Первое и самое важное – приложение должно каким-то образом поздравлять пользователя с его победой. Я реализовал это в виде производного от DrawableGameComponent компонента CongratulationsComponent (Компонент поздравления).

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

Разработку PhreeCell я начал не с приложения на XNA, а с приложения на Windows Presentation Foundation, формирующего одно растровое изображение размерами 1040 х 448, которое включает 52 игральные карты, каждая из которых 96 пикселов шириной и 112 пикселов высотой. Canvas заполняется числами, буквами и символами мастей преимущественно с помощью объектов TextBlock. После этого приложение передает Canvas в RenderTargetBitmap (Сформировать визуальное представление результирующего растрового изображения) и сохраняет результат в файл под именем cards.png. В XNA- проекте PhreeCell этот файл добавлен в содержимое приложения.

В проекте PhreeCell каждая карта – это объект типа CardInfo (Данные карты): Проект XNA: PhreeCell Файл: CardInfo.cs

using System;

using Microsoft.Xna.Framework;

namespace PhreeCell {

class CardInfo {

static string[] ranks = { "Ace", "Deuce", "Three", "Four",

"Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King" }; static string[] suits = { "Spades", "Clubs", "Hearts", "Diamonds" };

public int Suit { protected set; get; } public int Rank { protected set; get; }

public Vector2 AutoMoveOffset { set; get; } public TimeSpan AutoMoveTime { set; get; } public float AutoMoveInterpolation { set; get; }

public CardInfo(int suit, int rank) {

Suit = suit; Rank = rank;

}

// Используется для целей отладки

public override string ToString() {

return ranks[Rank] + " of " + suits[Suit];

}

}

}

Сначала этот класс имел просто свойства Suit (Масть) и Rank (Старшинство). Я добавил статические массивы string и метод ToString для целей отображения в ходе отладки и ввел еще три поля AutoMove (Автоматическое перемещение), когда реализовал возможность автоматического перемещения. Сам CardInfo не располагает сведениями о том, где фактически располагается карта во время игры. Эти данные сохраняются где-то в другом месте.

Игровое поле

На рисунке представлен исходный экран PhreeCell:

Я исхожу из того, что правила игры всем знакомы. Все 52 карты раскладываются лицом вверх в 8 столбцов, которые в приложении я называю «piles» (стопки). В верхнем левом углу предусмотрено четыре пустых поля для размещения отдельных карт. Я называю эти поля «holds» (свободные ячейки). В верхнем правом углу четыре поля для размещения карт одной

масти по старшинству, начиная с самого низкого ранга. Эти поля я называю «finals» (дом). Красная точка посередине – кнопка повторить игру.

Для удобства я разделил класс Game1 на два файла. Первый – это обычный файл Game1.cs; второй – файл под именем Game1.Helpers.cs. Файл Game1.cs включает только методы, обычно используемые в небольших играх, реализующих также логику захоронения. В файле Game1.Helpers.cs располагается все остальное. Я создал этот файл, добавив новый класс в проект. В обоих файлах класс Game1 наследуется от Game, и в обоих файлах ключевое слово partial указывает на то, что этот класс разделен на несколько файлов. В файлах Helpers нет полей экземпляров, только const и static readonly. Файл Game1.cs включает одно поле static и все поля экземпляров:

Проект XNA: PhreeCell Файл: Game1.cs (фрагмент, демонстрирующий поля)

public partial class Game1 : Microsoft.Xna.Framework.Game {

static readonly TimeSpan AutoMoveDuration = TimeSpan.FromSeconds(0.25);

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

CongratulationsComponent congratsComponent;

Texture2D cards; Texture2D surface;

Rectangle[] cardSpots = new Rectangle[16];

Matrix displayMatrix; Matrix inverseMatrix;

CardInfo[] deck = new CardInfo[52]; List<CardInfo>[] piles = new List<CardInfo>[8]; CardInfo[] holds = new CardInfo[4]; List<CardInfo>[] finals = new List<CardInfo>[4];

bool firstDragInGesture = true; CardInfo touchedCard; Vector2 touchedCardPosition; object touchedCardOrigin; int touchedCardOriginIndex;

}

Данное приложение использует только два объекта Texture2D. Объект cards (карты) – растровое изображение, включающее все 52 карты; каждая отдельная карта отображается через определение прямоугольных фрагментов этого растрового изображения. Объект surface (поверхность) – это темно-синяя область (ее можно видеть на представленном снимке экрана), которая включает также белые прямоугольники и красную кнопку. Координаты для позиционирования этих 16 белых прямоугольников – еще восемь располагаются под каждой стопкой карт – хранятся в массиве cardSpots (Местоположения карт)

Поле displayMatrix (Матрица отображения) – это обычно единичная матрица. Каждому игроку в «свободную ячейку» известно, что иногда стопки карт могут становиться очень длинными. В этом случае displayMatrix выполняет масштабирование по вертикали и сжимает все игровое поле. inverseMatrix (Обратная матрица) – это обратная матрица, которая необходима для преобразования координат сенсорного ввода относительно экрана в точки сжатого растрового изображения.

Следующий блок полей – это основные структуры данных, используемые приложением. Массив deck (колода) включает все 52 объекта, которые были созданы приложением ранее и

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

Остальные поля связаны с выбором и перемещением карт. Поле touchedCardPosition (Местоположение перемещаемой карты) – это текущее местоположение перемещаемой карты. Поле touchedCardOrigin (Источник перемещаемой карты) сохраняет объект, из которого поступила перемещаемая карта. Это либо массив holds, либо piles, тогда как touchedCardOriginIndex (Индекс выбранной карты в источнике) – это индекс данного массива. Эти данные используются для возвращения карты в исходное положение, если пользователь пытается выполнить недопустимое перемещение.

Конструктор Game1 показывает, что игре требуется игровое поле 800 пикселов шириной и 480 пикселов высотой без строки состояния. Также активируются три типа жестов:

Проект XNA: PhreeCell Файл: Game1.cs (фрагмент)

public Game1() {

graphics = new GraphicsDeviceManager(this); graphics.IsFullScreen = true; Content.RootDirectory = "Content";

// Частота кадров по умолчанию для устройств Windows Phone – 30 кадров/с TargetElapsedTime = TimeSpan.FromTicks(333333);

graphics.IsFullScreen = true; graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 480;

// Активируем жесты

TouchPanel.EnabledGestures = GestureType.Tap |

GestureType.FreeDrag | GestureType.DragComplete;

}

Метод Initialize создает объекты CardInfo для массива deck и инициализирует массивы piles и finals объектами List. Также здесь создается и добавляется в коллекцию Components компонент CongratulationsComponent:

Проект XNA: PhreeCell Файл: Game1.cs (фрагмент)

protected override void Initialize() {

// Инициализируем deck

for (int suit = 0; suit < 4; suit++)

for (int rank = 0; rank < 13; rank++) {

CardInfo cardInfo = new CardInfo(suit, rank); deck[suit * 13 + rank] = cardInfo;

}

// Создаем объекты List для 8 стопок for (int pile = 0; pile < 8; pile++)

piles[pile] = new List<CardInfo>();

// Создаем объекты List для четырех 4 окончательных стопок for (int final = 0; final < 4; final++)

finals[final] = new List<CardInfo>();

// Создаем компонент поздравления

congratsComponent = new CongratulationsComponent(this); congratsComponent.Enabled = false; this.Components.Add(congratsComponent); base.Initialize();

}

Метод LoadContent загружает растровое изображение с изображениями карт и также вызывает два метода части класса Game1, реализованной в файле Game1.Helpers.cs:

Проект XNA: PhreeCell Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice);

// Загружаем большое растровое изображение, включающее изображения карт cards = this.Content.Load<Texture2D>("cards");

// Создаем 16 прямоугольных областей для карт и растровую поверхность CreateCardSpots(cardSpots);

surface = CreateSurface(this.GraphicsDevice, cardSpots);

}

В приложении для коммерческого использования я бы, безусловно, добавил второй набор карт для небольшого экрана. Такое решение обеспечивает намного лучшее визуальное представление, чем отображение карт в масштабе 60% от их оригинального размера.

Файл Game1.Helpers.cs начинается с описания ряда констант, которые определяют все размеры игрового поля в пикселах:

Проект XNA: PhreeCell Файл: Game1.Helper.cs (фрагмент, демонстрирующий поля)

public partial class Game1   : Microsoft.Xna.Framework.Game {

const int wCard = 80;                 // ширина карты

const int hCard = 112;                // высота карты

// Горизонтальные размеры

const int wSurface = 800;         // ширина поверхности

const int xGap = 16;              // расстояние между стопками

const int xMargin = 8;            // поле слева и справа

// расстояние между "holds" и "finals"

const int xMidGap = wSurface – (2 * xMargin + 8 * wCard + 6 * xGap); // дополнительное поля для второго ряда

const int xIndent = (wSurface – (2 * xMargin + 8 * wCard + 7 * xGap)) / 2; // Вертикальные размеры

const int yMargin = 8;             // вертикальное поле над верхним рядом

const int yGap = 16;               // вертикальное поле между рядами

const int yOverlay = 28; // ширина видимой части вверху каждой карты в стопке const int hSurface = 2 * yMargin + yGap + 2 * hCard + 19 * yOverlay;

// Кнопка повтора игры

const int radiusReplay = xMidGap / 2 – 8; static readonly Vector2 centerReplay =

new Vector2(wSurface / 2, xMargin + hCard / 2);

Обратите внимание, что wSurface – ширина игрового поля – задано равным 800 пикселам, что соответствует ширине большого экрана телефона. Но может возникнуть необходимость сделать размер по вертикали больше 480. В области piles может располагаться до 20 перекрывающихся карт. Для обеспечения такой возможности hSurface вычисляется как максимально возможная высота, исходя из того, что в стопке может находиться 20 перекрывающихся карт.

Метод CreateCardSpots (Создать места размещения карт) использует эти константы для расчёта местоположения 16 объектов Rectangle, обозначающих места размещения карт на игровом поле. В верхнем ряду располагаются holds и finals, нижний ряд предназначен для piles:

Проект XNA: PhreeCell Файл: Game1.Helper.cs (фрагмент)

static void CreateCardSpots(Rectangle[] cardSpots) {

// Верхний ряд int x = xMargin; int y = yMargin;

for (int i = 0; i < 8; i++) {

cardSpots[i] = new Rectangle(x, y, wCard, hCard); x += wCard + (i == 3 ? xMidGap : xGap);

}

// Нижний ряд x = xMargin + xIndent; y += hCard + yGap;

for (int i = 8; i < 16; i++) {

cardSpots[i] = new Rectangle(x, y, wCard, hCard); x += wCard + xGap;

}

}

Метод CreateSurface (Создать поверхность) создает растровое изображение, используемое в качестве игрового поля. Размер этого растрового изображения вычисляется на основании значений hSurface (заданного как константа и равного 800) и wSurface, значение которого намного превышает 480. Для отрисовки белых прямоугольников и красной кнопки повтора игры этот метод работает напрямую со значениями пикселов растрового изображения:

Проект XNA: PhreeCell Файл: Game1.Helper.cs (фрагмент)

static Texture2D CreateSurface(GraphicsDevice graphicsDevice, Rectangle[] cardSpots) {

uint backgroundColor = new Color(0, 0, 0x60).PackedValue; uint outlineColor = Color.White.PackedValue; uint replayColor = Color.Red.PackedValue;

Texture2D surface = new Texture2D(graphicsDevice, wSurface, hSurface); uint[] pixels = new uint[wSurface * hSurface];

for (int i = 0; i < pixels.Length; i++) {

if ((new Vector2(i % wSurface, i / wSurface) – centerReplay).LengthSquared()

radiusReplay * radiusReplay) pixels[i] = replayColor;

else

pixels[i] = backgroundColor;

foreach (Rectangle rect in cardSpots) {

// верхушки прямоугольников

for (int x = 0; x < wCard; x++) {

pixels[(rect.Top – 1) * wSurface + rect.Left + x] = outlineColor; pixels[rect.Bottom * wSurface + rect.Left + x] = outlineColor;

}

// стороны прямоугольников

for (int y = 0; y < hCard; y++) {

pixels[(rect.Top + y) * wSurface + rect.Left – 1] = outlineColor; pixels[(rect.Top + y) * wSurface + rect.Right] = outlineColor;

}

}

surface.SetData<uint>(pixels); return surface;

}

Другой статический метод класса Game1 не требует особых пояснений. Проект XNA: PhreeCell Файл: Game1.Helper.cs (фрагмент)

static void ShuffleDeck(CardInfo[] deck) {

Random rand = new Random();

for (int card = 0; card < 52; card++) {

int random = rand.Next(52); CardInfo swap = deck[card]; deck[card] = deck[random]; deck[random] = swap;

}

}

static bool IsWithinRectangle(Vector2 point, Rectangle rect) {

return point.X >= rect.Left && point.X <= rect.Right && point.Y >= rect.Top && point.Y <= rect.Bottom;

}

static Rectangle GetCardTextureSource(CardInfo cardInfo) {

return new Rectangle(wCard * cardInfo.Rank,

hCard * cardInfo.Suit, wCard, hCard);

}

static CardInfo TopCard(List<CardInfo> cardInfos) {

if (cardInfos.Count > 0)

return cardInfos[cardInfos.Count – 1];

return null;

}

GetCardTextureSource (Получить источник текстуры карты) используется в сочетании с большим растровым изображением cards. Этот метод просто возвращает объект Rectangle, соответствующий конкретной карте. TopCard (Верхняя карта) возвращает последний элемент коллекции List<CardInfo>, что необходимо для получения самой верхней карты коллекции piles или finals.

В конце перегруженного LoadContent приложение практически готово к вызову метода Replay (Повторить игру), который перетасовывает колоду и «сдает» карты в коллекции piles. Но еще необходимо реализовать захоронение. Изначально, до реализации захоронения, это приложение было построено вокруг массивов и коллекций piles, holds и finals. Мне было приятно осознать, что эти три элемента были единственной частью приложения, которую требовалось сохранять и извлекать при захоронении. Но мне не давало покоя то, что эти три объекта включали ссылки на 52 экземпляра CardInfo, хранящихся в deck, и я хотел сохранить это отношение. Поэтому я пришел к тому, чтобы сохранять и извлекать не экземпляры CardInfo, а целочисленные индексы от 0 до 52. Для этого потребовалось небольшое количество довольно скучного кода:

Проект XNA: PhreeCell Файл: Game1.cs (фрагмент)

protected override void OnDeactivated(object sender, EventArgs args) {

PhoneApplicationService appService = PhoneApplicationService.Current;

// Сохраняем целочисленные индексы для piles List<int>[] piles = new List<int>[8];

for (int i = 0; i < piles.Length; i++) {

piles[i] = new List<int>();

foreach (CardInfo cardInfo in this.piles[i])

piles[i].Add(13 * cardInfo.Suit + cardInfo.Rank);

}

appService.State["piles"] = piles;

// Сохраняем целочисленные индексы для finals List<int>[] finals = new List<int>[4];

for (int i = 0; i < finals.Length; i++) {

finals[i] = new List<int>();

foreach (CardInfo cardInfo in this.finals[i])

finals[i].Add(13 * cardInfo.Suit + cardInfo.Rank);

}

appService.State["finals"] = finals;

// Сохраняем целочисленные индексы для holds int[] holds = new int[4];

for (int i = 0; i < holds.Length; i++) {

if (this.holds[i] == null) holds[i] = -1;

else

holds[i] = 13 * this.holds[i].Suit + this.holds[i].Rank;

}

appService.State["holds"] = holds; base.OnDeactivated(sender, args);

}

protected override void OnActivated(object sender, EventArgs args) {

PhoneApplicationService appService = PhoneApplicationService.Current; if (appService.State.ContainsKey("piles"))

// Извлекаем целочисленные индексы для piles

List<int>[] piles = appService.State["piles"] as List<int>[];

for (int i = 0; i < piles.Length; i++) {

foreach (int cardindex in piles[i])

this.piles[i].Add(deck[cardindex]);

}

// Извлекаем целочисленные индексы для finals

List<int>[] finals = appService.State["finals"] as List<int>[];

for (int i = 0; i < finals.Length; i++) {

foreach (int cardindex in finals[i])

this.finals[i].Add(deck[cardindex]);

}

// Извлекаем целочисленные индексы для holds int[] holds = appService.State["holds"] as int[];

for (int i = 0; i < holds.Length; i++) {

if (holds[i] != -1)

this.holds[i] = deck[holds[i]];

}

CalculateDisplayMatrix();

}

else {

Replay();

}

base.OnActivated(sender, args);

}

Замечательная новость в том, что в самом конце перегруженного OnActivated вызывается метод Replay, который фактически и запускает игру.

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии