MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
knight.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
5#include "mxvk/mxvk_png.hpp"
6
7#include <SDL3/SDL.h>
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <cstdlib>
13#include <ctime>
14#include <format>
15#include <iostream>
16#include <memory>
17#include <string>
18#include <utility>
19#include <vector>
20
21namespace knight {
22 class Tour {
23 public:
24 Tour();
25
26 void drawBoard(mxvk::VK_Sprite &whiteCell, mxvk::VK_Sprite &redCell, mxvk::VK_Sprite &visitedCell, float scaleX, float scaleY) const;
27 void drawKnight(mxvk::VK_Sprite &texture, float scaleX, float scaleY) const;
28 void nextMove();
29 void resetTour();
30 void resetTour(int startRow, int startCol);
31 void resetTourFromPoint(float x, float y);
32
33 [[nodiscard]] int getMoves() const { return moves; }
34 [[nodiscard]] bool isTourOver() const { return tourOver; }
35
36 private:
37 struct Position {
38 int row;
39 int col;
40
41 constexpr Position(int newRow = 0, int newCol = 0) : row(newRow), col(newCol) {}
42 };
43
44 void initializeBoard();
45 void clearBoard();
46 [[nodiscard]] bool isValidMove(const Position &position) const;
47 [[nodiscard]] int getDegree(const Position &position) const;
48 bool solveKnightsTour(Position position, int moveCount);
49
50 static constexpr int BOARD_SIZE = 8;
51 static constexpr int TOTAL_MOVES = BOARD_SIZE * BOARD_SIZE + 1;
52 static constexpr int START_X = 100;
53 static constexpr int START_Y = 30;
54 static constexpr int CELL_SIZE = 55;
55 static constexpr int CELL_DRAW_SIZE = 50;
56 static constexpr int KNIGHT_SIZE = 35;
57
58 std::vector<std::vector<int>> board;
59 std::vector<Position> moveSequence;
60 Position knightPos;
61 int moves;
62 bool tourOver;
63
64 static constexpr std::array<int, 8> horizontal = {2, 1, -1, -2, -2, -1, 1, 2};
65 static constexpr std::array<int, 8> vertical = {-1, -2, -2, -1, 1, 2, 2, 1};
66 };
67
69 public:
70 KnightsTourWindow(const std::string &path, int width, int height, bool fullscreen, bool enableVsync)
71 : mxvk::VK_Window("Knights Tour", width, height, fullscreen, MXVK_VALIDATION, enableVsync),
72 assetRoot((path.empty() || path == ".") ? std::string(KNIGHT_ASSET_DIR) : path),
73 fontPath(assetRoot + "/data/font.ttf"),
74 introStarted(SDL_GetTicks()) {
75 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
76 setFont(fontPath, TEXT_SIZE);
77 loadWindowIcon();
78
79 intro = createSprite(assetRoot + "/data/logo.png", "", assetRoot + "/data/fade.frag.spv");
80 whiteCell = makeSolidSprite(255, 255, 255, 255);
81 redCell = makeSolidSprite(255, 0, 0, 255);
82 visitedCell = makeSolidSprite(0, 0, 0, 255);
83 knightSprite = createSprite(assetRoot + "/data/knight.png", "", assetRoot + "/data/color_key.frag.spv");
84
85 if (mxvk::Joystick::joysticks() > 0) {
86 if (joystick.open(0)) {
87 std::cout << std::format("Joystick opened: {}\n", joystick.name());
88 } else {
89 std::cout << "Could not open joystick..\n";
90 }
91 }
92 }
93
94 void event(SDL_Event &event) override {
95 if (event.type == SDL_EVENT_KEY_DOWN) {
96 if (event.key.key == SDLK_ESCAPE) {
97 exit();
98 return;
99 }
100 if (event.key.key == SDLK_S && !event.key.repeat) {
101 try {
102 saveSnapshot("screenshot.png");
103 std::cout << "mx: Screenshot captured..\n";
104 } catch (const mxvk::Exception &exception) {
105 std::cerr << std::format("mxvk: screenshot failed: {}\n", exception.text());
106 }
107 return;
108 }
109 if (screen != Screen::Tour) {
110 return;
111 }
112 if (event.key.key == SDLK_SPACE) {
113 tour.nextMove();
114 } else if (event.key.key == SDLK_RETURN && !event.key.repeat) {
115 tour.resetTour();
116 }
117 return;
118 }
119
120 if (screen != Screen::Tour) {
121 return;
122 }
123
124 if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
125 if (event.button.button == SDL_BUTTON_LEFT) {
126 resetTourFromMousePosition(event.button.x, event.button.y);
127 } else if (event.button.button == SDL_BUTTON_RIGHT) {
128 tour.resetTour();
129 }
130 } else if (event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN) {
131 if (event.jbutton.button == 1) {
132 tour.nextMove();
133 } else if (event.jbutton.button == 2) {
134 tour.resetTour();
135 }
136 }
137 }
138
139 void proc() override {
140 const float scaleX = swapchain_extent.width > 0U ? static_cast<float>(swapchain_extent.width) / DESIGN_WIDTH : 1.0f;
141 const float scaleY = swapchain_extent.height > 0U ? static_cast<float>(swapchain_extent.height) / DESIGN_HEIGHT : 1.0f;
142
143 if (screen == Screen::Intro) {
144 drawIntro();
145 return;
146 }
147
148 updateFont(scaleY);
149 tour.drawBoard(*whiteCell, *redCell, *visitedCell, scaleX, scaleY);
150 tour.drawKnight(*knightSprite, scaleX, scaleY);
151
152 if (!tour.isTourOver()) {
153 printScaledText("Knights Tour - Space to Move, Click a Square to Restart", TEXT_OFFSET_X, TEXT_OFFSET_Y, scaleX, scaleY);
154 printMoveCount(scaleX, scaleY);
155 } else {
156 printScaledText("-[ Tour Complete ]- Press Return to Reset", TEXT_OFFSET_X, TEXT_OFFSET_Y, scaleX, scaleY);
157 }
158 }
159
160 private:
161 enum class Screen {
162 Intro,
163 Tour
164 };
165
166 static constexpr float DESIGN_WIDTH = 640.0f;
167 static constexpr float DESIGN_HEIGHT = 480.0f;
168 static constexpr int TEXT_OFFSET_X = 15;
169 static constexpr int TEXT_OFFSET_Y = 5;
170 static constexpr int TEXT_SIZE = 14;
171 static constexpr Uint64 INTRO_STEP_MS = 15;
172 static constexpr int INTRO_ALPHA_STEP = 3;
173
174 std::string assetRoot;
175 std::string fontPath;
176 mxvk::VK_Sprite *intro = nullptr;
177 mxvk::VK_Sprite *knightSprite = nullptr;
178 mxvk::VK_Sprite *whiteCell = nullptr;
179 mxvk::VK_Sprite *redCell = nullptr;
180 mxvk::VK_Sprite *visitedCell = nullptr;
181 mxvk::Joystick joystick;
182 Tour tour;
183 Screen screen = Screen::Intro;
184 Uint64 introStarted = 0;
185 int currentFontSize = TEXT_SIZE;
186
187 mxvk::VK_Sprite *makeSolidSprite(std::uint8_t red, std::uint8_t green, std::uint8_t blue, std::uint8_t alpha) {
188 const std::array<std::uint8_t, 4> pixel = {red, green, blue, alpha};
189 mxvk::VK_Sprite *sprite = createSprite(1, 1);
190 sprite->updateTexture(pixel.data(), 1, 1, 4);
191 return sprite;
192 }
193
194 void loadWindowIcon() {
195 const char *videoDriver = SDL_GetCurrentVideoDriver();
196 if (videoDriver != nullptr && std::string(videoDriver) == "wayland") {
197 return;
198 }
199
200 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> icon(
201 mxvk::LoadPNG((assetRoot + "/data/knight.png").c_str()), SDL_DestroySurface);
202 if (icon != nullptr && !SDL_SetWindowIcon(getSDLWindow(), icon.get())) {
203 std::cerr << std::format("knight: could not set window icon: {}\n", SDL_GetError());
204 }
205 }
206
207 void drawIntro() {
208 const Uint64 elapsed = SDL_GetTicks() - introStarted;
209 const int fadeSteps = static_cast<int>(elapsed / INTRO_STEP_MS);
210 const int alpha = std::max(0, 255 - fadeSteps * INTRO_ALPHA_STEP);
211 if (alpha == 0) {
212 screen = Screen::Tour;
213 return;
214 }
215
216 intro->setShaderParams(static_cast<float>(alpha) / 255.0f);
217 intro->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
218 }
219
220 void updateFont(float scaleY) {
221 const int desiredSize = std::max(1, static_cast<int>(std::lround(TEXT_SIZE * scaleY)));
222 if (desiredSize == currentFontSize) {
223 return;
224 }
225 setFont(fontPath, desiredSize);
226 currentFontSize = desiredSize;
227 }
228
229 void printScaledText(const std::string &text, int x, int y, float scaleX, float scaleY) {
230 printText(text,
231 static_cast<int>(std::lround(x * scaleX)),
232 static_cast<int>(std::lround(y * scaleY)),
233 SDL_Color{255, 255, 255, 255});
234 }
235
236 void printMoveCount(float scaleX, float scaleY) {
237 const std::string text = std::format("Moves: {}", tour.getMoves());
238 int textWidth = 0;
239 int textHeight = 0;
240 const int rightMargin = static_cast<int>(std::lround(TEXT_OFFSET_X * scaleX));
241 int x = static_cast<int>(std::lround(400.0f * scaleX));
242 if (getTextDimensions(text, textWidth, textHeight)) {
243 x = std::max(0, static_cast<int>(swapchain_extent.width) - textWidth - rightMargin);
244 }
245 printText(text,
246 x,
247 static_cast<int>(std::lround(TEXT_OFFSET_Y * scaleY)),
248 SDL_Color{255, 255, 255, 255});
249 }
250
251 void resetTourFromMousePosition(float mouseX, float mouseY) {
252 int windowWidth = 0;
253 int windowHeight = 0;
254 SDL_GetWindowSize(getSDLWindow(), &windowWidth, &windowHeight);
255 if (windowWidth <= 0 || windowHeight <= 0) {
256 return;
257 }
258
259 const float designX = mouseX * DESIGN_WIDTH / static_cast<float>(windowWidth);
260 const float designY = mouseY * DESIGN_HEIGHT / static_cast<float>(windowHeight);
261 tour.resetTourFromPoint(designX, designY);
262 }
263 };
264
265 Tour::Tour() : moves(1), tourOver(false) {
266 std::srand(static_cast<unsigned int>(std::time(nullptr)));
267 initializeBoard();
268 resetTour();
269 }
270
271 void Tour::initializeBoard() {
272 board.resize(BOARD_SIZE, std::vector<int>(BOARD_SIZE, 0));
273 }
274
275 void Tour::clearBoard() {
276 for (auto &row : board) {
277 std::fill(row.begin(), row.end(), 0);
278 }
279 }
280
281 bool Tour::isValidMove(const Position &position) const {
282 return position.row >= 0 && position.row < BOARD_SIZE &&
283 position.col >= 0 && position.col < BOARD_SIZE &&
284 board[position.row][position.col] == 0;
285 }
286
287 int Tour::getDegree(const Position &position) const {
288 int count = 0;
289 for (int index = 0; index < 8; ++index) {
290 const int newRow = position.row + vertical[index];
291 const int newCol = position.col + horizontal[index];
292 if (isValidMove(Position(newRow, newCol))) {
293 ++count;
294 }
295 }
296 return count;
297 }
298
299 bool Tour::solveKnightsTour(Position position, int moveCount) {
300 if (moveCount == TOTAL_MOVES) {
301 return true;
302 }
303
304 std::vector<std::pair<int, Position>> nextMoves;
305 for (int index = 0; index < 8; ++index) {
306 Position nextPosition(position.row + vertical[index], position.col + horizontal[index]);
307 if (isValidMove(nextPosition)) {
308 nextMoves.emplace_back(getDegree(nextPosition), nextPosition);
309 }
310 }
311
312 std::sort(nextMoves.begin(), nextMoves.end(), [](const auto &left, const auto &right) {
313 return left.first < right.first;
314 });
315
316 for (const auto &[degree, nextPosition] : nextMoves) {
317 [[maybe_unused]] const int moveDegree = degree;
318 board[nextPosition.row][nextPosition.col] = moveCount;
319 moveSequence.push_back(nextPosition);
320
321 if (solveKnightsTour(nextPosition, moveCount + 1)) {
322 return true;
323 }
324
325 board[nextPosition.row][nextPosition.col] = 0;
326 moveSequence.pop_back();
327 }
328 return false;
329 }
330
332 resetTour(std::rand() % BOARD_SIZE, std::rand() % BOARD_SIZE);
333 }
334
335 void Tour::resetTour(int startRow, int startCol) {
336 if (startRow < 0 || startRow >= BOARD_SIZE || startCol < 0 || startCol >= BOARD_SIZE) {
337 return;
338 }
339
340 clearBoard();
341 knightPos = Position(startRow, startCol);
342 board[knightPos.row][knightPos.col] = 1;
343 moveSequence.clear();
344 moveSequence.push_back(knightPos);
345 solveKnightsTour(knightPos, 2);
346 moves = 1;
347 tourOver = false;
348 }
349
350 void Tour::resetTourFromPoint(float x, float y) {
351 const int localX = static_cast<int>(std::floor(x)) - START_X;
352 const int localY = static_cast<int>(std::floor(y)) - START_Y;
353 if (localX < 0 || localY < 0) {
354 return;
355 }
356
357 const int col = localX / CELL_SIZE;
358 const int row = localY / CELL_SIZE;
359 if (row >= BOARD_SIZE || col >= BOARD_SIZE ||
360 localX % CELL_SIZE >= CELL_DRAW_SIZE || localY % CELL_SIZE >= CELL_DRAW_SIZE) {
361 return;
362 }
363
364 resetTour(row, col);
365 }
366
368 if (tourOver || static_cast<std::size_t>(moves) >= moveSequence.size()) {
369 return;
370 }
371
372 const Position nextPosition = moveSequence[static_cast<std::size_t>(moves)];
373 board[knightPos.row][knightPos.col] = -1;
374 knightPos = nextPosition;
375 ++moves;
376 board[knightPos.row][knightPos.col] = moves;
377 tourOver = static_cast<std::size_t>(moves) == moveSequence.size();
378 }
379
380 void Tour::drawBoard(mxvk::VK_Sprite &whiteCell, mxvk::VK_Sprite &redCell, mxvk::VK_Sprite &visitedCell, float scaleX, float scaleY) const {
381 for (int row = 0; row < BOARD_SIZE; ++row) {
382 for (int col = 0; col < BOARD_SIZE; ++col) {
383 mxvk::VK_Sprite *cell = nullptr;
384 if (board[row][col] == -1) {
385 cell = &visitedCell;
386 } else if ((row + col) % 2 == 0) {
387 cell = &whiteCell;
388 } else {
389 cell = &redCell;
390 }
391
392 cell->drawSpriteRect(
393 static_cast<int>(std::lround((START_X + col * CELL_SIZE) * scaleX)),
394 static_cast<int>(std::lround((START_Y + row * CELL_SIZE) * scaleY)),
395 static_cast<int>(std::lround(CELL_DRAW_SIZE * scaleX)),
396 static_cast<int>(std::lround(CELL_DRAW_SIZE * scaleY)));
397 }
398 }
399 }
400
401 void Tour::drawKnight(mxvk::VK_Sprite &texture, float scaleX, float scaleY) const {
402 texture.drawSpriteRect(
403 static_cast<int>(std::lround((START_X + knightPos.col * CELL_SIZE + 5) * scaleX)),
404 static_cast<int>(std::lround((START_Y + knightPos.row * CELL_SIZE + 5) * scaleY)),
405 static_cast<int>(std::lround(KNIGHT_SIZE * scaleX)),
406 static_cast<int>(std::lround(KNIGHT_SIZE * scaleY)));
407 }
408} // namespace knight
409
410int main(int argc, char **argv) {
411 try {
412 Arguments args = proc_args(argc, argv);
413 if (!args.resolutionSpecified) {
414 args.width = 960;
415 args.height = 720;
416 }
417 knight::KnightsTourWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
418 window.loop();
419 } catch (mxvk::Exception &exception) {
420 std::cerr << std::format("mxvk: Exception: {}\n", exception.text());
421 return EXIT_FAILURE;
422 } catch (ArgException<std::string> &exception) {
423 std::cerr << std::format("mxvk: Argument Exception: {}\n", exception.text());
424 return EXIT_FAILURE;
425 }
426 return EXIT_SUCCESS;
427}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:872
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:178
void proc() override
Execute one processing/update step.
Definition knight.cpp:139
void event(SDL_Event &event) override
Handle one SDL event.
Definition knight.cpp:94
KnightsTourWindow(const std::string &path, int width, int height, bool fullscreen, bool enableVsync)
Definition knight.cpp:70
void drawKnight(mxvk::VK_Sprite &texture, float scaleX, float scaleY) const
Definition knight.cpp:401
void drawBoard(mxvk::VK_Sprite &whiteCell, mxvk::VK_Sprite &redCell, mxvk::VK_Sprite &visitedCell, float scaleX, float scaleY) const
Definition knight.cpp:380
void resetTourFromPoint(float x, float y)
Definition knight.cpp:350
void resetTour()
Definition knight.cpp:331
int getMoves() const
Definition knight.cpp:33
void nextMove()
Definition knight.cpp:367
bool isTourOver() const
Definition knight.cpp:34
std::string text() const
static int joysticks()
Return the number of connected joysticks.
void updateTexture(SDL_Surface *surface)
Replace the sprite texture from an SDL_Surface.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:3477
VkExtent2D swapchain_extent
Definition mxvk.hpp:495
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:3069
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:165
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
Definition mxvk.cpp:782
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:593
void exit()
Request loop termination.
Definition mxvk.cpp:1126
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:2991
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3018
int main(void)
Definition main.cpp:7
#define MXVK_VALIDATION
Definition mxvk.hpp:27
SDL3 joystick and gamepad RAII wrappers.
PNG image loading and saving utilities via SDL3.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
VK_Joystick Joystick
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:103
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
bool resolutionSpecified
Whether -r/–resolution was provided.
Definition argz.hpp:734
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:750
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:735
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732