Marmalade SDK и кроссплатформенность


Marmalade SDK как среда для разработки кроссплатформенных мобильных (и не только) приложений предоставляет разработчику C++ API. По сути, это набор расширений (Extensions), каждое из которых внутри себя содержит конкретную реализацию функционала (работа с графикой, файловой системой, сетью, UI, внутриигровые покупки, работа с аудио-видео и т. д.) под каждую отдельную платформу (Android, iOS, WinPhone и множество других платформ).

Поэтому разработчику в процессе написания практически нет необходимости завязываться на особенности той или иной платформы, за исключением некоторых случаев (возможно список неполный):
— некий функционал может не поддерживаться в определенной ОС;
— разработчик сам решил реализовать логику приложения по-разному в зависимости ОС.

Но вот в случае, если разработчику потребуется функционал, который отсутствует в стандартном дистрибутиве Marmalade SDK, ему может потребоваться самому собрать свой Extension со своей реализацией под каждую платформу — а значит писать платформозависимый код.

Лично мне для реализации данного приложения хватило стандартного набора расширений, поэтому платформозависимого кода у меня нет. Большая часть тестирования и отладки (примерно 90%) была произведена на винде на симуляторе (остальные 10% — это отладка масштабирования на устройстве — так как для этого нужны zoom-жесты двумя пальцами). Билды под Android и iOS также собираются на винде (для этого нужно поставить соответствующие инструменты, описанные в доке мармелада). Мак нужен только для заливки ipa-файла в консоль iTunesConnect через эппловский ApplicationLoader.


Пара слов о том, что же такое филиппинский кроссворд


Филиппинский кроссворд – это такой вид графических головоломок, в которых с помощью множества пар чисел зашифрована картинка. Все кроссворды должны иметь единственное решение. Необходимо подобрать и соединить пары одинаковых чисел линиями так, результат удовлетворял следующим условиям (на которые я буду неоднократно ссылаться при дальнейшем изложении статьи):

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

Так как единица не имеет пары, то она закрашена по умолчанию. В результате решения кроссворда, когда все пары чисел (кроме единиц) соединены линиями, получается некоторый рисунок. Ознакомление с нижеприведенной gif-кой прольет свет на процесс решения филиппинского кроссворда:

image

Данная задача решалась в среде Marmalade SDK с использованием языка программирования C++, в результате чего были собраны версии приложения под iOS и Android.

Представление состояния кроссворда в оперативной памяти


Сетка кроссворда состоит из ячеек, каждая из которых может быть закрашена или не закрашена (в разный момент времени), а так же иметь или не иметь в себе число (это свойство ячейки не меняется).

Минимальное число, используемое в моих кроссвордах – 1, максимальное – 9. В зависимости от того, каким образом линия проходит через клетку, она может быть закрашена несколькими способами (т.е. иметь несколько различных состояний).

Перечислим их:

  • 1 – клетка не закрашена, т. е. линия через нее не проходит;
  • 2 – клетка закрашена, но линия через нее также не проходит. В таком состоянии находятся все клетки с числом 1.
  • 3 – линия проходит через клетку вертикально;
  • 4 – линия проходит через клетку горизонтально;
  • 5 – линия проходит через верхнюю и левую сторону квадрата клетки;
  • 6 – линия проходит через нижнюю и левую сторону квадрата клетки;
  • 7 – линия проходит через верхнюю и правую сторону квадрата клетки;
  • 8 – линия проходит через нижнюю и правую сторону квадрата клетки;
  • 9 – клетка является крайней клеткой линии и линия проходит через левую сторону квадрата клетки;
  • 10 – клетка является крайней клеткой линии и линия проходит через правую сторону квадрата клетки;
  • 11 – клетка является крайней клеткой линии и линия проходит через верхнюю сторону квадрата клетки;
  • 12 – клетка является крайней клеткой линии и линия проходит через нижнюю сторону квадрата клетки.

Примеры таких состояний наглядно показаны на рисунке ниже


В результате решения кроссворда пользователь получает следующее изображение:



Первое, что приходит на ум в качестве способа представления состояния кроссворда — это двумерный массив, т. е. матрица char-элементов. Тип char имеет размерность в 1 байт, т. е. позволяет хранить одно из 2^8 = 256 состояний.

