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

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


    Ящики на игровом поле

    Флаг видимости карты означает, что карта открыта. Примем, что если значение карты отрицательно, то больше карт в ящике нет.

    В каждый ящик можно поместить максимум 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 должен быть. Архив перезалит.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 37

      +1
      UPD. Прошу прощения, пока редактировал текст под макросы, сделал ошибку в функции MoveCard — цикл не до 52, а до 53 должен быть. А я чего-то решил, что правильно всё-таки до 52. Нет. :) Архив перезалит.

      Скачавшие старую версию не смогут её выиграть — там из-за этой ошибки теряется карта,
        +2
        Github вам в помощь
          –2
          Знаю. Но пока не научился им пользоваться. И вряд ли скоро научусь…
            +3
            Им не надо «учиться» пользоваться. Надо просто начать и выучить две команды — git commit и git push. Ну хорошо, один раз будет нужно сделать git init

            И еще. Я ваши статьи узнаю по внешнему виду исходников. Да. По этим ужасным простыням кода низкого качества, в котором отсутствует функциональное деление, использование констант и логичное именование переменных. Ваш код (если на него смотреть как на самостоятельную единицу ценности) годится только на выброс. Ну или в качестве наказания для других разработчиков.

            1) у вас имеются длииииннные методы (эмпирическое правило — размер метода должен быть не более одного «экрана» текста — той области, которую можно охватить зрением, не переводя взгляд). Разбивка простыней на части помогает их структурировать.

            2) многие данные по смыслу являются векторами. Вот и работайте с ними, как с векторами. Этим вы исключите копипаст и эффект последней строки при копипасте. Не волнуйтесь, компиляторы уже умеют раскручивать циклы. Посмотрите хотя бы на нашу с haqreu библиотеку для векторов и матриц.

            3) если вместе с выполнением (2) начать именовать переменные содержательно, программу можно будет вообще читать как сказку, а не продираться через x1; x2; dx; и прочее.

            4)константные данные следует помещать в const, а не толкать в дефайны.
              –1
              Им не надо «учиться» пользоваться. Надо просто начать и выучить две команды — git commit и git push. Ну хорошо, один раз будет нужно сделать git init


              Я чтобы попробовать поставил git отдельно на компьютер и скачал книжку. Так вот, не получилось. В книжке написано — сделайте git add *.cc. Хорошая идея. Только вот как добавить несколько проектов? Как добавить папку? Не написано в той главе, где упоминается git add. А git clone вызывало ошибку kerlen322.dll — у меня Windows XP, а оно, которое скачанное, желает как минимум висту.

              По этим ужасным простыням кода низкого качества, в котором отсутствует функциональное деление, использование констант и логичное именование переменных.


              По-моему, всё это есть как в этом коде, так и в коде остальных статей. Констант, конечно, нет. Я всё-таки предпочитаю define. Но уж именование переменных и функциональное деление там точно есть.

              Разбивка простыней на части помогает их структурировать.


              Не соглашусь. От множества мелких функций будет в глазах рябить.

              многие данные по смыслу являются векторами. Вот и работайте с ними, как с векторами.


              Для шахмат такого лучше не делать. В движке это сделано и так. А эта программа порт с PSP и там я точно их не использовал, потому как, помнится, компилятор там был использован C, а не C++.

              а не продираться через x1; x2; dx; и прочее.


              Но в данном случае как раз x1,x2 и dx очень даже содержательны. x1;y1-начальные координаты, dx-размер по x.
              Исключение составляют функции текстурирования в 2.5D движке — им 15 лет и я тогда написал именно так, а разбираться заново стало лень. Там действительно t1-индекс внутри текстуры, x1-координата, ну и т.д.

              4)константные данные следует помещать в const, а не толкать в дефайны.


              Не люблю глобальные переменные. А вот define смотрится органично и находится в тексте очень легко — большие буквы. А недостатки есть у многих вещей, но это не повод от них отказываться.
                0
                многие данные по смыслу являются векторами. Вот и работайте с ними, как с векторами.


                А, вы не про std:vector. Тогда зачем координаты представлять векторами?
                  0
                  зачем координаты представлять векторами?

                  Дело в том, что программа от этого сокращается раза эдак в два (когда 2D). Кроме того, данная абстракция позволяет лучше понять суть производимых с координатами операций.

                  А git clone вызывало ошибку kerlen322.dll — у меня Windows XP
                  У меня работает

                    0
                    Дело в том, что программа от этого сокращается раза эдак в два (когда 2D). Кроме того, данная абстракция позволяет лучше понять суть производимых с координатами операций.


                    Всё может быть. Но по-моему, программа и так достаточно прозрачна и вполне себе понятна.

                    У меня работает


                    А у меня portable-версия не работает.
                      +2

                      Плохому танцору…
                      В нынешние времена знание git/svn — жизненно необходимый навык для любого программиста.

                        0
                        Есть фирмы, где программу пишут один-два человека, и никаких систем контроля версий никто никогда не применял и не планирует применять.
                        И я инженер, а не программист или инженер-программист.
                          +1
                          Знаю инженеров или инженеров-программистов которые используют fossil (но не вижу смысла и не рекомендую)
                          Сам использую git чтобы… иногда посмотреть на предыдущую версию исходников.
                          Чтобы использовать github, git вообще не нужен. (ну чтобы выкладывать исходники и релизы)
                          1. git init
                          2. в .gitignore пишем обычными масками какие файлы в git не ложить
                          3. git add.; git commit -a -m «bla-bla changes» (у меня не windows и там есть alias-ы и выглядит как commit «bla-bla changes») и забываем про git add
                          4. gitk (gitgui) удобно чтобы просто глянуть на предыдущие версии исходников
                          5. git log — список версий
                          6. git checkout 1234567890 — переходим на предыдущую версию, где 123 — хэш из п.5
                          7. git chackout master — вернутся на самую последнюю версию
                          Проблемы могут возникнуть если файлы отредактировать, незакоммитить и перейти на другую версию, но все решается гуглом и stackoverflow.
                          0
                          И кстати, git нужен интернет или можно поднять на локальной машине? Потому как clone я делал как раз для локальной машины (вместо url вбивал путь к папке). Я бы git внедрил бы, но мне нужно, чтобы работало без интернета и без сетки. Потому как разрабатываемое ПО не предназначено для публичного доступа от слова совсем, ибо относится к военной аппаратуре.
                            0
                            Для git вообще «сервер» не нужен. Это просто толпа файлов в папке .git и сценарии для их обработки. Git может использовать некоторые протоколы для того, чтобы синхронизировать изменения между хранилищами.

                            Ничего «поднимать» для одиночного использования не нужно — достаточно
                            1) сделать git init в папке с исходниками (создаст пустую структуру в папке .git),
                            2) добавить файл .gitignore, в котором перечислить маски имен файлов, за которыми следить не нужно
                            3) перед каждым коммитом делать git add --all
                              0
                              А как разделить проекты? Я же не могу добавить все файлы в одну кучу. А как брать репозиторий в таком случае?
                                +1
                                У каждого проекта в корне сделать git init.

                                Если есть зависимости между проектами, есть две стратегии соединения репозиториев — submodule и subtree

                                Под «брать репозиторий» вы имеете в виду совместную работу? или «как получить из репозитория исходники для работы, что-то с ними сделать и положить результат обратно?»

                                Если второе, то это и не нужно. Вы просто работаете с файлами прямо в своей локальной копии (там, где у вас папка .git лежит). Вы можете приказать git выудить из репозитория конкретнyю ветку (git checkout) — тогда все отслеживаемые файлы у вас заменятся на нужные.
                                  0
                                  А, понятно, спасибо. Меня интересует и как совместную работу организовать и как получать исходники для работы и отправлять обратно. И ещё есть идея поставить на некий сетевой компьютер.

                                  В книжке автор запрашивает git clone с сервера .project. А откуда этот ".project" возьмётся в папке .git? Или это просто переименованный .git?
                                    0
                                    У git есть настройка «origin» — это как раз репозиторий (может быть сетевым), с которым он пытается синхронизироваться по умолчанию, когда ему командуют git pull/push

                                    Штатным средством сделать git сетевым является взять машину с линуксом и ssh, раздать всем ключи RSA подлиннее и создать на ней bare — репозиторий (git --bare init)

                                    Но так как git работает просто с папками, такой репозиторий можно создать хоть на машине с Windows, на общем сетевом диске. Примапить его на какую-то букву и дальше push/pull туда.
                                      0
                                      Ну, посмотрим, что получится.
            0
            Когда то в начале 90-стых тоже с другом занимался, на ассемблере на Profi клон ZX Spectrum, основная проблема не хватало скорости чтобы тащить карту по столу, несколько дней переписывали процедуру высчитывая машинные такты.
              +2
              В штатном видеорежиме спектрума или в каком-то своём у профи? В штатном должно хватать скорости на асме — там спрайт ведь небольшой.
                0
                в расширенном насколько помню при таскании карта дергалась вот и ужимали, а еще портировали музыку с писишных игрушек и крутили ее вовремя игры через AY, так еще у по продавать пасьянс, через разработчика Profi (кондор вроде), умудрились, правда пришлось к игрушке лепить защиту от копирования с записью секретного ключа в межсекторной области дискеты.
                  –2
                  Золотые времена! :)
                    –1
                    Надо же, кому-то не понравился этот мой комментарий. :) Удивительно.
              +1
              помню я писала шахматы… ох были времена, я тогда еще студенткой была, это мой диплом был такой, хэх сейчас даже смешно немного
                –1
                А про шахматы у меня статья уже была. :) Вот эта.
                +1
                Видел исходники оригинальной косынки в числе утекших исходников Win2k.
                Там же есть Сапёр и Реверси.
                Исходников оригинального Паука не видел, декомпилирую потихоньку в качестве хобби…
                  0
                  Не, я только косынку умею играть. Научил друг в далёком 2004 году. С тех пор и играю. :)
                  Кстати, сапёр у меня тоже есть самодельный. Может, и по нему какую-нибудь статью напишу. Вдруг кому столь простые игры интересны будут.
                    0
                    Если собрать все самодельны саперы, и еще те что под Linux получится целая БСЭ.
                    Но сам по себе сапер… непродуман и малоинтересен. Нацельтесь хотя-бы на пазлы от разработчика putty (в низу они под Windows, *nix, Andoid, Palm, iOS).
                      0
                      «Паука» много раскладывал с выходом XP и до сих пор балуюсь, параллельно с с просмотром каких-н. видео.
                      До компилябельного состояния еще не довёл…

                      Реверси увлекался в институте, в 90-е гг. Реверси шел в комплекте древней Windows 3.0, но запускался еще на XP. Хоть он и 16-разрядный, под wine-ом работает нормально. Немного рефакторнул реверси, отделил геймплей от гуи (и сказал, что это хорошо). Гуи перевел на Qt. Теперь и для малины и для андроида его собрать можно… Выкладывать не могу — нарушение прав всё-таки…
                    +1
                    Перво-наперво, нам понадобится графика.

                    хм, для консольного варианта можно псевдографикой воспользоваться :)

                      0
                      Можно. :)
                      0
                      Стоит похвалить за сделанную игру. Но…

                      Но эту статью скинул своим ученикам как пример кода, за который они могут остаться без башки. Вы понимаете, что по таким статьям могут захотеть учиться новички? Неужели сложно хотя бы вылизать код перед тем как залить его на хабр? Это всё-таки серьёзный ресурс, а не своя стена в вк
                        0
                        Если не секрет, что конкретно вам не нравится в коде? Я не понимаю. Покажите, если не сложно, пример хорошего кода.
                        Описанное gbg, мягко говоря, субъективное мнение. Если ему нравятся константы, то я их терпеть не могу — минимум глобальных переменных. Если его смущает функция больше 20 строчек, то это тоже маразм — линейная последовательность без ветвлений не требует сокращения. Функциональное деление? Вообще-то, оно там есть. Странные переменные? В них есть логика. s — source, d-destination, n,m,k,l — переменные циклов исторически (в таком порядке), 1-начальная точка, 2-конечная, d_чего-либо — приращение величины. xl-левая граница, xr-правая. Ну и так далее. Можно, конечно, макросами задать всякие 53 и коды ящиков. Но не сделал.
                          0
                          Я не являюсь программистом по специальности — моя специальность квантовая электроника и лазерная техника. Однако, я очень часто пишу программы. Я пишу их около 20 лет на самых разных языках (от бейсика до Си через паскаль и ассемблеры разных процессоров) под разные операционные системы от Windows до QNX. Насколько сложные я пишу программы? Трудно сказать, но аппаратными комплексами уровня изделий военного назначения они вполне управляют. И я не очень понимаю, чего конкретно вы ждёте от кода? Я видел код многих изделий — там нет также ничего понятного стороннему человеку. Не зная идеи работы программы, невозможно в ней разобраться. А идея может быть очень непростой. Для этого и существуют комментарии.
                          Вот смотрите, давайте возьмём вот эту функцию:

                          //----------------------------------------------------------------------------------------------------
                          //переместить карту из ящика 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::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;
                           }
                          }
                          


                          По-моему, они прозрачны донельзя. Или я ошибаюсь?
                            0
                            случайно промазал и не туда ответил)
                            https://habrahabr.ru/post/330470/#comment_10265140
                          0
                          Прошу прощения что резко ответил утром, формулировка некрасивая вышла.

                          1) стиль именования функций и переменных, который разный. Это не способствует удобному прочтению кода. Где-то есть подчёркивания, где-то нету, тоже не всегда ясно с большими/маленькими буквами. Так же вопрос к названиям переменных, которые не всегда прозрачны

                          2) Я посоветую немного попрактиковаться в написании чего-то ООПшного. Не обязательно с наследованием и т.д., а именно с позиции «ООП как стиль мышления».

                          К примеру, возьмём наш «ящик». В коде был такой фрагмент
                          //ищем первые свободные места в ящиках
                          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;
                          }

                          И с точки зрения быстродействия, и с точки зрения читабельности было бы круто вынести весь функционал «ящика» в структуру, в которой инкапсулировать весь нужный функционал. В итоге, написать что-то типа
                          s_end=sCard_Box[s].get_border_number();
                          И это упростит код не только тут, но и в других частях.

                          П.с.: потом увидел в коде ремарку именно про эту часть. Но тут дело не только в быстродействии, но и в читабельности и удобстве.

                          3) я бы посоветовал не использовать «магические константы». А допустим мы придумаем как изменить или разнообразить нашу игру;) Захотим добавить пятую-шестую масти, да и размер поля увеличить тогда. А вдруг. Захотелось) Нет смысла закладывать преждевременную основу для всех-всех модификаций. Но в тех местах, где это можно сделать легко — так стоит сделать. Например, ввести переменные для номеров 0,1,2,8,9,12. Опять-таки, даже риск механической ошибки тогда меньше. Когда кода станет много — некоторые такие заразки можно долго ловить по всей программе.
                          Ну и макросов многовато, это не считается хорошей практикой. Их можно заменить константами

                          4) в ChangeCard есть такое

                          CARD_SUIT md=sCard_Box[d_box][d_index].Suit;
                          CARD_SUIT ms=sCard_Box[s_box][s_index].Suit;

                          это удобно, ведь мд и мс будет явно легче читать, и ясно что оно такое, как их ввели(хоть и можно придраться к названиям: р). Читать полотно из ифов с кучей индексов сложно. Как вариант, использовать указатели на карты, что бы не писать индексы постоянно.

                            0
                            Прошу прощения что резко ответил утром, формулировка некрасивая вышла.

                            Да обычная самая формулировка! :) Спасибо за комментарии! Но я думал, что проблемы будут более необычными – перечисленное мне известно, но в основном проходят как рекомендации и дело вкуса (например, класс ящиков – это во много дело вкуса – слишком простой объект, эти ящики).

                            1) Стиль именования переменных следующий: структуры и классы начинаются с большой буквы S и C. Экземпляр структуры или класса начинаются с маленькой буквы: SCard sCard. Подчёркивание указывает в данном случае индивидуальное имя объекта: CMain cMain_Local. Если класс унаследован от кого-либо, то тот, от кого унаследован класс пишется перед именем наследника, а между ними ставится подчёркивание: class CWnd_Main:public CWnd. Параметры функции чаще всего идут маленькими буквами, но могут быть и большими (если, как мне кажется, они выглядят лучше). В данном случае подчёркивание может разделять части названия или функциональное назначение: dx_1; Имена функций всегда начинаются с больших букв и в тексте большими буквами разделяются отдельные слова в имени функции.

                            2) Данная программа изначально была написана на Си. Чистом Си (потому что с cpp компилятором для PSP я разобрался позже). Поэтому из ООП тут только обёртка

                            3) Ну, с магическими числами я согласен. А вот с константами вместо макросов не соглашусь. Это дело вкуса. Да, макрос проходит через единицу трансляции, но не для этого ли он макросом и делался? Он потому и макрос, что должен быть виден и доступен во всём коде. Константа же – локальная фишка. Локальность требуется редко. К тому же непонятно, как удобно именовать константы, чтобы в коде сразу было видно, что это не переменная, с которой можно что-то делать и при этом легко её найти. Макрос же у меня пишется всегда большими буквами и легко выделяется в коде. И сразу же ясно, что это – макрос, не переменная.

                            4) Дело в том, что ради одного-двух считываний делать указатель на элемент с индексом бессмысленно. Он не добавит читабельности, ибо будет использован те же один-два раза.

                          Only users with full accounts can post comments. Log in, please.