Пишем пасьянс «Косынка»

в 7:25, , рубрики: Косынка, пасьянс, Программирование, разработка игр

Девять лет назад я имел неосторожность приобрести приставку PSP, чему был очень рад. Омрачало радость только отсутствие пасьянса. Не то, чтобы я был любителем пасьянса, но как-то привык я раскладывать один из вариантов — “Косынку”. Пришлось такой пасьянс написать самому. В дальнейшем этот написанный для PSP пасьянс я портировал под Windows и под QNX. В этой вот статье я и расскажу, как написать такую игру.

Перво-наперво, нам понадобится графика. Рисовать я не умею, так что всю графику я взял из интернета. В версии для 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 отделений. Вот они на рисунке:

Пишем пасьянс «Косынка» - 1
Ящики на игровом поле

Флаг видимости карты означает, что карта открыта. Примем, что если значение карты отрицательно, то больше карт в ящике нет.
В каждый ящик можно поместить максимум 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<52;n++)
 {
  s_end=n;
  if (sCard_Box[s][n].Value<0) break;
 }
 for(n=0;n<52;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.

Автор: da-nie

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js