MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
puzzle_game.cpp
Go to the documentation of this file.
1#include "puzzle_game.hpp"
2
3#include <algorithm>
4#include <array>
5#include <cstdlib>
6#include <ctime>
7#include <utility>
8#include <vector>
9
10namespace mutatris {
11
12 PuzzleGame::PuzzleGame(int difficulty, std::function<void()> lineSoundCallback)
13 : playLineSound(std::move(lineSoundCallback)) {
14 newGame(difficulty);
15 }
16
17 void PuzzleGame::newGame(int difficulty) {
18 std::srand(static_cast<unsigned int>(std::time(nullptr)));
19 score = 0;
20 clears = 0;
21 level = 0;
22 timeout = difficulty == 0 ? 1200U : difficulty == 1 ? 900U
23 : 650U;
24 grid[0].initGrid(GRID_WIDTH, TOP_GRID_HEIGHT);
28 }
29
31 level = std::clamp((1200 - static_cast<int>(timeout)) / 100, 0, 10);
32 if (clearVisualRun()) {
33 return;
34 }
35 for (GameGrid &focusGrid : grid) {
36 for (int x = 0; x < focusGrid.width(); ++x) {
37 for (int y = 0; y < focusGrid.height(); ++y) {
38 if (clearRun(focusGrid, x, y, 0, 1) ||
39 clearRun(focusGrid, x, y, 1, 0) ||
40 clearRun(focusGrid, x, y, 1, 1) ||
41 clearRun(focusGrid, x, y, -1, 1)) {
42 return;
43 }
44 }
45 }
46 }
47 clearTopBottomSeams();
48 moveDownBlocks();
49 }
50
51 Block *PuzzleGame::blockAt(const CellRef &cell) {
52 if (cell.gridIndex < 0 || cell.gridIndex >= static_cast<int>(grid.size())) {
53 return nullptr;
54 }
55 return grid[static_cast<std::size_t>(cell.gridIndex)].at(cell.x, cell.y);
56 }
57
58 const Block *PuzzleGame::blockAt(const CellRef &cell) const {
59 if (cell.gridIndex < 0 || cell.gridIndex >= static_cast<int>(grid.size())) {
60 return nullptr;
61 }
62 return grid[static_cast<std::size_t>(cell.gridIndex)].at(cell.x, cell.y);
63 }
64
65 PuzzleGame::CellBounds PuzzleGame::cellBounds(const CellRef &cell) {
66 if (cell.gridIndex == 0) {
68 }
69 if (cell.gridIndex == 1) {
70 return {cell.y * BLOCK_HEIGHT, SIDE_GRID_Y + cell.x * BLOCK_WIDTH, BLOCK_HEIGHT, BLOCK_WIDTH};
71 }
72 if (cell.gridIndex == 2) {
73 return {CENTER_GRID_X + cell.x * BLOCK_WIDTH,
77 }
78 return {RIGHT_GRID_X + (SIDE_GRID_HEIGHT - 1 - cell.y) * BLOCK_HEIGHT,
79 SIDE_GRID_Y + (SIDE_GRID_WIDTH - 1 - cell.x) * BLOCK_WIDTH,
82 }
83
84 bool PuzzleGame::sameCell(const CellRef &a, const CellRef &b) const {
85 return a.gridIndex == b.gridIndex && a.x == b.x && a.y == b.y;
86 }
87
88 bool PuzzleGame::findNextVisualMatch(const CellRef &current, int color, int dx, int dy, CellRef &next) const {
89 const CellBounds currentBounds = cellBounds(current);
90 const int currentCenterX2 = currentBounds.x * 2 + currentBounds.width;
91 const int currentCenterY2 = currentBounds.y * 2 + currentBounds.height;
92 constexpr int TOLERANCE2 = BLOCK_HEIGHT + 2;
93 int bestError = TOLERANCE2 * TOLERANCE2 * 4 + 1;
94 bool found = false;
95
96 for (int gridIndex = 0; gridIndex < static_cast<int>(grid.size()); ++gridIndex) {
97 const GameGrid &focusGrid = grid[static_cast<std::size_t>(gridIndex)];
98 for (int y = 0; y < focusGrid.height(); ++y) {
99 for (int x = 0; x < focusGrid.width(); ++x) {
100 const CellRef candidate{gridIndex, x, y};
101 if (sameCell(current, candidate)) {
102 continue;
103 }
104 const Block *block = blockAt(candidate);
105 if (block == nullptr || block->color != color) {
106 continue;
107 }
108
109 const CellBounds candidateBounds = cellBounds(candidate);
110 const int candidateCenterX2 = candidateBounds.x * 2 + candidateBounds.width;
111 const int candidateCenterY2 = candidateBounds.y * 2 + candidateBounds.height;
112 const int expectedDeltaX2 = dx == 0 ? 0 : dx * (currentBounds.width + candidateBounds.width);
113 const int expectedDeltaY2 = dy == 0 ? 0 : dy * (currentBounds.height + candidateBounds.height);
114 const int errorX = std::abs((candidateCenterX2 - currentCenterX2) - expectedDeltaX2);
115 const int errorY = std::abs((candidateCenterY2 - currentCenterY2) - expectedDeltaY2);
116 if (errorX > TOLERANCE2 || errorY > TOLERANCE2) {
117 continue;
118 }
119
120 const int error = errorX * errorX + errorY * errorY;
121 if (error < bestError) {
122 bestError = error;
123 next = candidate;
124 found = true;
125 }
126 }
127 }
128 }
129
130 return found;
131 }
132
133 bool PuzzleGame::clearVisualRun() {
134 constexpr std::array<std::pair<int, int>, 4> directions{{{1, 0}, {0, 1}, {1, 1}, {-1, 1}}};
135 for (int gridIndex = 0; gridIndex < static_cast<int>(grid.size()); ++gridIndex) {
136 GameGrid &focusGrid = grid[static_cast<std::size_t>(gridIndex)];
137 for (int y = 0; y < focusGrid.height(); ++y) {
138 for (int x = 0; x < focusGrid.width(); ++x) {
139 const CellRef start{gridIndex, x, y};
140 const Block *startBlock = blockAt(start);
141 if (startBlock == nullptr || startBlock->color <= 0) {
142 continue;
143 }
144
145 for (const auto &[dx, dy] : directions) {
146 CellRef previous{};
147 if (findNextVisualMatch(start, startBlock->color, -dx, -dy, previous)) {
148 continue;
149 }
150
151 std::vector<CellRef> run{start};
152 CellRef current = start;
153 while (findNextVisualMatch(current, startBlock->color, dx, dy, current)) {
154 if (std::any_of(run.begin(), run.end(), [&](const CellRef &cell) {
155 return sameCell(cell, current);
156 })) {
157 break;
158 }
159 run.push_back(current);
160 }
161
162 if (run.size() < 3U) {
163 continue;
164 }
165
166 for (const CellRef &cell : run) {
167 if (Block *block = blockAt(cell)) {
168 markClearing(*block);
169 }
170 }
171 ++score;
172 recordClear();
173 playClearSound();
174 return true;
175 }
176 }
177 }
178 }
179 return false;
180 }
181
182 bool PuzzleGame::clearRun(GameGrid &focusGrid, int x, int y, int dx, int dy) {
183 Block *start = focusGrid.at(x, y);
184 if (start == nullptr || start->color <= 0) {
185 return false;
186 }
187
188 const int color = start->color;
189 const Block *previous = focusGrid.at(x - dx, y - dy);
190 if (previous != nullptr && previous->color == color) {
191 return false;
192 }
193
194 std::vector<Block *> blocks{};
195 int cx = x;
196 int cy = y;
197 while (Block *block = focusGrid.at(cx, cy)) {
198 if (block->color != color) {
199 break;
200 }
201 blocks.push_back(block);
202 cx += dx;
203 cy += dy;
204 }
205
206 if (blocks.size() < 3U) {
207 return false;
208 }
209
210 for (Block *block : blocks) {
211 markClearing(*block);
212 }
213 ++score;
214 recordClear();
215 playClearSound();
216 return true;
217 }
218
219 void PuzzleGame::clearTopBottomSeams() {
220 for (int x = 0; x < grid[0].width(); ++x) {
221 const int topY = grid[0].height() - 1;
222 const int bottomY = grid[2].height() - 1;
223 if (clearSeam({grid[0].at(x, topY), grid[0].at(x, topY - 1), grid[2].at(x, bottomY), grid[2].at(x, bottomY - 1)})) {
224 return;
225 }
226 if (clearSeam({grid[2].at(x, bottomY), grid[0].at(x, topY), grid[2].at(x, bottomY - 1), grid[0].at(x, topY - 1)})) {
227 return;
228 }
229 }
230 }
231
232 bool PuzzleGame::clearSeam(std::array<Block *, 4> blocks) {
233 if (blocks[0] == nullptr || blocks[1] == nullptr || blocks[2] == nullptr || blocks[0]->color <= 0) {
234 return false;
235 }
236 if (blocks[0]->color != blocks[1]->color || blocks[0]->color != blocks[2]->color) {
237 return false;
238 }
239 if (blocks[3] != nullptr && blocks[3]->color == blocks[0]->color) {
240 markClearing(*blocks[3]);
241 score += 10;
242 playClearSound();
243 }
244 markClearing(*blocks[0]);
245 markClearing(*blocks[1]);
246 markClearing(*blocks[2]);
247 ++score;
248 recordClear();
249 playClearSound();
250 return true;
251 }
252
253 void PuzzleGame::moveDownBlocks() {
254 for (GameGrid &focusGrid : grid) {
255 for (int x = 0; x < focusGrid.width(); ++x) {
256 for (int y = focusGrid.height() - 2; y >= 0; --y) {
257 Block *current = focusGrid.at(x, y);
258 Block *below = focusGrid.at(x, y + 1);
259 if (current != nullptr && below != nullptr && current->color > 0 && below->color == 0) {
260 std::swap(current->color, below->color);
261 return;
262 }
263 }
264 }
265 }
266 }
267
268 void PuzzleGame::markClearing(Block &block) {
269 block.color = -1;
270 block.clearElapsedMs = 0;
271 }
272
273 void PuzzleGame::recordClear() {
274 ++clears;
276 timeout = static_cast<unsigned int>(std::max(static_cast<int>(MIN_DROP_TIMEOUT_MS), static_cast<int>(timeout) - LEVEL_TIMEOUT_STEP_MS));
277 }
278 }
279
280 void PuzzleGame::playClearSound() {
281 if (playLineSound) {
282 playLineSound();
283 }
284 }
285
286} // namespace mutatris
PuzzleGame(int difficulty, std::function< void()> lineSoundCallback)
void newGame(int difficulty)
std::array< GameGrid, 4 > grid
constexpr int GRID_WIDTH
Definition common.hpp:16
constexpr unsigned int MIN_DROP_TIMEOUT_MS
Definition common.hpp:33
constexpr int BLOCK_HEIGHT
Definition common.hpp:11
constexpr int SIDE_GRID_Y
Definition common.hpp:24
constexpr int SIDE_GRID_WIDTH
Definition common.hpp:19
constexpr int CLEARS_PER_LEVEL
Definition common.hpp:31
constexpr int CENTER_GRID_X
Definition common.hpp:21
constexpr int BOTTOM_GRID_Y
Definition common.hpp:23
constexpr int TOP_GRID_Y
Definition common.hpp:22
constexpr int SIDE_GRID_HEIGHT
Definition common.hpp:20
constexpr int BOTTOM_GRID_HEIGHT
Definition common.hpp:18
constexpr int LEVEL_TIMEOUT_STEP_MS
Definition common.hpp:32
constexpr int TOP_GRID_HEIGHT
Definition common.hpp:17
constexpr int RIGHT_GRID_X
Definition common.hpp:25
constexpr int BLOCK_WIDTH
Definition common.hpp:10