Девять лет назад я имел неосторожность приобрести приставку 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 должен быть. Архив перезалит.