Разобьем 8 бит байта на две группы: младшие и старшие биты. Получаем 4 бита в каждой группе, каждая группа дает возможность хранить 2^4 = 16 состояний.

image

Таким образом, выделяем 4 старших бита для хранения информации о способе закрашивания ячейки, а 4 младших бита для хранения информации о числе, указанном в ячейке.

#define BYTE_NUMBER_PART  15  //  в двоичной системе 00001111
#define BYTE_FLAG_PART    240 //  в двоичной системе 11110000

Описание структуры кроссворда
struct JCStruct
{
    bool Resolved;     // признак решенности кроссворда
    char FileName[255];    // имя файла задания
    char W;    // ширина сетки
    char H;     // высота сетки
    
    char M[MAX_PUZZLE_HW][MAX_PUZZLE_ HW]; // матрица состояния ячеек
    
    char Vector[CHANGE_VECTOR_SIZE][3]; // вектор изменений для возможности отмены действий [i, j, old_value]
    int  Vector_s; // vector start pointer (ссылается на первый элемент в очереди)
    int  Vector_e; // vector end pointer   (ссылается на первый пустой элемент очереди)
    
    int  DigitsCnt; // Кол-во чисел в кроссворде (нужно для оптимизации алгоритма рисования чисел)
};



Объявляем функции, позволяющие с помощью побитового умножения получить значение старшей (флаг закраски) и младшей (число в ячейке) группы бит.

char GetNumberFromByte(char c) // функция возвращает число, указанное в ячейке. Если числа нет, то функция вернет 0
{
    return c & BYTE_NUMBER_PART;
}

char GetFlagFromByte  (char c) // функция возвращает флаг закраски ячейки. Вернет 0, если ячейка не закрашена
{
    return c & BYTE_FLAG_PART;
}

Кодируем возможные флаги закрашивания ячеек кроссворда:

#define LN_ONE          16  // 00010000 клетка закрашена, но линия ч/з нее не проходит. для клеток с числом 1
#define LN_VERTICAL     32  // 00100000 линия проходит ч/з клетку вертикально;
#define LN_HORIZONTAL   48  // 00110000 линия проходит ч/з клетку горизонтально;
#define LN_LEFT_TOP     64  // 01000000 линия проходит ч/з нижнюю и правую сторону квадрата клетки;
#define LN_LEFT_BOTTOM  80  // 01010000 линия проходит ч/з верхнюю и правую сторону квадрата клетки;
#define LN_RIGHT_TOP    96  // 01100000 линия проходит ч/з нижнюю и левую сторону квадрата клетки;
#define LN_RIGHT_BOTTOM 112 // 01110000 линия проходит ч/з верхнюю и левую сторону квадрата клетки;
#define LN_RIGHT        128 // 10000000 клетка является крайней клеткой линии и линия проходит ч/з левую сторону клетки;
#define LN_LEFT         144 // 10010000 клетка является крайней клеткой линии и линия проходит ч/з правую сторону клетки;
#define LN_TOP          160 // 10100000 клетка является крайней клеткой линии и линия проходит ч/з нижнюю сторону клетки;
#define LN_BOTTOM       176 // 10110000 клетка является крайней клеткой линии и линия проходит ч/з верхнюю сторону клетки.

Пример использования
JCStruct* jc;
...
if (GetFlagFromByte(jc->M[i][j]) == LN_HORIZONTAL) // если клетка закрашена и линия проходит через нее по горизонтали  	
{
}


Далее весь функционал по отрисовке и изменению кроссворда реализуем используя вышеприведенные константы и функции.

Сценарий соединения двух чисел линиями


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

Текущая линия, которую пользователь ведем пальцем, в отражается в структуре CurrentLineStackStruct:

struct PointStruct
{
    int x; // координата ячейки в сетке по горизонтали
    int y; // координата ячейки в сетке по вертикали
};

struct CurrentLineStackStruct // стек, где будет храниться текущая линия
{
    char num;               // число, с которого начали линию
    PointStruct stack[9];   // стек, в котором отражаются ячейки, через которой проходит текущая линия
    char len;               // указатель стека
};

CurrentLineStackStruct LineStack;

При инициализации, а также при каждом «отжатии» пальца от экрана этот стек инициализируется в исходное состояние вызовом следующей функции:

