Девять лет назад я имел неосторожность приобрести приставку PSP, чему был очень рад. Омрачало радость только отсутствие пасьянса. Не то, чтобы я был любителем пасьянса, но как-то привык я раскладывать один из вариантов — “Косынку”. Пришлось такой пасьянс написать самому. В дальнейшем этот написанный для PSP пасьянс я портировал под Windows и под QNX. В этой вот статье я и расскажу, как написать такую игру.
Перво-наперво, нам понадобится графика. Рисовать я не умею, так что всю графику я взял из интернета. В версии для PSP карты я выводил из фрагментов (цифры и масть), а в остальных версиях при портировании каждая карта получила отдельный спрайт. Дальше надо подумать о реализации алгоритма самого пасьянса.
Зададим ящики, где могут находиться карты вот такой вот структурой:
Всего у нас 13 ящиков. Каждый ящик состоит из 52 отделений. Вот они на рисунке:
Ящики на игровом поле
Флаг видимости карты означает, что карта открыта. Примем, что если значение карты отрицательно, то больше карт в ящике нет.
В каждый ящик можно поместить максимум 52 карты и ещё признак того, что больше карт нет – всего 53 отделения-ячейки.
Нам потребуется функция для перемещения карт между ящиками. Вот она:
Здесь мы ищем индекс отделения из которого можно взять и индекс отделения в которое можно положить. Но правила перемещения с учётом масти и значения карты эта функция не проверяет. Это просто перемещение нижних карт из одного ящика в другой.
Также нам потребуется функция перемещения карт из нулевого ящика в первый. Эти ящики являются магазином, так что их содержимое перемещается по кругу.
Здесь мы перемещаем карту из нулевого ящика в первый, а если такое перемещение не удалось, значит, нулевой ящик пуст и нужно все карты переместить из первого обратно в нулевой.
Теперь нам нужно инициализировать расклад. Сделаем это вот так:
Изначально все карты помещаются в нулевой ящик (магазин), затем этот ящик прокручивается на случайное число, а затем карта просто перемещается в остальные ящики с индексами от 2 до 8. Можно, конечно, раскидывать карты так, чтобы пасьянс гарантированно собирался, но я так не сделал. А можно просто выбирать карту из 52 карт случайным образом и класть в нужный ящик. Так я тоже не стал делать.
Вышеприведённая функция использует ещё одну функцию:
Ну, тут я думаю, всё и так понятно. Разумеется, в целях оптимизации можно было всегда помнить, сколько же к ящике карт, но особого смысла в этом нет – быстродействие тут значения не имеет, ввиду того, что эти функции редко вызываются.
Чтобы не отслеживать, какие карты видимы, а какие нет, я задал вот такую вот функцию:
Она открывает все нижние карты в ящиках со второго по восьмой.
Выше приводилась функция MoveCard, так вот, на самом деле, она в самой игре практически не используется, так как применяется только на этапе инициализации пасьянса и при прокручивании магазина. Всё дело в том, что в пасьянсе нужно переносить группы карт, а не отдельные карты. Для перемещения таких групп есть функция ChangeBox, которая требует указания исходного ящика, ящика назначения и индекса ячейки, начиная с которой нам нужно переносить карты.
А вот полное перемещение карт с учётом всех правил выполняет другая функция, использующая ChangeBox.
На поле сборки (ящики с индексами от 9 до 12) можно класть только одномастные карты в порядке увеличения значения, но первым должен быть всегда туз. На игровом поле цвета масти должны быть противоположны, значения карт должны увеличиваться, а переносить на пустое поле можно только короля.
Пасьянс собран, когда на поле сборки в каждом ящике ровно 13 карт:
Для удобной работы с ящиками есть массив с их координатами:
Заполняется этот массив так:
В этом массиве для каждого ящика формируются все расположения всех 52 карт колоды. С помощью такого массива можно легко определить, что выбрал игрок мышкой:
Собственно, на этом написание логической части пасьянса и заканчивается. Интерфейсную же часть вы можете сделать по своему вкусу. Поскольку я переносил программу с PSP (где она вертится в while(1)), то лично я привязал циклы к таймеру и каждому режиму таймера дал свой номер и обработчик. Также асинхронно привязал отработку OnPaint от таймера. Так проще всего оказалось сделать при портировании.
В архиве программа для Windows, для QNX и оригинал для PSP.
Переделанная программа на GitHub: github.com/da-nie/Patience
MoveCard — цикл не до 52, а до 53 должен быть. Архив перезалит.
Перво-наперво, нам понадобится графика. Рисовать я не умею, так что всю графику я взял из интернета. В версии для PSP карты я выводил из фрагментов (цифры и масть), а в остальных версиях при портировании каждая карта получила отдельный спрайт. Дальше надо подумать о реализации алгоритма самого пасьянса.
Зададим ящики, где могут находиться карты вот такой вот структурой:
//масти
enum CARD_SUIT
{
//пики
CARD_SUIT_SPADES,
//червы
CARD_SUIT_HEARTS,
//трефы
CARD_SUIT_CLUBS,
//буби
CARD_SUIT_DIAMONDS
};
struct SCard
{
CARD_SUIT Suit;//масть
long Value;//значение карты от двойки до туза
bool Visible;//true-карта видима
} sCard_Box[13][53];//тринадцать ящиков по 52 карты в каждой максимум
Всего у нас 13 ящиков. Каждый ящик состоит из 52 отделений. Вот они на рисунке:
Ящики на игровом поле
Флаг видимости карты означает, что карта открыта. Примем, что если значение карты отрицательно, то больше карт в ящике нет.
В каждый ящик можно поместить максимум 52 карты и ещё признак того, что больше карт нет – всего 53 отделения-ячейки.
Нам потребуется функция для перемещения карт между ящиками. Вот она:
//----------------------------------------------------------------------------------------------------
//переместить карту из ящика s в ячейку d
//----------------------------------------------------------------------------------------------------
bool CWnd_Main::MoveCard(long s,long d)
{
long n;
long s_end=0;
long d_end=0;
//ищем первые свободные места в ящиках
for(n=0;n<53;n++)
{
s_end=n;
if (sCard_Box[s][n].Value<0) break;
}
for(n=0;n<53;n++)
{
d_end=n;
if (sCard_Box[d][n].Value<0) break;
}
if (s_end==0) return(false);//начальный ящик пуст
//иначе переносим карты
sCard_Box[d][d_end]=sCard_Box[s][s_end-1];
sCard_Box[s][s_end-1].Value=-1;//карты там больше нет
return(true);
}
Здесь мы ищем индекс отделения из которого можно взять и индекс отделения в которое можно положить. Но правила перемещения с учётом масти и значения карты эта функция не проверяет. Это просто перемещение нижних карт из одного ящика в другой.
Также нам потребуется функция перемещения карт из нулевого ящика в первый. Эти ящики являются магазином, так что их содержимое перемещается по кругу.
//----------------------------------------------------------------------------------------------------
//перемещение карт внутри колоды
//----------------------------------------------------------------------------------------------------
void CWnd_Main::RotatePool(void)
{
bool r=MoveCard(0,1);//перемещаем карты из нулевого ящика в первый
if (r==false)//карт нет
{
//перемещаем обратно
while(MoveCard(1,0)==true);
}
}
Здесь мы перемещаем карту из нулевого ящика в первый, а если такое перемещение не удалось, значит, нулевой ящик пуст и нужно все карты переместить из первого обратно в нулевой.
Теперь нам нужно инициализировать расклад. Сделаем это вот так:
//----------------------------------------------------------------------------------------------------
//инициализировать расклад
//----------------------------------------------------------------------------------------------------
void CWnd_Main::InitGame(void)
{
TimerMode=TIMER_MODE_NONE;
long value=sCursor.Number[0]+10*sCursor.Number[1]+100*sCursor.Number[2]+1000*sCursor.Number[3]+10000*sCursor.Number[4];
srand(value);
long n,m,s;
//выставляем все отделения ящиков в исходное положение
for(s=0;s<13;s++)
for(n=0;n<53;n++) sCard_Box[s][n].Value=-1;
//помещаем в исходный ящик карты
long index=0;
CARD_SUIT suit[4]={CARD_SUIT_SPADES,CARD_SUIT_HEARTS,CARD_SUIT_CLUBS,CARD_SUIT_DIAMONDS};
for(s=0;s<4;s++)
{
for(n=0;n<13;n++,index++)
{
sCard_Box[0][index].Value=n;//ставим карты
sCard_Box[0][index].Suit=suit[s];
sCard_Box[0][index].Visible=true;
}
}
//теперь разбрасываем карты по ящикам
for(n=0;n<7;n++)
{
for(m=0;m<=n;m++)
{
long change=RND(100);
for(s=0;s<=change;s++) RotatePool();//пропускаем карты
//перемещаем карту
if (MoveCard(0,n+2)==false)//если пусто в ящике 0 - делаем заново
{
m--;
continue;
}
long amount=GetCardInBox(n+2);
if (amount>0) sCard_Box[n+2][amount-1].Visible=false;//карты невидимы
}
}
//приводим магазин в исходное состояние
while(1)
{
if (GetCardInBox(1)==0) break;//если пусто в ящике 1
RotatePool();//пропускаем карты
}
}
Изначально все карты помещаются в нулевой ящик (магазин), затем этот ящик прокручивается на случайное число, а затем карта просто перемещается в остальные ящики с индексами от 2 до 8. Можно, конечно, раскидывать карты так, чтобы пасьянс гарантированно собирался, но я так не сделал. А можно просто выбирать карту из 52 карт случайным образом и класть в нужный ящик. Так я тоже не стал делать.
Вышеприведённая функция использует ещё одну функцию:
//----------------------------------------------------------------------------------------------------
//получить количество карт в ящике
//----------------------------------------------------------------------------------------------------
long CWnd_Main::GetCardInBox(long box)
{
long n;
long amount=0;
for(n=0;n<53;n++)
{
if (sCard_Box[box][n].Value<0) break;
amount++;
}
return(amount);
}
Ну, тут я думаю, всё и так понятно. Разумеется, в целях оптимизации можно было всегда помнить, сколько же к ящике карт, но особого смысла в этом нет – быстродействие тут значения не имеет, ввиду того, что эти функции редко вызываются.
Чтобы не отслеживать, какие карты видимы, а какие нет, я задал вот такую вот функцию:
//----------------------------------------------------------------------------------------------------
//сделать нижние карты всех рядов видимыми
//----------------------------------------------------------------------------------------------------
void CWnd_Main::OnVisibleCard(void)
{
long n;
for(n=2;n<9;n++)
{
long amount=GetCardInBox(n);
if (amount>0) sCard_Box[n][amount-1].Visible=true;
}
}
Она открывает все нижние карты в ящиках со второго по восьмой.
Выше приводилась функция MoveCard, так вот, на самом деле, она в самой игре практически не используется, так как применяется только на этапе инициализации пасьянса и при прокручивании магазина. Всё дело в том, что в пасьянсе нужно переносить группы карт, а не отдельные карты. Для перемещения таких групп есть функция ChangeBox, которая требует указания исходного ящика, ящика назначения и индекса ячейки, начиная с которой нам нужно переносить карты.
//----------------------------------------------------------------------------------------------------
//переместить карты из одного ящика в другой
//----------------------------------------------------------------------------------------------------
void CWnd_Main::ChangeBox(long s_box,long s_index,long d_box)
{
long n;
long d_end=0;
//ищем первое свободное место в ящике назначения
for(n=0;n<52;n++)
{
d_end=n;
if (sCard_Box[d_box][n].Value<0) break;
}
//перемещаем туда карты из начального ящика
for(n=s_index;n<52;n++,d_end++)
{
if (sCard_Box[s_box][n].Value<0) break;
sCard_Box[d_box][d_end]=sCard_Box[s_box][n];
sCard_Box[s_box][n].Value=-1;//карты там больше нет
}
}
А вот полное перемещение карт с учётом всех правил выполняет другая функция, использующая ChangeBox.
//----------------------------------------------------------------------------------------------------
//переместить карты с учётом правил
//----------------------------------------------------------------------------------------------------
void CWnd_Main::ChangeCard(long s_box,long s_index,long d_box,long d_index)
{
if (d_box>=2 && d_box<9)//если ящик на игровом поле
{
//если он пуст, то класть туда можно только короля
if (d_index<0)
{
if (sCard_Box[s_box][s_index].Value==12) ChangeBox(s_box,s_index,d_box);//наша карта - король, перемещаем её
return;
}
//иначе, класть можно в порядке убывания и разных цветовых мастей
if (sCard_Box[d_box][d_index].Value<=sCard_Box[s_box][s_index].Value) return;//значение карты больше, чем та, что есть в ячейке ящика
if (sCard_Box[d_box][d_index].Value>sCard_Box[s_box][s_index].Value+1) return;//можно класть только карты, отличающиеся по значению на 1
CARD_SUIT md=sCard_Box[d_box][d_index].Suit;
CARD_SUIT ms=sCard_Box[s_box][s_index].Suit;
if ((md==CARD_SUIT_SPADES || md==CARD_SUIT_CLUBS) && (ms==CARD_SUIT_SPADES || ms==CARD_SUIT_CLUBS)) return;//цвета масти совпадают
if ((md==CARD_SUIT_HEARTS || md==CARD_SUIT_DIAMONDS) && (ms==CARD_SUIT_HEARTS || ms==CARD_SUIT_DIAMONDS)) return;//цвета масти совпадают
ChangeBox(s_box,s_index,d_box);//копируем карты
return;
}
if (d_box>=9 && d_box<13)//если ящик на поле сборки
{
//если выбрано несколько карт, то так перемещать карты нельзя - только по одной
if (GetCardInBox(s_box)>s_index+1) return;
//если ящик пуст, то класть туда можно только туза
if (d_index<0)
{
if (sCard_Box[s_box][s_index].Value==0)//наша карта - туз, перемещаем её
{
DrawMoveCard(s_box,s_index,d_box);
}
return;
}
//иначе, класть можно в порядке возрастания и одинаковых цветовых мастей
if (sCard_Box[d_box][d_index].Value>sCard_Box[s_box][s_index].Value) return;//значение карты меньше, чем та, что есть в ячейке ящика
if (sCard_Box[d_box][d_index].Value+1<sCard_Box[s_box][s_index].Value) return;//можно класть только карты, отличающиеся по значению на 1
CARD_SUIT md=sCard_Box[d_box][d_index].Suit;
CARD_SUIT ms=sCard_Box[s_box][s_index].Suit;
if (ms!=md) return;//масти не совпадают
DrawMoveCard(s_box,s_index,d_box);
return;
}
}
На поле сборки (ящики с индексами от 9 до 12) можно класть только одномастные карты в порядке увеличения значения, но первым должен быть всегда туз. На игровом поле цвета масти должны быть противоположны, значения карт должны увеличиваться, а переносить на пустое поле можно только короля.
Пасьянс собран, когда на поле сборки в каждом ящике ровно 13 карт:
//----------------------------------------------------------------------------------------------------
//проверить на собранность пасьянс
//----------------------------------------------------------------------------------------------------
bool CWnd_Main::CheckFinish(void)
{
long n;
for(n=9;n<13;n++)
{
if (GetCardInBox(n)!=13) return(false);
}
return(true);
}
Для удобной работы с ящиками есть массив с их координатами:
//координаты расположения ячеек карт
long BoxXPos[13][53];
long BoxYPos[13][53];
Заполняется этот массив так:
//размер поля по X
#define BOX_WIDTH 30
//положение ящиков 0 и 2 по X и Y
#define BOX_0_1_OFFSET_X 5
#define BOX_0_1_OFFSET_Y 5
//положение ящиков с 2 по 8 по X и Y
#define BOX_2_8_OFFSET_X 5
#define BOX_2_8_OFFSET_Y 45
//положение ящиков с 9 по 12 по X и Y
#define BOX_9_12_OFFSET_X 95
#define BOX_9_12_OFFSET_Y 5
//смещение каждой следующей карты вниз
#define CARD_DX_OFFSET 10
//масштабный коэффициент относительно размеров карт на PSP
#define SIZE_SCALE 2
for(n=0;n<13;n++)
{
long xl=0;
long yl=0;
long dx=0;
long dy=0;
if (n<2)
{
xl=BOX_0_1_OFFSET_X+BOX_WIDTH*n;
yl=BOX_0_1_OFFSET_Y;
xl*=SIZE_SCALE;
yl*=SIZE_SCALE;
dx=0;
dy=0;
}
if (n>=2 && n<9)
{
xl=BOX_2_8_OFFSET_X+BOX_WIDTH*(n-2);
yl=BOX_2_8_OFFSET_Y;
xl*=SIZE_SCALE;
yl*=SIZE_SCALE;
dx=0;
dy=CARD_DX_OFFSET*SIZE_SCALE;
}
if (n>=9 && n<13)
{
xl=BOX_9_12_OFFSET_X+(n-9)*BOX_WIDTH;
yl=BOX_9_12_OFFSET_Y;
xl*=SIZE_SCALE;
yl*=SIZE_SCALE;
dx=0;
dy=0;
}
for(m=0;m<53;m++)
{
BoxXPos[n][m]=xl+dx*m;
BoxYPos[n][m]=yl+dy*m;
}
}
В этом массиве для каждого ящика формируются все расположения всех 52 карт колоды. С помощью такого массива можно легко определить, что выбрал игрок мышкой:
//размер карты по X
#define CARD_WIDTH 27
//размер карты по Y
#define CARD_HEIGHT 37
//----------------------------------------------------------------------------------------------------
//определение что за ящик и номер ячейки в данной позиции экрана
//----------------------------------------------------------------------------------------------------
bool CWnd_Main::GetSelectBoxParam(long x,long y,long *box,long *index)
{
*box=-1;
*index=-1;
long n,m;
//проходим по ячейкам "магазина"
for(n=0;n<13;n++)
{
long amount;
amount=GetCardInBox(n);
for(m=0;m<=amount;m++)//ради m<=amount сделана 53-я ячейка (чтобы щёлкать на пустых ячейках)
{
long xl=BoxXPos[n][m];
long yl=BoxYPos[n][m];
long xr=xl+CARD_WIDTH*SIZE_SCALE;
long yr=yl+CARD_HEIGHT*SIZE_SCALE;
if (x>=xl && x<=xr && y>=yl && y<=yr)
{
*box=n;
if (m<amount) *index=m;
}
}
}
if (*box<0) return(false);
return(true);
}
Собственно, на этом написание логической части пасьянса и заканчивается. Интерфейсную же часть вы можете сделать по своему вкусу. Поскольку я переносил программу с PSP (где она вертится в while(1)), то лично я привязал циклы к таймеру и каждому режиму таймера дал свой номер и обработчик. Также асинхронно привязал отработку OnPaint от таймера. Так проще всего оказалось сделать при портировании.
В архиве программа для Windows, для QNX и оригинал для PSP.
Переделанная программа на GitHub: github.com/da-nie/Patience
MoveCard — цикл не до 52, а до 53 должен быть. Архив перезалит.