wxMahjong/Controller.cpp
2022-06-10 05:44:42 +03:00

487 lines
25 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Controller.h"
#include <exception>
const std::array<uint8_t, 42> defaultCardsCounter{
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1};
void Controller::loadLayout(const wxString& path) {
layout.openFile(path); // открываем файл карты
gridSize = layout.getDimensions(); // получаем размеры карты
table = TLVec( // создаём трёхмерный вектор из id карт
gridSize.z,
vector<vector<CardT>>(gridSize.x, vector<CardT>(gridSize.y, EMPTY)));
layout.readLayout(table); // считываем формат поля из файла
remaining = layout.getTilesNumber(); // получаем предполагаемое количество камней
if (remaining == 144) // другие форматы не каноничны, поэтому мы их не поддерживаем
// Заполняем массив-счётчик карт с различными id
cardsCounter = defaultCardsCounter;
}
TLVec& Controller::getTable() {
return table;
}
void Controller::fill(bool solveable) {
if (solveable)
fillSolveableTable();
else
fillRandom();
}
void Controller::fillSolveableTable() {
srand(time(NULL)); // инициализируем генератор случайных чисел
auto not_end = remaining; // сохраняем в отдельную переменную количество оставшихся камней
PosSet positions; // инициализируем сет для хранения доступных для вставки позиций
positions.insert(getRandLowest()); // вставляем случайную начальную позицию
auto next_ptr = positions.begin(); // инициализируем указатель на позицию, куда будет вставляться следующий камень
while (!positions.empty()) {
auto id = genRandId();
emplace_table(id, *next_ptr, positions); // вставляем id в next_ptr
not_end--; // уменьшаем счётчик оставшихся для вставки камней
next_rand(positions, next_ptr, false, not_end); // Находим случайную новую позицию так, чтобы она не накрывала предыдущую
if (id < 34) // если id парный
cardsCounter[id]--; // уменьшаем счётчик карт этого id ещё на 1, так как вставим его ещё раз
else
id = getFreeSingularId(id);
emplace_table(id, *next_ptr, positions); // вставляем id в next_ptr
not_end--; // уменьшаем счётчик оставшихся для вставки камней
next_rand(positions, next_ptr, true, not_end); // Находим случайную новую позицию
}
}
wxPoint Controller::getRandLowest() const {
int overall = gridSize.x * gridSize.y; // вычисляем количество позиций в горизонтальном "срезе" массива
int x, y; // объявляем координаты для возвращаемой позиции
do {
int pos = rand() % overall; // получаем случайный номер позиции
x = pos / gridSize.y; // вычисляем x
y = pos % gridSize.y; // и y
} while (table[0][x][y] != FREE); // повторяем цикл, если эта позиция недоступна для вставки
return wxPoint(x, y); // возвращаем wxPoint
}
void Controller::emplace_table(CardT id, const ThreePoint& pos, PosSet& positions) {
table[pos.z][pos.x][pos.y] = id;
push_available(positions, pos); // записываем в сет новые позиции
}
#ifdef WXDEBUG // Если компилируем в режиме дебага
#include <wx/file.h>
void print_list(const PosSet& positions) {
wxFile f("tmp.txt", wxFile::write_append); // Открываем файл для записи
for (const auto& el : positions) // Итерируемся по всем позициям
f.Write(itowxS(el.z) + " " + itowxS(el.x) + " " + itowxS(el.y) + "\n"); // Выводим координаты в файл
f.Write("_ size: " + itowxS(positions.size()) + "\n"); // В конце выводим количество элементов
}
#endif
void Controller::next_rand(PosSet& positions,
PosSet::iterator& ptr,
bool canOverlap, uint8_t& not_end) {
#ifdef WXDEBUG // Если компилируем в режиме дебага
print_list(positions); // выводим список позиций
#endif
ThreePoint prev = *ptr; // сохраняем предыдущее значение итератора
positions.erase(ptr); // удаляем только что вставленный итератор
if (not_end) { // если ещё есть камни для вставки
if (positions.empty()) // если не осталось позиций,
ptr = positions.insert(getRandLowest()).first; // вставляем любую позицию из нижней плоскости и используем её в следующий раз
else { // иначе
ptr = positions.begin(); // устанавливаем итератор в первую позицию
int rand_d = rand() % positions.size(); // получаем случайное смещение внутри набора возможных позиций
std::advance(ptr, rand_d); // смещаем итератор
const auto rand_ptr = ptr; // сохраняем предыдущее положение итератора
if (!canOverlap) {
while (ptr != positions.end() && wouldOverlap(prev, *ptr)) // Пока не найдём тот, что не будет закрывать только что вставленную позицию, если не canBeUp или дошли до конца набора
ptr++; // наращиваем итератор
if (ptr == positions.end()) { // если ни одна из позиций начиная с rand_ptr не подошла (нельзя выбирать накрывающую предыдущий камень и все позиции накрывают)
ptr = positions.begin(); // начинаем с начала
while (ptr != rand_ptr && wouldOverlap(prev, *ptr)) // Пока не найдём тот, что не будет закрывать только что вставленную позицию, если не canBeUp и не дошли до rand_ptr
ptr++; // наращиваем итератор
}
if (ptr == rand_ptr && wouldOverlap(prev, *ptr)) { // если итератор совпадает с rand_ptr и при этом ptr перекрывает prev,
if (not_end == positions.size()) // если уже все позиции добавлены в набор
ptr = positions.begin(); // просто выбираем первую из них
else { // иначе
auto res = positions.insert(getRandLowest()); // пытаемся вставить вставляем случайную позицию в нижней плоскости
while (!res.second) // пока не произошла вставка позиции в набор
res = positions.insert(getRandLowest()); // пытаемся вставить случайную позицию в нижней плоскости
ptr = res.first; // получаем итератор на только что вставленную позицию
}
}
}
}
}
}
bool Controller::wouldOverlap(const ThreePoint& prev, const ThreePoint& next) {
table[next.z][next.x][next.y] = 1; // вставляем в позицию next временный камень
bool res = !upFree(prev); // проверяем, будет ли свободен сверху камень в позиции prev
table[next.z][next.x][next.y] = FREE; // удаляем временный камень
return res; // возвращаем результат проверки
}
/**
* Checks if position `p`, shifted by `d`, is not out of bounds of array `table` with dimensions `gridSize`
*/
bool Controller::corrInd(const ThreePoint& p, const ThreePoint& d) const {
auto& gS = gridSize; // более короткий алиас для переменной
return ((d.z == 0) || (d.z < 0 && p.z >= -d.z) || (d.z > 0 && p.z + d.z < gS.z)) &&
((d.x == 0) || (d.x < 0 && p.x >= -d.x) || (d.x > 0 && p.x + d.x < gS.x)) &&
((d.y == 0) || (d.y < 0 && p.y >= -d.y) || (d.y > 0 && p.y + d.y < gS.y));
}
/**
* Checks if position `p`, shifted by `d`, is not out of bounds and available for insert (FREE)
*/
bool Controller::Free(const ThreePoint& p, const ThreePoint& d) const {
return corrInd(p, d) && (table[p.z + d.z][p.x + d.x][p.y + d.y] == FREE);
}
/**
* Checks if position `p`, shifted by `d`, is out of bounds or unavailable for insert (FREE)
*/
bool Controller::NFree(const ThreePoint& p, const ThreePoint& d) const {
return !corrInd(p, d) || (table[p.z + d.z][p.x + d.x][p.y + d.y] != FREE);
}
/**
* Pushes all positions, that are close to `pos`, available for insert and don't overlap other available positions
*/
void Controller::push_available(PosSet& positions,
const ThreePoint& pos) const {
auto& p = pos; // короткий алиас для переменной
int z = pos.z, x = pos.x, y = pos.y; // "разбираем" объект pos на координаты
// дальше идёт миллион условий, которые проще просто прочитать, нежели описывать, что они проверяют. В комментариях возле условий указано, в какую сторону относительно pos смещается вставляемая позиция
if (NFree(p, {-1, -2, 0}) && NFree(p, {-1, -3, 0}) && NFree(p, {-1, -2, -1}) && NFree(p, {-1, -3, -1}) && NFree(p, {-1, -2, 1}) && NFree(p, {-1, -3, 1}) && Free(p, {0, -2, 0})) // left
positions.emplace(z, x-2, y);
if (NFree(p, {-1, 2, 0}) && NFree(p, {-1, 3, 0}) && NFree(p, {-1, 2, -1}) && NFree(p, {-1, 3, -1}) && NFree(p, {-1, 2, 1}) && NFree(p, {-1, 3, 1}) && Free(p, {0, 2, 0})) // right
positions.emplace(z, x+2, y);
if (NFree(p, {0, 0, -2})) { // half top
if (NFree(p, {-1, -2, -1}) && NFree(p, {-1, -3, -1}) && NFree(p, {-1, -2, 0}) && NFree(p, {-1, -3, 0}) && NFree(p, {-1, -2, -2}) && NFree(p, {-1, -3, -2}) && NFree(p, {-1, -1, -2}) && Free(p, {0, -2, -1})) // left
positions.emplace(z, x-2, y-1);
if (NFree(p, {-1, 2, -1}) && NFree(p, {-1, 3, -1}) && NFree(p, {-1, 2, 0}) && NFree(p, {-1, 3, 0}) && NFree(p, {-1, 2, -2}) && NFree(p, {-1, 3, -2}) && NFree(p, {-1, 1, -2}) && Free(p, {0, 2, -1})) // right
positions.emplace(z, x+2, y-1);
}
if (NFree(p, {0, 0, 2})) { // half bottom
if (NFree(p, {-1, -2, 1}) && NFree(p, {-1, -3, 1}) && NFree(p, {-1, -2, 0}) && NFree(p, {-1, -3, 0}) && NFree(p, {-1, -2, 2}) && NFree(p, {-1, -3, 2}) && NFree(p, {-1, -1, 2}) && Free(p, {0, -2, 1})) // left
positions.emplace(z, x-2, y+1);
if (NFree(p, {-1, 2, 1}) && NFree(p, {-1, 3, 1}) && NFree(p, {-1, 2, 0}) && NFree(p, {-1, 3, 0}) && NFree(p, {-1, 2, 2}) && NFree(p, {-1, 3, 2}) && NFree(p, {-1, 1, 2}) && Free(p, {0, 2, 1})) // right
positions.emplace(z, x+2, y+1);
}
if (NFree(p, {-1, 0, -2}) && NFree(p, {-1, 0, -3}) && NFree(p, {-1, -1, -2}) && NFree(p, {-1, -1, -3}) && NFree(p, {-1, 1, -2}) && NFree(p, {-1, 1, -3}) && Free(p, {0, 0, -2})) // top
positions.emplace(z, x, y-2);
if (NFree(p, {-1, 0, 2}) && NFree(p, {-1, 0, 3}) && NFree(p, {-1, -1, 2}) && NFree(p, {-1, -1, 3}) && NFree(p, {-1, 1, 2}) && NFree(p, {-1, 1, 3}) && Free(p, {0, 0, 2})) // bottom
positions.emplace(z, x, y+2);
/* Higher */
if (Free(p, {1, 0, 0})) // straight
positions.emplace(z+1, x, y);
if (NFree(p, {0, -1, -2}) && NFree(p, {0, 0, -2}) && NFree(p, {0, 1, -2}) && Free(p, {1, 0, -1})) // half top
positions.emplace(z+1, x, y-1);
if (NFree(p, {0, -1, 2}) && NFree(p, {0, 0, 2}) && NFree(p, {0, 1, 2}) && Free(p, {1, 0, 1})) // half bottom
positions.emplace(z+1, x, y+1);
if (NFree(p, {0, -2, 0})) {// half left
if (NFree(p, {0, -2, -1}) && NFree(p, {0, -2, 1}) && Free(p, {1, -1, 0})) // straight
positions.emplace(z+1, x-1, y);
if (NFree(p, {0, -2, -1}) && NFree(p, {0, -2, -2}) && NFree(p, {0, 0, -2}) && Free(p, {1, -1, -1})) // half top
positions.emplace(z+1, x-1, y-1);
if (NFree(p, {0, -2, 1}) && NFree(p, {0, -2, 2}) && NFree(p, {0, 0, 2}) && Free(p, {1, -1, 1})) // half bottom
positions.emplace(z+1, x-1, y+1);
}
if (NFree(p, {0, 2, 0})) { // half right
if (NFree(p, {0, 2, -1}) && NFree(p, {0, 2, 1}) && Free(p, {1, 1, 0})) // straight
positions.emplace(z+1, x+1, y);
if (NFree(p, {0, 2, -1}) && NFree(p, {0, 2, -2}) && NFree(p, {0, 0, -2}) && Free(p, {1, 1, -1})) // half top
positions.emplace(z+1, x+1, y-1);
if (NFree(p, {0, 2, 1}) && NFree(p, {0, 2, 2}) && NFree(p, {0, 0, 2}) && Free(p, {1, 1, 1})) // half bottom
positions.emplace(z+1, x+1, y+1);
}
}
/**
* Removes all set stones and makes their positions free again
*/
void Controller::free_table() {
// Итерируемся по массиву table размерности gridSize
for (int z = 0; z < gridSize.z; z++)
for (int x = 0; x < gridSize.x; x++)
for (int y = 0; y < gridSize.y; y++) {
CardT id = table[z][x][y]; // считываем id данной ячейки
if (id >= 0) { // если это валидный id камня
cardsCounter[id]++; // наращиваем счётчик камней
table[z][x][y] = FREE;
}
}
steps = decltype(steps)(); // сбрасываем стек steps (decltype удобен, ибо тогда не зависим от класса, просто вызываем стандартный конструктор)
}
void Controller::fillRandom() {
srand(time(NULL)); // инициализируем генератор случайных чисел
wxLogDebug(itowxS(remaining));
auto not_end = remaining; // сохраняем количество оставшихся для вставки камней
// итерируемся по всему массиву поля, пока не вставим все камни
for (int z = 0; z < gridSize.z && not_end; z++)
for (int x = 0; x < gridSize.x && not_end; x++)
for (int y = 0; y < gridSize.y && not_end; y++)
if (table[z][x][y] == FREE) { // если в эту позицию можно вставить камень
table[z][x][y] = genRandId(); // получаем случайный id и вставляем его туда
not_end--; // уменьшаем счётчик оставшихся камней
}
}
CardT Controller::genRandId() {
CardT id;
do {
id = rand() % TILE_IMAGES_N; // получаем случайное число-индекс в массиве имён камней
} while (cardsCounter[id] == 0); // повторяем тело цикла, если эти id уже закончились
cardsCounter[id]--; // уменьшаем счётчик оставшихся для вставки камней этого типа
return id; // возвращаем полученный id
}
CardT Controller::getFreeSingularId(CardT prev) {
CardT id = (prev < 38) ? 34 : 38; // устанавливаем первый из id, которые считаются одинаковыми с prev
while (id < TILE_IMAGES_N && cardsCounter[id] == 0) // ищем в массиве оставшихся камней свободный id (если начинаем с 34, так как id выбираются парами, обязательно останется хотя бы один id, пренадлежащий этой группе (до 48), поэтому границу можно оставить одинаковой)
id++;
cardsCounter[id]--; // уменьшаем счётчик оставшихся id
return id; // возвращаем его
}
/**
* Gets pointer to top card by grid position
* It also changes point to top right coordinate of card
*/
CardT* Controller::getCardByPosition(ThreePoint& point) {
int topIndex = -1; // начинаем с -1, чтобы если не нашёлся ни один камень, получить невалидную позицию
CardT* res = nullptr; // указатель на элемент массива
ThreePoint realPos(point); // сохраняем копию позиции, чтобы при смещении не ломать позицию, для которой ищем
// ищем верхнюю карту с верхним левым углом в данной позиции (нажатие в левую верхнюю четверть камня)
for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x][point.y] >= 0) {
if (z > topIndex) {
topIndex = z;
res = &table[z][point.x][point.y];
}
break;
}
// ищем верхнюю карту с верхним левым углом в данной позиции, смещённой на единицу влево (нажатие в правую верхнюю четверть камня)
if (point.x > 0)
for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x - 1][point.y] >= 0) {
if (z > topIndex) {
topIndex = z;
res = &table[z][point.x - 1][point.y];
realPos.x = point.x - 1;
realPos.y = point.y;
}
break;
}
// ищем верхнюю карту с верхним левым углом в данной позиции, смещённой на единицу вверх (нажатие в левую нижнюю четверть камня)
if (point.y > 0)
for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x][point.y - 1] >= 0) {
if (z > topIndex) {
topIndex = z;
res = &table[z][point.x][point.y - 1];
realPos.x = point.x;
realPos.y = point.y - 1;
}
break;
}
// ищем верхнюю карту с верхним левым углом в данной позиции, одновременно смещённой вверх и влево (нажатие в правую нижнюю четверть камня)
if (point.x > 0 && point.y > 0)
for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x - 1][point.y - 1] >= 0) {
if (z > topIndex) {
topIndex = z;
res = &table[z][point.x - 1][point.y - 1];
realPos.x = point.x - 1;
realPos.y = point.y - 1;
}
break;
}
// обновляем переданную позицию так, чтобы она указывала на левый верхний угол карты
point.x = realPos.x;
point.y = realPos.y;
point.z = topIndex;
return res;
}
bool Controller::available(const ThreePoint& point) const {
return upFree(point) && sideFree(point);
}
bool Controller::upFree(const ThreePoint& point) const {
if (point.z == table.size() - 1) // если находимся на самом верхнем уровне по оси z
return true;
return !((table[point.z + 1][point.x][point.y] >= 0) ||
(point.x > 0 && table[point.z + 1][point.x - 1][point.y] >= 0) ||
(point.y > 0 && table[point.z + 1][point.x][point.y - 1] >= 0) ||
(point.x > 0 && point.y > 0 &&
table[point.z + 1][point.x - 1][point.y - 1] >= 0) ||
(point.x < table[point.z].size() - 1 &&
table[point.z + 1][point.x + 1][point.y] >= 0) ||
(point.y < table[point.z][point.x].size() - 1 &&
table[point.z + 1][point.x][point.y + 1] >= 0) ||
(point.x < table[point.z].size() - 1 &&
point.y < table[point.z][point.x].size() - 1 &&
table[point.z + 1][point.x + 1][point.y + 1] >= 0) ||
(point.x > 0 && point.y < table[point.z][point.x].size() - 1 &&
table[point.z + 1][point.x - 1][point.y + 1] >= 0) ||
(point.x < table[point.z].size() - 1 && point.y > 0 &&
table[point.z + 1][point.x + 1][point.y - 1] >= 0));
}
bool Controller::sideFree(const ThreePoint& point) const {
bool lfree = true;
bool rfree = true;
if (point.x > 1)
lfree =
!((point.y > 0 && table[point.z][point.x - 2][point.y - 1] >= 0) ||
(table[point.z][point.x - 2][point.y] >= 0) ||
(point.y < table[point.z][point.x].size() - 1 &&
table[point.z][point.x - 2][point.y + 1] >= 0));
if (point.x < table[point.z].size() - 2)
rfree =
!((point.y > 0 && table[point.z][point.x + 2][point.y - 1] >= 0) ||
(table[point.z][point.x + 2][point.y] >= 0) ||
(point.y < table[point.z][point.x].size() - 1 &&
table[point.z][point.x + 2][point.y + 1] >= 0));
return lfree || rfree;
}
void Controller::handleClick(const wxPoint& point) {
ThreePoint pos(drawer.toGrid(point)); // переводим позицию в координатах окна в координаты сетки
if (pos.x > -1) { // если попали по полю
CardT* card = getCardByPosition(pos); // получаем карту, в которую попали и смещаем позицию в её левый верхний угол
if (pos.z >= 0 && available(pos)) { // если действительно получили карту и она доступна для убирания
if (selected != nullptr && sameValues(*card, *selected) && // если уже есть выбранная карта и она такая же, как эта по значению,
selected != card) { // но при этом не является тем же указателем
steps.push({CardEntry{drawer.marked, *selected}, // сохраняем эту пару в истории
CardEntry{pos, *card}});
*selected = MATCHED; // записываем в доску то, что эти карты убраны
*card = MATCHED;
selected = nullptr; // сбрасываем убранную карту
remaining -= 2; // уменьшаем счётчик оставшихся для убирания карт на 1
drawer.marked = {-1, -1, -1}; // сбрасываем координаты выбранной карты для "художника"
} else {
selected = card; // устанавливаем указатель на выбранную сейчас карту
drawer.marked = pos; // устанавливаем координаты выбранной карты для "художника"
}
}
}
}
bool Controller::sameValues(CardT a, CardT b) const {
if (a == b) // если id карт равны
return true;
else if (a >= 38 && b >= 38) // или они входят в одну
return true;
else if (a >= 34 && a <= 37 && b >= 34 && b <= 37) // из групп, где каждой карты по одной,но при этом все они считаются одинаковыми
return true;
return false;
}
void Controller::undo() {
if (steps.size()) { // если есть шаги для отмены
for (const CardEntry& entry : steps.top()) // в цикле по каждому из пары камней
table[entry.pos.z][entry.pos.x][entry.pos.y] = entry.id; // возвращаем его на доску
remaining += 2; // наращиваем счётчик оставшихся для уборки камней
steps.pop(); // удаляем только что восстановленные камни из истории
}
}
bool Controller::gameStarted() const {
return stopwatch > 0; // если счётчик таймера наращивается, игра началась
}