void ClearCurrentStack()
{
    LineStack.len = 0;   // длину стека обнуляем
    LineStack.num = 0;   // число, с которого стартует стек обнуляем
}

Обработка ведения пальца/стилуса по сетке кроссворда осуществляется функцией Redraw, которая возвращает true, если после обработки кроссворд требуется перерисовать, и false — в противном случае.

Redraw
/*
jc     - указатель на структуру JCStruct
DrawContext - структура-контекст, которая хранит текущие геометрические параметры отрисовки, в частности:
    jc_screen_x - координата x левого верхнего угла сетки кроссворда
    jc_screen_y - координата y левого верхнего угла сетки кроссворда
    cell_wh   - высота и ширина ячейки кроссворда
    jc_screen_w - ширина всей сетки кроссворда
    jc_screen_h - высота всей сетки кроссворда
*/

bool Redraw(int x, int y) // x,y -- координаты пальца-стилуса
{
  if (!jc->Resolved) // Решенный кроссворд нельзя менять, можно только стереть все
    if ((DrawContext.jc_screen_x <= x) && (x < DrawContext.jc_screen_x + DrawContext.jc_screen_w))
      if ((DrawContext.jc_screen_y <= y) && (y < DrawContext.jc_screen_y + DrawContext.jc_screen_h))
      {
        // получаем координаты ячейки
        int i = (x - DrawContext.jc_screen_x) / DrawContext.cell_wh;
        int j = (y - DrawContext.jc_screen_y) / DrawContext.cell_wh;

        // Если стек еще не начат, а на ячейке нет числа, то выходим
        char n = GetNumberFromByte(jc->M[i][j]);

        if ((LineStack.len == 0) && (n == 0)) return false;

        // Если ячейка уже закрашена, то выходим
        if (GetFlagFromByte(jc->M[i][j]) > 0) return false;

        // смотрим, может мы вернулись назад на одну из предыдущих клеток стека
        for (int s = 0; s < LineStack.len; s++)
          if ((LineStack.stack[s].x == i) && (LineStack.stack[s].y == j))
          {
            // укорачиваем стек
            LineStack.len = s + 1;
            return true;
          }

        // Если стек еще не начат, то начинаем его с того числа, на которое нажали (то что это число следует из условия выше)
        if (LineStack.len == 0) 
        {
          // Стартуем стек
          LineStack.len++;
          LineStack.num = n; // запоминаем число, с которого начали

          // записываем первую точку в стек
          LineStack.stack[LineStack.len - 1].x = i;
          LineStack.stack[LineStack.len - 1].y = j;

          return true;
        }
        else // если стек уже начат, то значит это очередная ячейка
        {
          // смотрим, чтобы не было переполнения
          // если есть еще куда добавлять ячейку (длина линии не может быть больше num)
          if (LineStack.len < LineStack.num) 
          {
            // смотрим, чтобы новая ячейка была соседней, по отношению к последней добавленной
            if ((abs(LineStack.stack[LineStack.len - 1].x - i) == 1 && LineStack.stack[LineStack.len - 1].y == j) 
                  ||
                (LineStack.stack[LineStack.len - 1].x == i && abs(LineStack.stack[LineStack.len - 1].y - j) == 1)
               )
            {
              // соседняя ячейка должна быть либо пустой, либо иметь такое число, как и то, 
              // с которого начали, при этом длине линии не должно хвать именно этой одной клетки
              if (n == 0 || LineStack.num == n && LineStack.len == n - 1)
              {
                LineStack.len++;
                LineStack.stack[LineStack.len - 1].x = i;
                LineStack.stack[LineStack.len - 1].y = j;
                return true;
              }

              return false;
            }

            // Если новая ячейка не соседняя, но лежит на одной горизонтали с последней
            if ((LineStack.stack[LineStack.len - 1].x != i) && (LineStack.stack[LineStack.len - 1].y == j))
            {
              int len = abs(i - LineStack.stack[LineStack.len - 1].x); // определяем длину приращения
              int d = (i - LineStack.stack[LineStack.len - 1].x) / len; // определяем направление (знак приращения)

              for (int s = 0; s < len; s++)
              {
                if (LineStack.len < LineStack.num)
                {
                  // Если наткнулись на закрашенную ячейку
                  if (GetFlagFromByte(jc->M[LineStack.stack[LineStack.len - 1].x + d][j]) > 0) 
                  {
                    if (s > 0) return true;	 // Если перед этим уже что-то добавили, то делаем перерисовку
                    else    return false; // Если ничего еще не добавили, то перерисовка не нужна
                  }

                  n = GetNumberFromByte(jc->M[LineStack.stack[LineStack.len - 1].x + d][j]);

                  if (n > 0) // Если наткнулись на числовую ячейку
                  {
                    if (n != LineStack.num) // Если ее значение не равно числу, с которого начали
                    {
                      if (s > 0) return true;	 // Если перед этим уже что-то добавили, то делаем перерисовку
                      else    return false; // Если ничего еще не добавили, то перерисовка не нужна
                    }
                    else // Если число в ячейке равно числу, с которого начали, т.е. n == LineStack.num
                    {
                      if (LineStack.num != LineStack.len + 1) // Если как раз не хватает только одной ячейки
                      {
                        if (s > 0) return true;	 // Если перед этим уже что-то добавили, то делаем перерисовку
                        else    return false; // Если ничего еще не добавили, то перерисовка не нужна
                      }
                    }
                  }

                  LineStack.len++;
                  LineStack.stack[LineStack.len - 1].x = LineStack.stack[LineStack.len - 2].x + d;
                  LineStack.stack[LineStack.len - 1].y = j;
                }
              }

              return true;
            }

            // Если новая ячейка не соседняя, но лежит на одной вертикали с последней
            if ((LineStack.stack[LineStack.len - 1].x == i) && (LineStack.stack[LineStack.len - 1].y != j))
            {
              int len = abs(j - LineStack.stack[LineStack.len - 1].y);  // определяем длину приращения
              int d = (j - LineStack.stack[LineStack.len - 1].y) / len; // определяем направление (знак приращения)

              for (int s = 0; s < len; s++)
              {
                if (LineStack.len < LineStack.num)
                {
                  // Если наткнулись на закрашенную ячейку
                  if (GetFlagFromByte(jc->M[i][LineStack.stack[LineStack.len - 1].y + d]) > 0) 
                  {
                    if (s > 0) return true;	 // Если перед этим уже что-то добавили, то делаем перерисовку
                    else    return false; // Если ничего еще не добавили, то перерисовка не нужна
                  }

                  n = GetNumberFromByte(jc->M[i][LineStack.stack[LineStack.len - 1].y + d]);

                  if (n > 0) // Если наткнулись на числовую ячейку
                  {
                    if (n != LineStack.num) // Если ее значение не равно числу, с которого начали
                    {
                      if (s > 0) return true;	 // Если перед этим уже что-то добавили, то делаем перерисовку
                      else    return false; // Если ничего еще не добавили, то перерисовка не нужна
                    }
                    else // Если число в ячейке равно числу, с которого начали, т.е. n == LineStack.num
                    {
                      if (LineStack.num != LineStack.len + 1) // Если как раз не хватает только одной ячейки
                      {
                        if (s > 0) return true;	 // Если перед этим уже что-то добавили, то делаем перерисовку
                        else    return false; // Если ничего еще не добавили, то перерисовка не нужна
                      }
                    }
                  }

                  LineStack.len++;
                  LineStack.stack[LineStack.len - 1].x = i;
                  LineStack.stack[LineStack.len - 1].y = LineStack.stack[LineStack.len - 2].y + d;
                }
              }

              return true;
            }

            return false;
          }
          else // если переполнение, то выходим
          {
            return false;
          }
        }
      }

  return false;
}


