MXVK Vulkan Framework 0.35.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) : mxvk::VK_Window("Knights Tour", width, height, fullscreen, MXVK_VALIDATION, enableVsync), assetRoot((path.empty() || path == ".") ? std::string(KNIGHT_ASSET_DIR) : path), fontPath(assetRoot + "/data/font.ttf"), introStarted(SDL_GetTicks()) {
71 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
72 setFont(fontPath, TEXT_SIZE);
73 loadWindowIcon();
74
75 intro = createSprite(assetRoot + "/data/logo.png", "", assetRoot + "/data/fade.frag.spv");
76 whiteCell = makeSolidSprite(255, 255, 255, 255);
77 redCell = makeSolidSprite(255, 0, 0, 255);
78 visitedCell = makeSolidSprite(0, 0, 0, 255);
79 knightSprite = createSprite(assetRoot + "/data/knight.png", "", assetRoot + "/data/color_key.frag.spv");
80
81 if (mxvk::Joystick::joysticks() > 0) {
82 if (joystick.open(0)) {
83 std::cout << std::format("Joystick opened: {}\n", joystick.name());
84 } else {
85 std::cout << "Could not open joystick..\n";
86 }
87 }
88 }
89
90 void event(SDL_Event &event) override {
91 if (event.type == SDL_EVENT_KEY_DOWN) {
92 if (event.key.key == SDLK_ESCAPE) {
93 exit();
94 return;
95 }
96 if (event.key.key == SDLK_S && !event.key.repeat) {
97 try {
98 saveSnapshot("screenshot.png");
99 std::cout << "mx: Screenshot captured..\n";
100 } catch (const mxvk::Exception &exception) {
101 std::cerr << std::format("mxvk: screenshot failed: {}\n", exception.text());
102 }
103 return;
104 }
105 if (screen != Screen::Tour) {
106 return;
107 }
108 if (event.key.key == SDLK_SPACE) {
109 tour.nextMove();
110 } else if (event.key.key == SDLK_RETURN && !event.key.repeat) {
111 tour.resetTour();
112 }
113 return;
114 }
115
116 if (screen != Screen::Tour) {
117 return;
118 }
119
120 if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
121 if (event.button.button == SDL_BUTTON_LEFT) {
122 resetTourFromMousePosition(event.button.x, event.button.y);
123 } else if (event.button.button == SDL_BUTTON_RIGHT) {
124 tour.resetTour();
125 }
126 } else if (event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN) {
127 if (event.jbutton.button == 1) {
128 tour.nextMove();
129 } else if (event.jbutton.button == 2) {
130 tour.resetTour();
131 }
132 }
133 }
134
135 void proc() override {
136 const float scaleX = swapchain_extent.width > 0U ? static_cast<float>(swapchain_extent.width) / DESIGN_WIDTH : 1.0f;
137 const float scaleY = swapchain_extent.height > 0U ? static_cast<float>(swapchain_extent.height) / DESIGN_HEIGHT : 1.0f;
138
139 if (screen == Screen::Intro) {
140 drawIntro();
141 return;
142 }
143
144 updateFont(scaleY);
145 tour.drawBoard(*whiteCell, *redCell, *visitedCell, scaleX, scaleY);
146 tour.drawKnight(*knightSprite, scaleX, scaleY);
147
148 if (!tour.isTourOver()) {
149 printScaledText("Knights Tour - Space to Move, Click a Square to Restart", TEXT_OFFSET_X, TEXT_OFFSET_Y, scaleX, scaleY);
150 printMoveCount(scaleX, scaleY);
151 } else {
152 printScaledText("-[ Tour Complete ]- Press Return to Reset", TEXT_OFFSET_X, TEXT_OFFSET_Y, scaleX, scaleY);
153 }
154 }
155
156 private:
157 enum class Screen { Intro, Tour };
158
159 static constexpr float DESIGN_WIDTH = 640.0f;
160 static constexpr float DESIGN_HEIGHT = 480.0f;
161 static constexpr int TEXT_OFFSET_X = 15;
162 static constexpr int TEXT_OFFSET_Y = 5;
163 static constexpr int TEXT_SIZE = 14;
164 static constexpr Uint64 INTRO_STEP_MS = 15;
165 static constexpr int INTRO_ALPHA_STEP = 3;
166
167 std::string assetRoot;
168 std::string fontPath;
169 mxvk::VK_Sprite *intro = nullptr;
170 mxvk::VK_Sprite *knightSprite = nullptr;
171 mxvk::VK_Sprite *whiteCell = nullptr;
172 mxvk::VK_Sprite *redCell = nullptr;
173 mxvk::VK_Sprite *visitedCell = nullptr;
175 Tour tour;
176 Screen screen = Screen::Intro;
177 Uint64 introStarted = 0;
178 int currentFontSize = TEXT_SIZE;
179
180 mxvk::VK_Sprite *makeSolidSprite(std::uint8_t red, std::uint8_t green, std::uint8_t blue, std::uint8_t alpha) {
181 const std::array<std::uint8_t, 4> pixel = {red, green, blue, alpha};
182 mxvk::VK_Sprite *sprite = createSprite(1, 1);
183 sprite->updateTexture(pixel.data(), 1, 1, 4);
184 return sprite;
185 }
186
187 void loadWindowIcon() {
188 const char *videoDriver = SDL_GetCurrentVideoDriver();
189 if (videoDriver != nullptr && std::string(videoDriver) == "wayland") {
190 return;
191 }
192
193 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> icon(mxvk::LoadPNG((assetRoot + "/data/knight.png").c_str()), SDL_DestroySurface);
194 if (icon != nullptr && !SDL_SetWindowIcon(getSDLWindow(), icon.get())) {
195 std::cerr << std::format("knight: could not set window icon: {}\n", SDL_GetError());
196 }
197 }
198
199 void drawIntro() {
200 const Uint64 elapsed = SDL_GetTicks() - introStarted;
201 const int fadeSteps = static_cast<int>(elapsed / INTRO_STEP_MS);
202 const int alpha = std::max(0, 255 - fadeSteps * INTRO_ALPHA_STEP);
203 if (alpha == 0) {
204 screen = Screen::Tour;
205 return;
206 }
207
208 intro->setShaderParams(static_cast<float>(alpha) / 255.0f);
209 intro->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
210 }
211
212 void updateFont(float scaleY) {
213 const int desiredSize = std::max(1, static_cast<int>(std::lround(TEXT_SIZE * scaleY)));
214 if (desiredSize == currentFontSize) {
215 return;
216 }
217 setFont(fontPath, desiredSize);
218 currentFontSize = desiredSize;
219 }
220
221 void printScaledText(const std::string &text, int x, int y, float scaleX, float scaleY) { printText(text, static_cast<int>(std::lround(x * scaleX)), static_cast<int>(std::lround(y * scaleY)), SDL_Color{255, 255, 255, 255}); }
222
223 void printMoveCount(float scaleX, float scaleY) {
224 const std::string text = std::format("Moves: {}", tour.getMoves());
225 int textWidth = 0;
226 int textHeight = 0;
227 const int rightMargin = static_cast<int>(std::lround(TEXT_OFFSET_X * scaleX));
228 int x = static_cast<int>(std::lround(400.0f * scaleX));
229 if (getTextDimensions(text, textWidth, textHeight)) {
230 x = std::max(0, static_cast<int>(swapchain_extent.width) - textWidth - rightMargin);
231 }
232 printText(text, x, static_cast<int>(std::lround(TEXT_OFFSET_Y * scaleY)), SDL_Color{255, 255, 255, 255});
233 }
234
235 void resetTourFromMousePosition(float mouseX, float mouseY) {
236 int windowWidth = 0;
237 int windowHeight = 0;
238 SDL_GetWindowSize(getSDLWindow(), &windowWidth, &windowHeight);
239 if (windowWidth <= 0 || windowHeight <= 0) {
240 return;
241 }
242
243 const float designX = mouseX * DESIGN_WIDTH / static_cast<float>(windowWidth);
244 const float designY = mouseY * DESIGN_HEIGHT / static_cast<float>(windowHeight);
245 tour.resetTourFromPoint(designX, designY);
246 }
247 };
248
249 Tour::Tour() : moves(1), tourOver(false) {
250 std::srand(static_cast<unsigned int>(std::time(nullptr)));
251 initializeBoard();
252 resetTour();
253 }
254
255 void Tour::initializeBoard() { board.resize(BOARD_SIZE, std::vector<int>(BOARD_SIZE, 0)); }
256
257 void Tour::clearBoard() {
258 for (auto &row : board) {
259 std::fill(row.begin(), row.end(), 0);
260 }
261 }
262
263 bool Tour::isValidMove(const Position &position) const { return position.row >= 0 && position.row < BOARD_SIZE && position.col >= 0 && position.col < BOARD_SIZE && board[position.row][position.col] == 0; }
264
265 int Tour::getDegree(const Position &position) const {
266 int count = 0;
267 for (int index = 0; index < 8; ++index) {
268 const int newRow = position.row + vertical[index];
269 const int newCol = position.col + horizontal[index];
270 if (isValidMove(Position(newRow, newCol))) {
271 ++count;
272 }
273 }
274 return count;
275 }
276
277 bool Tour::solveKnightsTour(Position position, int moveCount) {
278 if (moveCount == TOTAL_MOVES) {
279 return true;
280 }
281
282 std::vector<std::pair<int, Position>> nextMoves;
283 for (int index = 0; index < 8; ++index) {
284 Position nextPosition(position.row + vertical[index], position.col + horizontal[index]);
285 if (isValidMove(nextPosition)) {
286 nextMoves.emplace_back(getDegree(nextPosition), nextPosition);
287 }
288 }
289
290 std::sort(nextMoves.begin(), nextMoves.end(), [](const auto &left, const auto &right) { return left.first < right.first; });
291
292 for (const auto &[degree, nextPosition] : nextMoves) {
293 [[maybe_unused]] const int moveDegree = degree;
294 board[nextPosition.row][nextPosition.col] = moveCount;
295 moveSequence.push_back(nextPosition);
296
297 if (solveKnightsTour(nextPosition, moveCount + 1)) {
298 return true;
299 }
300
301 board[nextPosition.row][nextPosition.col] = 0;
302 moveSequence.pop_back();
303 }
304 return false;
305 }
306
307 void Tour::resetTour() { resetTour(std::rand() % BOARD_SIZE, std::rand() % BOARD_SIZE); }
308
309 void Tour::resetTour(int startRow, int startCol) {
310 if (startRow < 0 || startRow >= BOARD_SIZE || startCol < 0 || startCol >= BOARD_SIZE) {
311 return;
312 }
313
314 clearBoard();
315 knightPos = Position(startRow, startCol);
316 board[knightPos.row][knightPos.col] = 1;
317 moveSequence.clear();
318 moveSequence.push_back(knightPos);
319 solveKnightsTour(knightPos, 2);
320 moves = 1;
321 tourOver = false;
322 }
323
324 void Tour::resetTourFromPoint(float x, float y) {
325 const int localX = static_cast<int>(std::floor(x)) - START_X;
326 const int localY = static_cast<int>(std::floor(y)) - START_Y;
327 if (localX < 0 || localY < 0) {
328 return;
329 }
330
331 const int col = localX / CELL_SIZE;
332 const int row = localY / CELL_SIZE;
333 if (row >= BOARD_SIZE || col >= BOARD_SIZE || localX % CELL_SIZE >= CELL_DRAW_SIZE || localY % CELL_SIZE >= CELL_DRAW_SIZE) {
334 return;
335 }
336
337 resetTour(row, col);
338 }
339
341 if (tourOver || static_cast<std::size_t>(moves) >= moveSequence.size()) {
342 return;
343 }
344
345 const Position nextPosition = moveSequence[static_cast<std::size_t>(moves)];
346 board[knightPos.row][knightPos.col] = -1;
347 knightPos = nextPosition;
348 ++moves;
349 board[knightPos.row][knightPos.col] = moves;
350 tourOver = static_cast<std::size_t>(moves) == moveSequence.size();
351 }
352
353 void Tour::drawBoard(mxvk::VK_Sprite &whiteCell, mxvk::VK_Sprite &redCell, mxvk::VK_Sprite &visitedCell, float scaleX, float scaleY) const {
354 for (int row = 0; row < BOARD_SIZE; ++row) {
355 for (int col = 0; col < BOARD_SIZE; ++col) {
356 mxvk::VK_Sprite *cell = nullptr;
357 if (board[row][col] == -1) {
358 cell = &visitedCell;
359 } else if ((row + col) % 2 == 0) {
360 cell = &whiteCell;
361 } else {
362 cell = &redCell;
363 }
364
365 cell->drawSpriteRect(static_cast<int>(std::lround((START_X + col * CELL_SIZE) * scaleX)), static_cast<int>(std::lround((START_Y + row * CELL_SIZE) * scaleY)), static_cast<int>(std::lround(CELL_DRAW_SIZE * scaleX)), static_cast<int>(std::lround(CELL_DRAW_SIZE * scaleY)));
366 }
367 }
368 }
369
370 void Tour::drawKnight(mxvk::VK_Sprite &texture, float scaleX, float scaleY) const { texture.drawSpriteRect(static_cast<int>(std::lround((START_X + knightPos.col * CELL_SIZE + 5) * scaleX)), static_cast<int>(std::lround((START_Y + knightPos.row * CELL_SIZE + 5) * scaleY)), static_cast<int>(std::lround(KNIGHT_SIZE * scaleX)), static_cast<int>(std::lround(KNIGHT_SIZE * scaleY))); }
371} // namespace knight
372
373int main(int argc, char **argv) {
374 try {
375 Arguments args = proc_args(argc, argv);
376 if (!args.resolutionSpecified) {
377 args.width = 960;
378 args.height = 720;
379 }
380 knight::KnightsTourWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
381 window.loop();
382 } catch (mxvk::Exception &exception) {
383 std::cerr << std::format("mxvk: Exception: {}\n", exception.text());
384 return EXIT_FAILURE;
385 } catch (ArgException<std::string> &exception) {
386 std::cerr << std::format("mxvk: Argument Exception: {}\n", exception.text());
387 return EXIT_FAILURE;
388 }
389 return EXIT_SUCCESS;
390}
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:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
void proc() override
Execute one processing/update step.
Definition knight.cpp:135
void event(SDL_Event &event) override
Handle one SDL event.
Definition knight.cpp:90
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:370
int BOARD_SIZE
Definition knight.py:55
void drawBoard(mxvk::VK_Sprite &whiteCell, mxvk::VK_Sprite &redCell, mxvk::VK_Sprite &visitedCell, float scaleX, float scaleY) const
Definition knight.cpp:353
list board
Definition knight.py:69
void resetTourFromPoint(float x, float y)
Definition knight.cpp:324
void resetTour()
Definition knight.cpp:307
int getMoves() const
Definition knight.cpp:33
void nextMove()
Definition knight.cpp:340
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:39
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
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:4455
VkExtent2D swapchain_extent
Definition mxvk.hpp:616
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4004
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:192
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
Definition mxvk.cpp:826
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
void exit()
Request loop termination.
Definition mxvk.cpp:1378
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:3872
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:3919
#define MXVK_VALIDATION
Definition mxvk.hpp:28
SDL3 joystick and gamepad RAII wrappers.
PNG image loading and saving utilities via SDL3.
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
VK_Joystick Joystick
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:89
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool resolutionSpecified
Whether -r/–resolution was provided.
Definition argz.hpp:722
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720