30 void resetTour(
int startRow,
int startCol);
33 [[nodiscard]]
int getMoves()
const {
return moves; }
34 [[nodiscard]]
bool isTourOver()
const {
return tourOver; }
41 constexpr Position(
int newRow = 0,
int newCol = 0) : row(newRow), col(newCol) {}
44 void initializeBoard();
46 [[nodiscard]]
bool isValidMove(
const Position &position)
const;
47 [[nodiscard]]
int getDegree(
const Position &position)
const;
48 bool solveKnightsTour(Position position,
int moveCount);
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;
58 std::vector<std::vector<int>> board;
59 std::vector<Position> moveSequence;
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};
70 KnightsTourWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enableVsync)
72 assetRoot((path.empty() || path ==
".") ? std::string(KNIGHT_ASSET_DIR) : path),
73 fontPath(assetRoot +
"/data/font.ttf"),
74 introStarted(SDL_GetTicks()) {
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");
86 if (joystick.open(0)) {
87 std::cout << std::format(
"Joystick opened: {}\n", joystick.name());
89 std::cout <<
"Could not open joystick..\n";
95 if (
event.type == SDL_EVENT_KEY_DOWN) {
96 if (
event.key.key == SDLK_ESCAPE) {
100 if (
event.key.key == SDLK_S && !
event.key.repeat) {
103 std::cout <<
"mx: Screenshot captured..\n";
105 std::cerr << std::format(
"mxvk: screenshot failed: {}\n", exception.
text());
109 if (screen != Screen::Tour) {
112 if (
event.key.key == SDLK_SPACE) {
114 }
else if (
event.key.key == SDLK_RETURN && !
event.key.repeat) {
120 if (screen != Screen::Tour) {
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) {
130 }
else if (
event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN) {
131 if (
event.jbutton.button == 1) {
133 }
else if (
event.jbutton.button == 2) {
143 if (screen == Screen::Intro) {
149 tour.drawBoard(*whiteCell, *redCell, *visitedCell, scaleX, scaleY);
150 tour.drawKnight(*knightSprite, scaleX, scaleY);
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);
156 printScaledText(
"-[ Tour Complete ]- Press Return to Reset", TEXT_OFFSET_X, TEXT_OFFSET_Y, scaleX, scaleY);
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;
174 std::string assetRoot;
175 std::string fontPath;
184 Uint64 introStarted = 0;
185 int currentFontSize = TEXT_SIZE;
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};
194 void loadWindowIcon() {
195 const char *videoDriver = SDL_GetCurrentVideoDriver();
196 if (videoDriver !=
nullptr && std::string(videoDriver) ==
"wayland") {
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());
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);
212 screen = Screen::Tour;
216 intro->setShaderParams(
static_cast<float>(alpha) / 255.0f);
220 void updateFont(
float scaleY) {
221 const int desiredSize = std::max(1,
static_cast<int>(std::lround(TEXT_SIZE * scaleY)));
222 if (desiredSize == currentFontSize) {
225 setFont(fontPath, desiredSize);
226 currentFontSize = desiredSize;
229 void printScaledText(
const std::string &text,
int x,
int y,
float scaleX,
float scaleY) {
231 static_cast<int>(std::lround(x * scaleX)),
232 static_cast<int>(std::lround(y * scaleY)),
233 SDL_Color{255, 255, 255, 255});
236 void printMoveCount(
float scaleX,
float scaleY) {
237 const std::string text = std::format(
"Moves: {}", tour.getMoves());
240 const int rightMargin =
static_cast<int>(std::lround(TEXT_OFFSET_X * scaleX));
241 int x =
static_cast<int>(std::lround(400.0f * scaleX));
243 x = std::max(0,
static_cast<int>(
swapchain_extent.width) - textWidth - rightMargin);
247 static_cast<int>(std::lround(TEXT_OFFSET_Y * scaleY)),
248 SDL_Color{255, 255, 255, 255});
251 void resetTourFromMousePosition(
float mouseX,
float mouseY) {
253 int windowHeight = 0;
254 SDL_GetWindowSize(
getSDLWindow(), &windowWidth, &windowHeight);
255 if (windowWidth <= 0 || windowHeight <= 0) {
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);
266 std::srand(
static_cast<unsigned int>(std::time(
nullptr)));
271 void Tour::initializeBoard() {
272 board.resize(BOARD_SIZE, std::vector<int>(BOARD_SIZE, 0));
275 void Tour::clearBoard() {
276 for (
auto &row : board) {
277 std::fill(row.begin(), row.end(), 0);
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;
287 int Tour::getDegree(
const Position &position)
const {
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))) {
299 bool Tour::solveKnightsTour(Position position,
int moveCount) {
300 if (moveCount == TOTAL_MOVES) {
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);
312 std::sort(nextMoves.begin(), nextMoves.end(), [](
const auto &left,
const auto &right) {
313 return left.first < right.first;
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);
321 if (solveKnightsTour(nextPosition, moveCount + 1)) {
325 board[nextPosition.row][nextPosition.col] = 0;
326 moveSequence.pop_back();
332 resetTour(std::rand() % BOARD_SIZE, std::rand() % BOARD_SIZE);
336 if (startRow < 0 || startRow >= BOARD_SIZE || startCol < 0 || startCol >= BOARD_SIZE) {
341 knightPos = Position(startRow, startCol);
342 board[knightPos.row][knightPos.col] = 1;
343 moveSequence.clear();
344 moveSequence.push_back(knightPos);
345 solveKnightsTour(knightPos, 2);
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) {
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) {
368 if (tourOver ||
static_cast<std::size_t
>(moves) >= moveSequence.size()) {
372 const Position nextPosition = moveSequence[
static_cast<std::size_t
>(moves)];
373 board[knightPos.row][knightPos.col] = -1;
374 knightPos = nextPosition;
376 board[knightPos.row][knightPos.col] = moves;
377 tourOver =
static_cast<std::size_t
>(moves) == moveSequence.size();
381 for (
int row = 0; row < BOARD_SIZE; ++row) {
382 for (
int col = 0; col < BOARD_SIZE; ++col) {
384 if (board[row][col] == -1) {
386 }
else if ((row + col) % 2 == 0) {
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)));
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)));
410int main(
int argc,
char **argv) {
420 std::cerr << std::format(
"mxvk: Exception: {}\n", exception.
text());
423 std::cerr << std::format(
"mxvk: Argument Exception: {}\n", exception.
text());
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.
Exception thrown by Argz::proc() on unrecognised or malformed options.
void proc() override
Execute one processing/update step.
void event(SDL_Event &event) override
Handle one SDL event.
KnightsTourWindow(const std::string &path, int width, int height, bool fullscreen, bool enableVsync)
void drawKnight(mxvk::VK_Sprite &texture, float scaleX, float scaleY) const
void drawBoard(mxvk::VK_Sprite &whiteCell, mxvk::VK_Sprite &redCell, mxvk::VK_Sprite &visitedCell, float scaleX, float scaleY) const
void resetTourFromPoint(float x, float y)
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.
void loop()
Run the main event/render loop.
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.
VkExtent2D swapchain_extent
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
void saveSnapshot(const std::string &path)
Save the most recently rendered window contents as a PNG file.
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
void exit()
Request loop termination.
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
SDL3 joystick and gamepad RAII wrappers.
PNG image loading and saving utilities via SDL3.
Utilities for loading and saving PNG images.
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Plain data structure returned by proc_args() with all common libmx2 CLI options.
bool fullscreen
Whether fullscreen mode was requested.
bool resolutionSpecified
Whether -r/–resolution was provided.
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
int height
Viewport height in pixels (default: 720).
std::string path
Asset root; proc_args() defaults it to the executable directory.
int width
Viewport width in pixels (default: 1280).