При «отжатии» пальца/стилуса делам обработку стека текущей линии. Необходимо понять, правильно ли нарисована линия, т. е. проверить ее на корректность, и в случае корректности обновить соответствующие ячейки матрицы.

CheckCurrentLineStack
/* тут мы должны принять решение о том, 
   что делать с текущим стеком линии - заносить ее в основную матрицу перед тем, как стереть
*/
void CheckCurrentLineStack() 
{
// Если стек не пуст, если длина стека соответствует первому числу, 
// если число, с которго начинали, равно числу в последней ячейке, то значит линия корректна
if ((LineStack.len > 0) && (LineStack.len == LineStack.num) && 
    (LineStack.num == GetNumberFromByte(jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y]))
   )
{
    // Добавляем линию в вектор отмены
    SetMatrixElement(jc, LineStack.stack[0].x, LineStack.stack[0].y/*, 1*/);

    // перерисовываем текущий стек в матрицу М
    
    // признаки того, где находится предыдущая/следующая клетка [0 - слева, 1 - справа, 2 - сверху, 3 - снизу]
    char l_prev, l_next; 

    // сначала рисуем всю линию, кроме первой и последней клетки
    for (int i = 1; i < LineStack.len - 1; i++)
    {
        // определяем положение пред клетки
        if (LineStack.stack[i - 1].x + 1 == LineStack.stack[i].x) l_prev = 0;
        if (LineStack.stack[i - 1].x - 1 == LineStack.stack[i].x) l_prev = 1;
        if (LineStack.stack[i - 1].y + 1 == LineStack.stack[i].y) l_prev = 2;
        if (LineStack.stack[i - 1].y - 1 == LineStack.stack[i].y) l_prev = 3;

        // определяем положение след. клетки
        if (LineStack.stack[i + 1].x + 1 == LineStack.stack[i].x) l_next = 0;
        if (LineStack.stack[i + 1].x - 1 == LineStack.stack[i].x) l_next = 1;
        if (LineStack.stack[i + 1].y + 1 == LineStack.stack[i].y) l_next = 2;
        if (LineStack.stack[i + 1].y - 1 == LineStack.stack[i].y) l_next = 3;

        l_prev = MAX(l_prev, l_next) * 10 + MIN(l_prev, l_next);

        switch (l_prev)
        {
            case 32: jc->M[LineStack.stack[i].x][LineStack.stack[i].y] = 
                jc->M[LineStack.stack[i].x][LineStack.stack[i].y] + LN_VERTICAL; break;
            case 31: jc->M[LineStack.stack[i].x][LineStack.stack[i].y] = 
                jc->M[LineStack.stack[i].x][LineStack.stack[i].y] + LN_LEFT_TOP; break;
            case 30: jc->M[LineStack.stack[i].x][LineStack.stack[i].y] = 
                jc->M[LineStack.stack[i].x][LineStack.stack[i].y] + LN_RIGHT_TOP; break;
            case 21: jc->M[LineStack.stack[i].x][LineStack.stack[i].y] = 
                jc->M[LineStack.stack[i].x][LineStack.stack[i].y] + LN_LEFT_BOTTOM; break;
            case 20: jc->M[LineStack.stack[i].x][LineStack.stack[i].y] = 
                jc->M[LineStack.stack[i].x][LineStack.stack[i].y] + LN_RIGHT_BOTTOM; break;
            case 10: jc->M[LineStack.stack[i].x][LineStack.stack[i].y] = 
                jc->M[LineStack.stack[i].x][LineStack.stack[i].y] + LN_HORIZONTAL; break;
        }
    }
    // определяем для первой клетки			
    if (LineStack.stack[1].x + 1 == LineStack.stack[0].x) 
        jc->M[LineStack.stack[0].x][LineStack.stack[0].y] = jc->M[LineStack.stack[0].x][LineStack.stack[0].y] + LN_RIGHT;
    if (LineStack.stack[1].x - 1 == LineStack.stack[0].x) 
        jc->M[LineStack.stack[0].x][LineStack.stack[0].y] = jc->M[LineStack.stack[0].x][LineStack.stack[0].y] + LN_LEFT;
    if (LineStack.stack[1].y + 1 == LineStack.stack[0].y) 
        jc->M[LineStack.stack[0].x][LineStack.stack[0].y] = jc->M[LineStack.stack[0].x][LineStack.stack[0].y] + LN_BOTTOM;
    if (LineStack.stack[1].y - 1 == LineStack.stack[0].y) 
        jc->M[LineStack.stack[0].x][LineStack.stack[0].y] = jc->M[LineStack.stack[0].x][LineStack.stack[0].y] + LN_TOP;


    // определяем для последней клетки
    if (LineStack.stack[LineStack.len - 2].x + 1 == LineStack.stack[LineStack.len - 1].x) 
        jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] = 
            jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] + LN_RIGHT;
    if (LineStack.stack[LineStack.len - 2].x - 1 == LineStack.stack[LineStack.len - 1].x) 
        jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] = 
            jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] + LN_LEFT;
    if (LineStack.stack[LineStack.len - 2].y + 1 == LineStack.stack[LineStack.len - 1].y) 
        jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] = 
            jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] + LN_BOTTOM;
    if (LineStack.stack[LineStack.len - 2].y - 1 == LineStack.stack[LineStack.len - 1].y) 
        jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] = 
            jc->M[LineStack.stack[LineStack.len - 1].x][LineStack.stack[LineStack.len - 1].y] + LN_TOP;

    DrawContext.need_save = true;
}
}


