Did some refactoring in Drawer and Controller, added comments

This commit is contained in:
Dmitriy Shishkov 2022-06-09 21:13:24 +03:00
parent 1a5b905464
commit 258eaf9e3b
No known key found for this signature in database
GPG Key ID: 26720CB2A9608C97
5 changed files with 252 additions and 233 deletions

View File

@ -35,12 +35,13 @@ void Controller::fill(bool solveable) {
fillRandom(); fillRandom();
} }
void Controller::fillSolveableTable() { void Controller::fillSolveableTable() {
srand(time(NULL)); // инициализируем генератор случайных чисел srand(time(NULL)); // инициализируем генератор случайных чисел
auto not_end = remaining; // сохраняем в отдельную переменную количество оставшихся камней auto not_end = remaining; // сохраняем в отдельную переменную количество оставшихся камней
std::set<ThreePoint> positions; // инициализируем сет для хранения доступных для вставки позиций PosSet positions; // инициализируем сет для хранения доступных для вставки позиций
positions.insert(getRandLowest()); // вставляем случайную начальную позицию positions.insert(getRandLowest()); // вставляем случайную начальную позицию
@ -66,7 +67,7 @@ void Controller::fillSolveableTable() {
} }
} }
wxPoint Controller::getRandLowest() { wxPoint Controller::getRandLowest() const {
int overall = gridSize.x * gridSize.y; // вычисляем количество позиций в горизонтальном "срезе" массива int overall = gridSize.x * gridSize.y; // вычисляем количество позиций в горизонтальном "срезе" массива
int x, y; // объявляем координаты для возвращаемой позиции int x, y; // объявляем координаты для возвращаемой позиции
@ -79,7 +80,7 @@ wxPoint Controller::getRandLowest() {
return wxPoint(x, y); // возвращаем wxPoint return wxPoint(x, y); // возвращаем wxPoint
} }
void Controller::emplace_table(CardT id, const ThreePoint& pos, std::set<ThreePoint>& positions) { void Controller::emplace_table(CardT id, const ThreePoint& pos, PosSet& positions) {
table[pos.z][pos.x][pos.y] = id; table[pos.z][pos.x][pos.y] = id;
push_available(positions, pos); // записываем в сет новые позиции push_available(positions, pos); // записываем в сет новые позиции
@ -89,7 +90,7 @@ void Controller::emplace_table(CardT id, const ThreePoint& pos, std::set<ThreePo
#include <wx/file.h> #include <wx/file.h>
void print_list(const std::set<ThreePoint>& positions) { void print_list(const PosSet& positions) {
wxFile f("tmp.txt", wxFile::write_append); // Открываем файл для записи wxFile f("tmp.txt", wxFile::write_append); // Открываем файл для записи
for (const auto& el : positions) // Итерируемся по всем позициям for (const auto& el : positions) // Итерируемся по всем позициям
@ -100,8 +101,8 @@ void print_list(const std::set<ThreePoint>& positions) {
#endif #endif
void Controller::next_rand(std::set<ThreePoint>& positions, void Controller::next_rand(PosSet& positions,
std::set<ThreePoint>::iterator& ptr, PosSet::iterator& ptr,
bool canOverlap, uint8_t& not_end) { bool canOverlap, uint8_t& not_end) {
#ifdef WXDEBUG // Если компилируем в режиме дебага #ifdef WXDEBUG // Если компилируем в режиме дебага
print_list(positions); // выводим список позиций print_list(positions); // выводим список позиций
@ -111,42 +112,38 @@ void Controller::next_rand(std::set<ThreePoint>& positions,
positions.erase(ptr); // удаляем только что вставленный итератор positions.erase(ptr); // удаляем только что вставленный итератор
if (not_end) { if (not_end) { // если ещё есть камни для вставки
if (positions.empty()) if (positions.empty()) // если не осталось позиций,
ptr = positions.insert(getRandLowest()).first; ptr = positions.insert(getRandLowest()).first; // вставляем любую позицию из нижней плоскости и используем её в следующий раз
else { else { // иначе
ptr = positions.begin(); ptr = positions.begin(); // устанавливаем итератор в первую позицию
int rand_d = rand() % positions.size(); int rand_d = rand() % positions.size(); // получаем случайное смещение внутри набора возможных позиций
for (int i = 0; i < rand_d; i++) std::advance(ptr, rand_d); // смещаем итератор
ptr++;
auto rand_ptr = ptr; const auto rand_ptr = ptr; // сохраняем предыдущее положение итератора
while (!canOverlap && ptr != positions.end() && wouldOverlap(prev, *ptr)) // Пока не найдём тот, что не будет закрывать только что вставленную позицию, если не canBeUp while (!canOverlap && ptr != positions.end() && wouldOverlap(prev, *ptr)) // Пока не найдём тот, что не будет закрывать только что вставленную позицию, если не canBeUp или дошли до конца набора
ptr++; ptr++; // наращиваем итератор
if (ptr == positions.end()) if (ptr == positions.end()) { // если ни одна из позиций начиная с rand_ptr не подошла (нельзя выбирать накрывающую предыдущий камень и все позиции накрывают)
ptr = positions.begin(); ptr = positions.begin(); // начинаем с начала
while (!canOverlap && ptr != rand_ptr && wouldOverlap(prev, *ptr)) while (!canOverlap && ptr != rand_ptr && wouldOverlap(prev, *ptr)) // Пока не найдём тот, что не будет закрывать только что вставленную позицию, если не canBeUp и не дошли до rand_ptr
ptr++; ptr++; // наращиваем итератор
}
if (ptr == rand_ptr && wouldOverlap(prev, *ptr)) { if (ptr == rand_ptr && !canOverlap && wouldOverlap(prev, *ptr)) { // если итератор совпадает с rand_ptr и при этом ptr перекрывает prev,
auto _ = wouldOverlap(prev, *ptr); if (not_end == positions.size()) // если уже все позиции добавлены в набор
if (not_end == positions.size()) ptr = positions.begin(); // просто выбираем первую из них
ptr = positions.begin(); else { // иначе
else { auto res = positions.insert(getRandLowest()); // пытаемся вставить вставляем случайную позицию в нижней плоскости
auto res = positions.insert(getRandLowest());
while (!res.second) while (!res.second) // пока не произошла вставка позиции в набор
res = positions.insert(getRandLowest()); res = positions.insert(getRandLowest()); // пытаемся вставить случайную позицию в нижней плоскости
if (!res.second) ptr = res.first; // получаем итератор на только что вставленную позицию
throw std::runtime_error("Wrong map layout or internal error");
ptr = res.first;
} }
} }
} }
@ -166,8 +163,8 @@ bool Controller::wouldOverlap(const ThreePoint& prev, const ThreePoint& next) {
/** /**
* Checks if position `p`, shifted by `d`, is not out of bounds of array `table` with dimensions `gridSize` * 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) { bool Controller::corrInd(const ThreePoint& p, const ThreePoint& d) const {
auto& gS = gridSize; // более короткий алиса для переменной auto& gS = gridSize; // более короткий алиас для переменной
return ((d.z == 0) || (d.z < 0 && p.z >= -d.z) || (d.z > 0 && p.z + d.z < gS.z)) && 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.x == 0) || (d.x < 0 && p.x >= -d.x) || (d.x > 0 && p.x + d.x < gS.x)) &&
@ -177,27 +174,29 @@ bool Controller::corrInd(const ThreePoint& p, const ThreePoint& d) {
/** /**
* Checks if position `p`, shifted by `d`, is not out of bounds and available for insert (FREE) * 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) { 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); 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) * 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) { 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); 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 * Pushes all positions, that are close to `pos`, available for insert and don't overlap other available positions
*/ */
void Controller::push_available(std::set<ThreePoint>& positions, void Controller::push_available(PosSet& positions,
const ThreePoint& pos) { const ThreePoint& pos) const {
auto& p = pos; auto& p = pos; // короткий алиас для переменной
int z = pos.z, x = pos.x, y = pos.y; int z = pos.z, x = pos.x, y = pos.y; // "разбираем" объект 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})) // дальше идёт миллион условий, которые проще просто прочитать, нежели описывать, что они проверяют. В комментариях возле условий указано, в какую сторону относительно 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); 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 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); positions.emplace(z, x+2, y);
@ -271,60 +270,60 @@ void Controller::free_table() {
} }
} }
steps = decltype(steps)(); steps = decltype(steps)(); // сбрасываем стек steps (decltype удобен, ибо тогда не зависим от класса, просто вызываем стандартный конструктор)
} }
void Controller::fillRandom() { void Controller::fillRandom() {
srand(time(NULL)); srand(time(NULL)); // инициализируем генератор случайных чисел
wxLogDebug(itowxS(remaining)); wxLogDebug(itowxS(remaining));
auto not_end = remaining; auto not_end = remaining; // сохраняем количество оставшихся для вставки камней
// итерируемся по всему массиву поля, пока не вставим все камни
for (int z = 0; z < gridSize.z && not_end; z++) for (int z = 0; z < gridSize.z && not_end; z++)
for (int x = 0; x < gridSize.x && not_end; x++) for (int x = 0; x < gridSize.x && not_end; x++)
for (int y = 0; y < gridSize.y && not_end; y++) for (int y = 0; y < gridSize.y && not_end; y++)
if (table[z][x][y] == FREE) { if (table[z][x][y] == FREE) { // если в эту позицию можно вставить камень
table[z][x][y] = genRandId(); table[z][x][y] = genRandId(); // получаем случайный id и вставляем его туда
not_end--; not_end--; // уменьшаем счётчик оставшихся камней
} }
} }
CardT Controller::genRandId() { CardT Controller::genRandId() {
CardT id; CardT id;
int w = 0;
do { do {
id = rand() % TILE_IMAGES_N; id = rand() % TILE_IMAGES_N; // получаем случайное число-индекс в массиве имён камней
w++; } while (cardsCounter[id] == 0); // повторяем тело цикла, если эти id уже закончились
} while (cardsCounter[id] == 0);
cardsCounter[id]--; cardsCounter[id]--; // уменьшаем счётчик оставшихся для вставки камней этого типа
return id; return id; // возвращаем полученный id
} }
CardT Controller::getFreeSingularId(CardT prev) { CardT Controller::getFreeSingularId(CardT prev) {
CardT id = (prev < 38) ? 34 : 38; CardT id = (prev < 38) ? 34 : 38; // устанавливаем первый из id, которые считаются одинаковыми с prev
while (id < TILE_IMAGES_N && cardsCounter[id] == 0) while (id < TILE_IMAGES_N && cardsCounter[id] == 0) // ищем в массиве оставшихся камней свободный id (если начинаем с 34, так как id выбираются парами, обязательно останется хотя бы один id, пренадлежащий этой группе (до 48), поэтому границу можно оставить одинаковой)
id++; id++;
cardsCounter[id]--; cardsCounter[id]--; // уменьшаем счётчик оставшихся id
return id; return id; // возвращаем его
} }
/** /**
* Gets pointer to top card by grid position
* It also changes point to top right coordinate of card * It also changes point to top right coordinate of card
*/ */
CardT* Controller::getCardByPosition(ThreePoint& point) { CardT* Controller::getCardByPosition(ThreePoint& point) {
int8_t topIndex = -1; int8_t topIndex = -1; // начинаем с -1, чтобы если не нашёлся ни один камень, получить невалидную позицию
CardT* res = nullptr; CardT* res = nullptr; // указатель на элемент массива
ThreePoint realPos = point; ThreePoint realPos(point); // сохраняем копию позиции, чтобы при смещении не ломать позицию, для которой ищем
// ищем верхнюю карту с верхним левым углом в данной позиции (нажатие в левую верхнюю четверть камня)
for (int z = table.size() - 1; z >= 0; z--) for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x][point.y] >= 0) { if (table[z][point.x][point.y] >= 0) {
if (z > topIndex) { if (z > topIndex) {
@ -334,6 +333,7 @@ CardT* Controller::getCardByPosition(ThreePoint& point) {
break; break;
} }
// ищем верхнюю карту с верхним левым углом в данной позиции, смещённой на единицу влево (нажатие в правую верхнюю четверть камня)
if (point.x > 0) if (point.x > 0)
for (int z = table.size() - 1; z >= 0; z--) for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x - 1][point.y] >= 0) { if (table[z][point.x - 1][point.y] >= 0) {
@ -347,6 +347,7 @@ CardT* Controller::getCardByPosition(ThreePoint& point) {
break; break;
} }
// ищем верхнюю карту с верхним левым углом в данной позиции, смещённой на единицу вверх (нажатие в левую нижнюю четверть камня)
if (point.y > 0) if (point.y > 0)
for (int z = table.size() - 1; z >= 0; z--) for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x][point.y - 1] >= 0) { if (table[z][point.x][point.y - 1] >= 0) {
@ -360,6 +361,7 @@ CardT* Controller::getCardByPosition(ThreePoint& point) {
break; break;
} }
// ищем верхнюю карту с верхним левым углом в данной позиции, одновременно смещённой вверх и влево (нажатие в правую нижнюю четверть камня)
if (point.x > 0 && point.y > 0) if (point.x > 0 && point.y > 0)
for (int z = table.size() - 1; z >= 0; z--) for (int z = table.size() - 1; z >= 0; z--)
if (table[z][point.x - 1][point.y - 1] >= 0) { if (table[z][point.x - 1][point.y - 1] >= 0) {
@ -373,6 +375,7 @@ CardT* Controller::getCardByPosition(ThreePoint& point) {
break; break;
} }
// обновляем переданную позицию так, чтобы она указывала на левый верхний угол карты
point.x = realPos.x; point.x = realPos.x;
point.y = realPos.y; point.y = realPos.y;
point.z = topIndex; point.z = topIndex;
@ -386,7 +389,7 @@ bool Controller::available(const ThreePoint& point) const {
bool Controller::upFree(const ThreePoint& point) const { bool Controller::upFree(const ThreePoint& point) const {
if (point.z == table.size() - 1) if (point.z == table.size() - 1) // если находимся на самом верхнем уровне по оси z
return true; return true;
return !((table[point.z + 1][point.x][point.y] >= 0) || return !((table[point.z + 1][point.x][point.y] >= 0) ||
@ -429,58 +432,54 @@ bool Controller::sideFree(const ThreePoint& point) const {
} }
void Controller::handleClick(const wxPoint& point) { void Controller::handleClick(const wxPoint& point) {
wxPoint posPlain = drawer.toGrid(point); ThreePoint pos(drawer.toGrid(point)); // переводим позицию в координатах окна в координаты сетки
ThreePoint pos = {-1, posPlain.x, posPlain.y}; if (pos.x > -1) { // если попали по полю
CardT* card = getCardByPosition(pos); // получаем карту, в которую попали и смещаем позицию в её левый верхний угол
if (pos.x > -1) { if (pos.z >= 0 && available(pos)) { // если действительно получили карту и она доступна для убирания
CardT* card = getCardByPosition(pos); if (selected != nullptr && sameValues(*card, *selected) && // если уже есть выбранная карта и она такая же, как эта по значению,
selected != card) { // но при этом не является тем же указателем
if (pos.z >= 0 && available(pos)) { steps.push({CardEntry{drawer.marked, *selected}, // сохраняем эту пару в истории
if (selected != nullptr && sameValues(*card, *selected) &&
selected != card) {
steps.push({CardEntry{drawer.marked, *selected},
CardEntry{pos, *card}}); CardEntry{pos, *card}});
*selected = MATCHED; *selected = MATCHED; // записываем в доску то, что эти карты убраны
*card = MATCHED; *card = MATCHED;
selected = nullptr; selected = nullptr; // сбрасываем убранную карту
remaining -= 2; remaining -= 2; // уменьшаем счётчик оставшихся для убирания карт на 1
drawer.marked = {-1, -1, -1}; drawer.marked = {-1, -1, -1}; // сбрасываем координаты выбранной карты для "художника"
} else { } else {
selected = card; selected = card; // устанавливаем указатель на выбранную сейчас карту
drawer.marked = pos; drawer.marked = pos; // устанавливаем координаты выбранной карты для "художника"
} }
} }
} }
} }
bool Controller::sameValues(CardT a, CardT b) const { bool Controller::sameValues(CardT a, CardT b) const {
if (a == b) if (a == b) // если id карт равны
return true; return true;
else if (a >= 38 && b >= 38) else if (a >= 38 && b >= 38) // или они входят в одну
return true; return true;
else if (a >= 34 && a <= 37 && b >= 34 && b <= 37) else if (a >= 34 && a <= 37 && b >= 34 && b <= 37) // из групп, где каждой карты по одной,но при этом все они считаются одинаковыми
return true; return true;
return false; return false;
} }
void Controller::undo() { void Controller::undo() {
if (steps.size()) { if (steps.size()) { // если есть шаги для отмены
for (const CardEntry& entry : steps.top()) { for (const CardEntry& entry : steps.top()) // в цикле по каждому из пары камней
table[entry.pos.z][entry.pos.x][entry.pos.y] = entry.id; table[entry.pos.z][entry.pos.x][entry.pos.y] = entry.id; // возвращаем его на доску
cardsCounter[entry.id]++;
}
remaining += 2; remaining += 2; // наращиваем счётчик оставшихся для уборки камней
steps.pop(); steps.pop(); // удаляем только что восстановленные камни из истории
} }
} }
bool Controller::gameStarted() const { bool Controller::gameStarted() const {
return stopwatch > 0; return stopwatch > 0; // если счётчик таймера наращивается, игра началась
} }

View File

@ -2,7 +2,6 @@
#define CONTROLLER_H #define CONTROLLER_H
#include <array> #include <array>
#include <set>
#include <stack> #include <stack>
#include "wxw.h" #include "wxw.h"
@ -44,19 +43,19 @@ private:
void fillSolveableTable(); void fillSolveableTable();
wxPoint getRandLowest(); wxPoint getRandLowest() const;
void emplace_table(CardT id, const ThreePoint& pos, std::set<ThreePoint>& positions); void emplace_table(CardT id, const ThreePoint& pos, PosSet& positions);
void next_rand(std::set<ThreePoint>& positions, void next_rand(PosSet& positions,
std::set<ThreePoint>::iterator& ptr, bool canOverlap, uint8_t& not_end); PosSet::iterator& ptr, bool canOverlap, uint8_t& not_end);
bool wouldOverlap(const ThreePoint& prev, const ThreePoint& next); bool wouldOverlap(const ThreePoint& prev, const ThreePoint& next);
bool corrInd(const ThreePoint& p, const ThreePoint& d); bool corrInd(const ThreePoint& p, const ThreePoint& d) const;
bool Free(const ThreePoint& p, const ThreePoint& d); bool Free(const ThreePoint& p, const ThreePoint& d) const;
bool NFree(const ThreePoint& p, const ThreePoint& d); bool NFree(const ThreePoint& p, const ThreePoint& d) const;
void push_available(std::set<ThreePoint>& positions, const ThreePoint& pos); void push_available(PosSet& positions, const ThreePoint& pos) const;
void fillRandom(); void fillRandom();

View File

@ -1,5 +1,8 @@
#include "Drawer.h" #include "Drawer.h"
#include <wx/filename.h>
#include <wx/stdpaths.h>
static const char* tileImageNames[TILE_IMAGES_N] = { static const char* tileImageNames[TILE_IMAGES_N] = {
// clang-format off // clang-format off
"Pin1", "Pin2", "Pin3", "Pin4", "Pin5", "Pin6", "Pin7", "Pin8", "Pin9", "Pin1", "Pin2", "Pin3", "Pin4", "Pin5", "Pin6", "Pin7", "Pin8", "Pin9",
@ -12,112 +15,117 @@ static const char* tileImageNames[TILE_IMAGES_N] = {
// clang-format on // clang-format on
}; };
Drawer::Drawer() : marked{-1, -1, -1} {
wxString path = wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + _("tiles") + wxFileName::GetPathSeparator();
for (int i = 0; i < TILE_IMAGES_N; i++) { // В цикле по именам изображений
bool succeed = tileImages[i].LoadFile( // загружаем их
path + _(tileImageNames[i]) + _(".png"), // из стандартной папки для ресурсов приложения
wxBITMAP_TYPE_PNG); // в формате png
if (!succeed) // В случае ошибки заргрузки выводим сообщение об этом
wxLogDebug(_("failed to load tile ./resources/tiles/") + // с путём,
_(tileImageNames[i]) + _(".png with index") + // именем файла
wxString::Format("%i", i)); // и индексом в массиве
}
}
void Drawer::drawTable(wxDC& dc) const {
dc.DrawBitmap(bgBitmap, 0, 0, false); // отрисовываем в dc битмап с фоном, начиная из левого верхнего угла без маски
if (boardBitmap.IsOk()) { // Если изображение доски построено
wxLogDebug("Drawing board");
dc.DrawBitmap(boardBitmap, boardPixelRect.GetPosition(), true); // отрисовываем битмап с ней в установленном при рейсайзе положении, используя маску
}
}
static const wxColor lGreen{0x07, 0x55, 0x2b}; static const wxColor lGreen{0x07, 0x55, 0x2b};
static const wxColor dGreen{0x01, 0x2d, 0x16}; static const wxColor dGreen{0x01, 0x2d, 0x16};
Drawer::Drawer() : marked{-1, -1, -1} {
for (int i = 0; i < TILE_IMAGES_N; i++) {
bool succeed = tileImages[i].LoadFile(
_("./resources/tiles/") + _(tileImageNames[i]) + _(".png"),
wxBITMAP_TYPE_PNG);
if (!succeed)
wxLogDebug(_("failed to load tile ./resources/tiles/") +
_(tileImageNames[i]) + _(".png with index") +
wxString::Format("%i", i));
}
}
void Drawer::drawTable(wxDC& dc) {
dc.DrawBitmap(bgBitmap, 0, 0, false);
if (boardBitmap.IsOk()) {
wxLogDebug("Drawing board");
dc.DrawBitmap(boardBitmap, tablePixelRect.GetPosition(), true);
}
}
void Drawer::composeBG() { void Drawer::composeBG() {
bgBitmap = wxBitmap(resolution); bgBitmap = wxBitmap(resolution); // создаём битмап размером со всю панейль
wxLogDebug( wxLogDebug(
wxString::Format("Rebuild bg %i %i", resolution.x, resolution.y)); wxString::Format("Rebuild bg %i %i", resolution.x, resolution.y));
wxMemoryDC dc; wxMemoryDC dc; // создаём dc в памяти
dc.SelectObject(bgBitmap); dc.SelectObject(bgBitmap); // выбираем свежесозданный битмап как холст для рисования
dc.GradientFillConcentric(wxRect(wxPoint(0, 0), resolution), lGreen, dc.GradientFillConcentric(wxRect(wxPoint(0, 0), resolution), lGreen, // рисуем радиальный градиент на весь битмап, переходящий от светлозелёного в центре
dGreen); dGreen); // к тёмнозелёному по краям
} }
void Drawer::composeBoard(const TLVec& layout, const Dimensions& gridSize) { void Drawer::composeBoard(const TLVec& table, const Dimensions& gridSize) {
boardBitmap = wxBitmap(tablePixelRect.GetSize()); boardBitmap = wxBitmap(boardPixelRect.GetSize()); // Создаём битмап согласно размерам, полученным при последнем ресайзе окна
wxLogDebug(_("Rebuild board")); wxLogDebug(_("Rebuild board"));
wxMemoryDC dc; wxMemoryDC dc; // создаём dc в памяти
dc.SelectObject(boardBitmap); dc.SelectObject(boardBitmap); // выбираем свежесозданный битмап как холст для рисования
// итерируемся по всем индексам массива table
for (int z = 0; z < gridSize.z; z++) for (int z = 0; z < gridSize.z; z++)
for (int x = 0; x < gridSize.x; x++) for (int x = 0; x < gridSize.x; x++)
for (int y = 0; y < gridSize.y; y++) { for (int y = 0; y < gridSize.y; y++) {
CardT c = layout[z][x][y]; CardT c = table[z][x][y]; // получаем id в данной позиции
if (c >= 0) if (c >= 0) // если тут стоит камень
drawTile(dc, c, fromGrid(x, y), z); drawTile(dc, c, fromGrid(x, y), z); // отрисовываем его
} }
dc.SelectObject(wxNullBitmap); dc.SelectObject(wxNullBitmap); // отцепляем dc (обычно это происходит при выходе объекта за поле видимости, но здесь это нужно сделать вручную, чтобы создать маску)
wxMask* mask = new wxMask(boardBitmap, wxColor(0x00, 0x00, 0x00)); wxMask* mask = new wxMask(boardBitmap, wxColor(0x00, 0x00, 0x00)); // создаём маску по полю, чтобы при отрисовке там, где нет камней не было чёрного фона
boardBitmap.SetMask(mask); boardBitmap.SetMask(mask); // устанавливаем маску для битмапа
} }
void Drawer::drawTile(wxDC& dc, int8_t index, const wxPoint& position, void Drawer::drawTile(wxDC& dc, int8_t index, const wxPoint& position,
uint8_t zIndex) const { uint8_t zIndex) const {
wxBrush _bgColor = dc.GetBrush(); wxBrush _bgColor = dc.GetBrush(); // сохраняем цвет кисти, которая была в dc по умолчанию
wxBrush front = wxColor(0xff, 0xff, 0xff); wxBrush front = wxColor(0xff, 0xff, 0xff); // создаём кисти для лицевой части камня
wxBrush back = wxColor(0xc8, 0xc8, 0xc8); wxBrush back = wxColor(0xc8, 0xc8, 0xc8); // и подложки
if (position == fromGrid({marked.x, marked.y}) && marked.z == zIndex) { // wxLogDebug(wxString::Format("%i %i %i - %i %i %i (%i %i %i)", zIndex, position.x, position.y, marked.z, fromGrid(marked).x, fromGrid(marked).y, marked.z, marked.x, marked.y));
front = wxColor(0xc8, 0xff, 0xc8);
if (position == fromGrid(marked) && marked.z == zIndex) { // если данный камень выбран,
front = wxColor(0xc8, 0xff, 0xc8); // заменяем цвета на аналогичные с зелёным оттенком
back = wxColor(0xbe, 0xdc, 0xbe); back = wxColor(0xbe, 0xdc, 0xbe);
} }
dc.SetBrush(back); dc.SetBrush(back); // устанавливаем кисть для рисования подложки камня
dc.DrawRoundedRectangle(position.x + (tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE) - dc.DrawRoundedRectangle( // рисуем подложку,
(tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE) * zIndex, position.x - tilePadding.x * (zIndex - 1), // смещённую относительно плоской позиции на оду единицу смещения вправо и на zIndex единиц влево,
position.y + (tilePixelSize.GetHeight() / TILE_WIDTH * TILE_PADDING_SCALE) - position.y - tilePadding.y * (zIndex - 1), // на единицу смещения вниз и на zIndex единиц вверх
(tilePixelSize.GetHeight() / TILE_WIDTH * TILE_PADDING_SCALE) * zIndex, tilePixelSize.x * 2, tilePixelSize.y * 2, // размер каждой из сторон в два раза больше, чем размеры на сетке
tilePixelSize.GetWidth() * 2, tilePadding.y // радиус закругления равен одной единице смещения
tilePixelSize.GetHeight() * 2, (tilePixelSize.GetHeight() / TILE_HEIGHT * TILE_PADDING_SCALE)); );
dc.SetBrush(front); dc.SetBrush(front);
dc.DrawRoundedRectangle( dc.DrawRoundedRectangle( // рисуем лицевую часть,
position.x - (tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE) * zIndex, position.x - tilePadding.x * zIndex, // смещённую относительно плоской позиции на zIndex единиц смещения влево
position.y - (tilePixelSize.GetHeight() / TILE_WIDTH * TILE_PADDING_SCALE) * zIndex, position.y - tilePadding.y * zIndex, // и на zIndex единиц вверх
tilePixelSize.GetWidth() * 2, tilePixelSize.GetHeight() * 2, (tilePixelSize.GetHeight() / TILE_HEIGHT * TILE_PADDING_SCALE)); tilePixelSize.x * 2, tilePixelSize.y * 2, // размер каждой из сторон в два раза больше, чем размеры на сетке
tilePadding.y // радиус закругления равен одной единице смещения
);
dc.SetBrush(_bgColor); dc.SetBrush(_bgColor); // возвращаем изначальный цвет кисти dc
if (tileImages[index].IsOk()) { if (tileImages[index].IsOk()) { // если при загрузке картинки не возникло проблем
wxPoint pos; wxPoint pos; // верхний левый угол картинки (так же как у подложки) смещён относительно лицевой части на одну единицу смещения
pos.x = position.x + (tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE) - (tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE) * zIndex; pos.x = position.x - tilePadding.x * (zIndex - 1);
pos.y = position.y + (tilePixelSize.GetHeight() / TILE_HEIGHT * TILE_PADDING_SCALE) - (tilePixelSize.GetHeight() / TILE_WIDTH * TILE_PADDING_SCALE) * zIndex; pos.y = position.y - tilePadding.y * (zIndex - 1);
if (tileImages[index].GetWidth() != tilePixelSize.x * 2 - tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE * 2) dc.DrawBitmap( // отрисовываем картинку, масштабируя её под размер камня - две единицы смещения для отступов
dc.DrawBitmap(tileImages[index].Scale(tilePixelSize.x * 2 - tilePixelSize.GetWidth() / TILE_WIDTH * TILE_PADDING_SCALE * 2, tileImages[index].Scale(tilePixelSize.x * 2 - tilePadding.x * 2,
tilePixelSize.y * 2 - tilePixelSize.GetHeight() / TILE_HEIGHT * TILE_PADDING_SCALE * 2), tilePixelSize.y * 2 - tilePadding.y * 2),
pos); pos);
else
dc.DrawBitmap(tileImages[index], pos);
} }
} }
void Drawer::resizeBg(const wxSize& resolution) { void Drawer::resizeBg(const wxSize& resolution) {
if (this->resolution != resolution) { if (this->resolution != resolution) { // если размер окна действительно изменился
this->resolution = resolution; this->resolution = resolution; // устанавливаем новое разрешение и
composeBG(); composeBG(); // перерисовываем фон
} }
} }
@ -125,78 +133,87 @@ void Drawer::resizeBg(const wxSize& resolution) {
* Resizes tile and whole board bitmap size to the resolution, set in this * Resizes tile and whole board bitmap size to the resolution, set in this
* instance * instance
*/ */
bool Drawer::resizeBoard(const TLVec& layout, const Dimensions& gridSize, bool force) { bool Drawer::resizeBoard(const TLVec& table, const Dimensions& gridSize, bool force) {
bool res = false; bool res = false; // произошла ли полная перерисовка поля, или только был о рассчитано новое положение битмапа
const int gridPoint = mmin( const int gridPoint = mmin( // минимум из двух осей (по x и по y), подробнее о формулах в (4)
resolution.x / (gridSize.x * TILE_WIDTH + gridSize.z * TILE_PADDING_SCALE), resolution.x / (gridSize.x * TILE_WIDTH + gridSize.z * TILE_PADDING_SCALE),
resolution.y * TILE_WIDTH / resolution.y * TILE_WIDTH /
(gridSize.y * TILE_HEIGHT * TILE_WIDTH + TILE_HEIGHT * gridSize.z * TILE_PADDING_SCALE)); (gridSize.y * TILE_HEIGHT * TILE_WIDTH + TILE_HEIGHT * gridSize.z * TILE_PADDING_SCALE));
wxLogDebug(wxString::Format("Resize board: %i", gridPoint)); wxLogDebug(wxString::Format("Resize board: %i", gridPoint));
if (gridPoint != prevGridPoint || force) { if (gridPoint != prevGridPoint || force) { // если gridPoint изменился, или перерасчёт принудительный
tilePixelSize.Set(gridPoint * TILE_WIDTH, gridPoint * TILE_HEIGHT); tilePixelSize.Set(gridPoint * TILE_WIDTH, gridPoint * TILE_HEIGHT); // устанавливаем новый размер половины стороны камня (по сетке)
boardPadding.x = (tilePixelSize.x / TILE_WIDTH * TILE_PADDING_SCALE) * (gridSize.z - 1); // Смещение, создаваемое самыми левыми картами на верхних позициях (их может и не быть, но проверять это дорого) tilePadding.x = tilePixelSize.x / TILE_WIDTH * TILE_PADDING_SCALE; // Смещение, даваемое подложками карт вдоль оси x
boardPadding.y = (tilePixelSize.y / TILE_WIDTH * TILE_PADDING_SCALE) * (gridSize.z - 1); // Смещение, создаваемое самыми верхними (в плоскости xy) картами на верхних позициях (их может и не быть, но проверять это дорого) tilePadding.y = tilePixelSize.y / TILE_WIDTH * TILE_PADDING_SCALE; // Смещение, даваемое подложками карт вдоль оси y
tablePixelRect.SetSize( boardPadding.x = tilePadding.x * (gridSize.z - 1); // Смещение, создаваемое самыми левыми картами на верхних позициях (их может и не быть, но проверять это дорого)
wxSize((tilePixelSize.x * gridSize.x) + // Размер только плоских карт boardPadding.y = tilePadding.y * (gridSize.z - 1); // Смещение, создаваемое самыми верхними (в плоскости xy) картами на верхних позициях (их может и не быть, но проверять это дорого)
boardPixelRect.SetWidth(
(tilePixelSize.x * gridSize.x) + // Размер только плоских карт
boardPadding.x + // см. выше boardPadding.x + // см. выше
(tilePixelSize.x / TILE_WIDTH * TILE_PADDING_SCALE), // Смещение, даваемое подложками самых правых tilePadding.x // Смещение, даваемое подложками самых правых
);
boardPixelRect.SetHeight(
(tilePixelSize.y * gridSize.y) + // Размер только плоских карт (tilePixelSize.y * gridSize.y) + // Размер только плоских карт
boardPadding.y + // см. выше boardPadding.y + // см. выше
(tilePixelSize.y / TILE_WIDTH * TILE_PADDING_SCALE) // Смещение, даваемое подложками самых нижних (в плоскости xy) tilePadding.y // Смещение, даваемое подложками самых нижних (в плоскости xy)
)
); );
} }
tablePixelRect.SetPosition({(resolution.x - tablePixelRect.width) / 2, boardPixelRect.SetPosition( // выравниваем по центру окна
(resolution.y - tablePixelRect.height) / 2}); wxPoint((resolution.x - boardPixelRect.width) / 2,
(resolution.y - boardPixelRect.height) / 2)
);
if (gridPoint != prevGridPoint || force) { if (gridPoint != prevGridPoint || force) { // если gridPoint изменился, или перерасчёт принудительный
composeBoard(layout, gridSize); composeBoard(table, gridSize); // перерисовываем стол
res = true; res = true; // и сообщаем об этом
} }
prevGridPoint = gridPoint; prevGridPoint = gridPoint; // сохраняем новое значение gridPoint для "кеширования" стола
return res; return res;
} }
wxPoint Drawer::toGrid(wxPoint point) const { wxPoint Drawer::toGrid(const wxPoint& point) const {
wxPoint out(-1, -1); wxPoint out(-1, -1); // инициализируем координату не валидными значениями
point.x -= boardPadding.x; if (point.x >= boardPixelRect.x + boardPadding.x && // если попали внутрь стола (за исключением декоративных отступов)
point.y -= boardPadding.y; point.x <= boardPixelRect.x + boardPixelRect.width - tilePadding.x &&
point.y >= boardPixelRect.y + boardPadding.y &&
if (point.x >= tablePixelRect.x && point.y <= boardPixelRect.y + boardPixelRect.height - tilePadding.y) {
point.x <= tablePixelRect.x + tablePixelRect.width && out.x = (point.x - boardPixelRect.x - boardPadding.x) / tilePixelSize.x; // перводим курсор в координаты стола (поэтому вычитаем положение битмапа и декоративные отступы)
point.y >= tablePixelRect.y && out.y = (point.y - boardPixelRect.y - boardPadding.y) / tilePixelSize.y;
point.y <= tablePixelRect.y + tablePixelRect.height) {
out.x = (point.x - tablePixelRect.x) / tilePixelSize.x;
out.y = (point.y - tablePixelRect.y) / tilePixelSize.y;
} }
return out; return out; // возвращаем либо заглушку, либо, если попали, валидные координаты
} }
/**
* Converts from grid position to board bitmap coordinates
*/
wxPoint Drawer::fromGrid(int x, int y) const { wxPoint Drawer::fromGrid(int x, int y) const {
return {x * tilePixelSize.x + boardPadding.x, y * tilePixelSize.y + boardPadding.y}; return {x * tilePixelSize.x + boardPadding.x, y * tilePixelSize.y + boardPadding.y}; // переводим из координат сетки в позицию внутри битмапа
} }
/**
* Converts from grid position to board bitmap coordinates
*/
wxPoint Drawer::fromGrid(const wxPoint& point) const { wxPoint Drawer::fromGrid(const wxPoint& point) const {
return fromGrid(point.x, point.y); return fromGrid(point.x, point.y); // просто проксируем класс wxPoint в функцию с двумя аргументами
} }
wxSize Drawer::composeMinSize(const Dimensions& gridSize) { wxSize Drawer::composeMinSize(const Dimensions& gridSize) const {
wxSize ms; wxSize ms;
ms.SetWidth(TILE_WIDTH * gridSize.x + gridSize.z * TILE_PADDING_SCALE); ms.SetWidth(TILE_WIDTH * gridSize.x + gridSize.z * TILE_PADDING_SCALE); // минимальная ширина экрана при gridPoint = 1
ms.SetHeight(TILE_HEIGHT * gridSize.y + gridSize.z * TILE_PADDING_SCALE * TILE_HEIGHT / TILE_WIDTH); ms.SetHeight(TILE_HEIGHT * gridSize.y + gridSize.z * TILE_PADDING_SCALE * TILE_HEIGHT / TILE_WIDTH); // минимальная высота экрана при gridPoint = 1
ms += {1, 1}; ms += {1, 1}; // добавляем по единице к каждой из сторон, чтобы при округлении не получить число меньше, чем нужно
wxLogDebug(wxString::Format("MinSize %i %i", ms.x, ms.y)); wxLogDebug(wxString::Format("MinSize %i %i", ms.x, ms.y));

View File

@ -16,25 +16,25 @@ class Drawer {
public: public:
Drawer(); Drawer();
void drawTable(wxDC& dc); void drawTable(wxDC& dc) const;
void composeBG(); void composeBG();
void composeBoard(const TLVec& layout, const Dimensions& gridSize); void composeBoard(const TLVec& table, const Dimensions& gridSize);
void resizeBg(const wxSize& tableSize); void resizeBg(const wxSize& tableSize);
bool resizeBoard(const TLVec& layout, const Dimensions& gridSize, bool force); bool resizeBoard(const TLVec& table, const Dimensions& gridSize, bool force);
wxPoint toGrid(wxPoint point) const; wxPoint toGrid(const wxPoint& point) const;
wxPoint fromGrid(int x, int y) const; wxPoint fromGrid(int x, int y) const;
wxPoint fromGrid(const wxPoint& point) const; wxPoint fromGrid(const wxPoint& point) const;
wxSize composeMinSize(const Dimensions& gridSize); wxSize composeMinSize(const Dimensions& gridSize) const;
wxSize tableSize; wxSize tableSize;
wxSize tilePixelSize; // кратно 3x4, по умолчанию 600x800 wxSize tilePixelSize; // кратно 3x4, по умолчанию 600x800
wxSize resolution; wxSize resolution;
wxRect tablePixelRect; wxRect boardPixelRect;
ThreePoint marked; ThreePoint marked;

52
utils.h
View File

@ -3,60 +3,64 @@
#include "wxw.h" #include "wxw.h"
#include <set> #include <unordered_set>
#include <vector> #include <vector>
#include <iterator> #include <iterator>
using std::vector; using std::vector;
wxString LTimeToStr(int time); wxString LTimeToStr(int time);
int upDiv(int a, int b);
wxString itowxS(int a); wxString itowxS(int a);
wxString PRemaining(uint8_t remaining); wxString PRemaining(uint8_t remaining);
#define mmin(a, b) (a + b - abs(a - b)) / 2 #define mmin(a, b) (a + b - abs(a - b)) / 2 // среднее арифметическое минус половина разницы
#define mmax(a, b) (a + b + abs(a - b)) / 2 #define mmax(a, b) (a + b + abs(a - b)) / 2 // среднее арифметическое плюс половина разницы
using CardT = int16_t; using CardT = int16_t;
class Dimensions : public wxSize { struct Dimensions : wxSize { // используется там, где необходимо задать размеры в трёх координатах
public: Dimensions(int _z, int _x, int _y) : z(_z), wxSize(_x, _y){};
Dimensions(int _z, int _x, int _y) : wxSize(_x, _y), z(_z){};
Dimensions() : wxSize(), z(0){}; Dimensions() : wxSize(), z(0){};
int z; int z;
}; };
class ThreePoint { struct ThreePoint : wxPoint { // используется там, где необходимо задать положение в трёх координатах. Так же засчёт наследования от wxPoint, может быть передан в функцию, принимающую wxPoint
public: ThreePoint(int _z, int _x, int _y) : z(_z), wxPoint(_x, _y){};
constexpr ThreePoint(int _z, int _x, int _y) : x(_x), y(_y), z(_z){}; ThreePoint(const wxPoint& a) : z(0), wxPoint(a.x, a.y){};
ThreePoint(const wxPoint& a) : x(a.x), y(a.y), z(0){}; ThreePoint() : z(0), wxPoint(0, 0){};
ThreePoint() : x(0), y(0), z(0){};
bool operator<(const ThreePoint& b) const { bool operator==(const ThreePoint& b) const { // требуется для того, чтобы использовать в std::unordered_set (так как это множетсво)
return z * 144 * 144 + x * 144 + y < b.z * 144 * 144 + b.x * 144 + b.y;
}
bool operator==(const ThreePoint& b) {
return z == b.z && x == b.x && y == b.y; return z == b.z && x == b.x && y == b.y;
} }
int x;
int y;
int z; int z;
}; };
class CardEntry { // доопределяем функтор std::hash для параметра шаблона ThreePoint, чтобы использовать в std::unordered_set
public: namespace std {
template<> struct hash<ThreePoint> {
size_t operator()(const ThreePoint& p) const {
return std::hash<uint32_t>()(p.z * 288 * 288 + p.x * 288 + p.y); // координаты точки можно представить как число в 288-ичной системе счисления, так как в крайнем случае (если индексируется прямая линия из всех камней), максимальная координата будет 144*2-1. хэш для этого числа может иметь максимальное значение 287*288*288+287*288+287, что больше, чем 2^16, но меньше, чем 2^32, поэтому используем uint32_t (на самом деле это преобразуется в простое статическое преобразование типа к size_t, но документация совертует делать так)
}
};
}
using PosSet = std::unordered_set<ThreePoint>; // алиас для типа
struct CardEntry { // используется как элемент стека, хранящего историю ходов
ThreePoint pos; ThreePoint pos;
CardT id; CardT id;
}; };
using TLVec = vector<vector<vector<CardT>>>; using TLVec = vector<vector<vector<CardT>>>;
enum Values { MATCHED = -3, EMPTY, FREE }; enum Values { // перечисление псевдо-id в таблице для не занятых реальными id камней позиций
MATCHED = -3, // уже убран
EMPTY, // не должно быть камня
FREE // доступен для вставки камня
};
bool isPositive(const wxSize& size); bool isPositive(const wxSize& size);
void cyclic_shift(std::set<ThreePoint>::iterator& it, const std::set<ThreePoint>& cont);
#endif #endif