Отрисовка кроссворда и текущей линии


Отрисовка кроссворда делится на следующие этапы:

  • Рисование закрашенных ячеек матрицы;
  • Рисование ячеек стека текущей линии;
  • Рисование линий на закрашенных ячейках матрицы;
  • Рисование линий на ячейках стека текущей линии;
  • Рисование чисел на ячейках.

Дабы не излишне не перегружать статью кодом, приведем только листинг самого из интересного из вышеуказанных пунктов.

Рисование линий на закрашенных ячейках матрицы
if (jc->Resolved == false) // Если кроссворд не решен
{
    //Рисуем на клетках соединительные линии
    Iw2DSetColour(ColorSchema.JCCellLineColor);

    for (int i = 0; i < jc->W; i++)
        for (int j = 0; j < jc->H; j++)
        {
            n1 = jc->M[i][j] & BYTE_FLAG_PART;

            switch (n1)
            {
            case LN_HORIZONTAL:
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + 0, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1
                                      ), 
                             CIwFVec2(DrawContext.cell_wh - 0, 3)
                            );
                break;

            case LN_VERTICAL:
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + 0), 
                             CIwFVec2(3, DrawContext.cell_wh - 0)
                            );
                break;

            case LN_RIGHT:
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + 0, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(4, 3)
                            );
                break;

            case LN_LEFT:
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i + 1)* DrawContext.cell_wh - 4, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(4, 3)
                            );
                break;

            case LN_BOTTOM:
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + 0), 
                             CIwFVec2(3, 4)
                            );
                break;

            case LN_TOP:
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j + 1)* DrawContext.cell_wh + 0 - 4), 
                             CIwFVec2(3, 4)
                            );
                break;

            case LN_RIGHT_BOTTOM:
                // горизонтальная полулиния
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + 0, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(DrawContext.cell_wh / 2 - 1 + 3, 3)
                            );
                // вертикальная полулиния						
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + 0), 
                             CIwFVec2(3, DrawContext.cell_wh / 2 - 1 + 3)
                            );
                break;

            case LN_RIGHT_TOP:
                // горизонтальная полулиния						
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + 0, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(DrawContext.cell_wh / 2 - 1 + 3, 3)
                            );
                // вертикальная полулиния			   	      
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(3, DrawContext.cell_wh / 2 - 1 + 3)
                            );
                break;

            case LN_LEFT_BOTTOM:
                // горизонтальная полулиния						
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(DrawContext.cell_wh / 2 - 1 + 3, 3)
                            );
                // вертикальная полулиния						
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + 0), 
                             CIwFVec2(3, DrawContext.cell_wh / 2 - 1 + 3)
                            );
                break;

            case LN_LEFT_TOP:
                // горизонтальная полулиния
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                                      CIwFVec2(DrawContext.cell_wh / 2 - 1 + 3, 3)
                            );
                // вертикальная полулиния
                Iw2DFillRect(CIwFVec2(DrawContext.jc_screen_x + (i)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1, 
                                      DrawContext.jc_screen_y + (j)* DrawContext.cell_wh + DrawContext.cell_wh / 2 - 1), 
                             CIwFVec2(3, DrawContext.cell_wh / 2 - 1 + 3)
                            );
                break;
            }
        }
}


Проверка решенности кроссворда


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

CheckJC
/* функция вернет 0, если кроссворд решен, 
   иначе координату первой встретившейся некорректной ячейки,
   не соответствующей требуемому конечному решению
*/
int CheckJC(JCStruct * p) 
{
    for(int i = 0; i < p->W; i++)
      for(int j = 0; j < p->H; j++)      
          if(GetNumberFromByte(p->M[i][j]) > 1) // считаем, что цифра 1 всегда закрашена по умолчанию, по этому ее не проверяем
             if(GetFlagFromByte(p->M[i][j]) == 0) // если ячейка не закрашена
                 return 100 * (i + 1) + (j + 1);

    return 0;
}

Поделиться с друзьями
-->

Комментарии (12)


  1. sergarcada
    19.05.2016 11:46

    Разгадывать такие кроссворды и самому интересно. Но какой мегамозг их составляет?


    1. akk0rd87
      19.05.2016 11:59

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


  1. bfDeveloper
    19.05.2016 12:47
    +1

    Спасибо за статью, позволю себе немного покритиковать код.

    #define LN_ONE          16  // 00010000 клетка закрашена, но линия ч/з нее не проходит. для клеток с числом 1
    

    Перестаньте объявлять константы дефайнами. В С++ есть const, который нормально живёт в неймспейсах, имеет нормальные области видимости и права доступа и вообще является хорошим стилем против плохого препроцессора.
    Флаги лучше объявлять через операторы побитовго сдвига или через литералы
    Разнообразные объединения флагов лучше писать через побитовое или, например:
    const size_t TOP_LEFT = TOP | LEFT;
    

    У вас случай посложнее, но всё равно прослеживается логика. Не зря же написаны эти комментарии с бинарным представлением. Гораздо лучше чтобы вместо объяснений был код с подобным уровнем пояснений.


    1. akk0rd87
      19.05.2016 13:00

      Спасибо за замечания. Намотаю на ус.


    1. Randl
      19.05.2016 22:44

      Плюсую. Вместо


      #define BYTE_FLAG_PART    240 //  в двоичной системе 11110000

      я б написал что-то вроде


      const uint8_t BYTE_FLAG_PART = 0b11110000;


      1. akk0rd87
        20.05.2016 01:06

        Да, теперь буду знать.


      1. monah_tuk
        20.05.2016 04:52
        +1

        Отмечу, что легальным это стало в C++ только с 14 стандарта. Раньше было как расширение GNU.


        1. akk0rd87
          23.05.2016 22:38

          Судя по всему Marmalade SDK недавно вышел на поддержку 11 стандарта, и то в beta-версии. А по факту C++03.
          beta of the Marmalade SDK with support for C++11.
          C++11 support — BETA



  1. unique_ak
    19.05.2016 13:33

    Всегда было интересно, насколько много приходится писать специфичного для платформы кода, в частности в Marmalade SDK? К сожалению, про этот SDK пишут не часто.


    1. akk0rd87
      19.05.2016 14:13

      Marmalade SDK как среда для разработки кроссплатформенных приложений предоставляет разработчику C++ API. По сути это набор расширений (Extensions), каждое из которых внутри себя содержит конкретную реализацию функционала (работа с графикой, файловой системой, сетью, UI, внутриигровые покупки, работа с аудио-видео и т. д.) под каждую отдельную платформу (Android, iOS, WinPhone и множество других платформ).

      Поэтому разработчику в процессе написания практически нет необходимости завязываться на особенности той или иной платформы, за исключением некоторых случаев (возможно список неполный):
      — некий функционал может не поддерживаться в определенной ОС;
      — разработчик сам решил реализовать логику приложения по-разному в зависимости ОС.

      Но вот в случае, если разработчику потребуется функционал, который отсутствует в стандартном дистрибутиве Marmalade SDK, ему может потребоваться самому собрать свой Extension со своей реализацией под каждую платформу — а значит писать платформозависимый код.

      Лично мне для реализации данного приложения хватило стандартного набора расширений, поэтому платформозависимого кода у меня нет.
      Большая часть тестирования и отладки (примерно 90%) была произведена на винде на симуляторе (остальные 10% — это отладка масштабирования на устройстве — так как для этого нужны zoom-жесты двумя пальцами). Билды под Android и iOS также собираются на винде (для этого нужно поставить соответствующие инструменты, описанное в доке мармелада). Мак нужен только для заливки ipa-файла в консоль iTunesConnect через эппловский ApplicationLoader.


      1. akk0rd87
        19.05.2016 14:25

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