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) :
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()) {
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");
82 if (joystick.open(0)) {
83 std::cout << std::format(
"Joystick opened: {}\n", joystick.name());
85 std::cout <<
"Could not open joystick..\n";
91 if (
event.type == SDL_EVENT_KEY_DOWN) {
92 if (
event.key.key == SDLK_ESCAPE) {
96 if (
event.key.key == SDLK_S && !
event.key.repeat) {
99 std::cout <<
"mx: Screenshot captured..\n";
101 std::cerr << std::format(
"mxvk: screenshot failed: {}\n", exception.
text());
105 if (screen != Screen::Tour) {
108 if (
event.key.key == SDLK_SPACE) {
110 }
else if (
event.key.key == SDLK_RETURN && !
event.key.repeat) {
116 if (screen != Screen::Tour) {
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) {
126 }
else if (
event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN) {
127 if (
event.jbutton.button == 1) {
129 }
else if (
event.jbutton.button == 2) {
139 if (screen == Screen::Intro) {
145 tour.drawBoard(*whiteCell, *redCell, *visitedCell, scaleX, scaleY);
146 tour.drawKnight(*knightSprite, scaleX, scaleY);
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);
152 printScaledText(
"-[ Tour Complete ]- Press Return to Reset", TEXT_OFFSET_X, TEXT_OFFSET_Y, scaleX, scaleY);
157 enum class Screen { Intro,
Tour };
167 std::string assetRoot;
168 std::string fontPath;
177 Uint64 introStarted = 0;
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};
187 void loadWindowIcon() {
188 const char *videoDriver = SDL_GetCurrentVideoDriver();
189 if (videoDriver !=
nullptr && std::string(videoDriver) ==
"wayland") {
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());
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);
204 screen = Screen::Tour;
208 intro->setShaderParams(
static_cast<float>(alpha) / 255.0f);
212 void updateFont(
float scaleY) {
213 const int desiredSize = std::max(1,
static_cast<int>(std::lround(TEXT_SIZE * scaleY)));
214 if (desiredSize == currentFontSize) {
217 setFont(fontPath, desiredSize);
218 currentFontSize = desiredSize;
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}); }
223 void printMoveCount(
float scaleX,
float scaleY) {
224 const std::string text = std::format(
"Moves: {}", tour.getMoves());
227 const int rightMargin =
static_cast<int>(std::lround(TEXT_OFFSET_X * scaleX));
228 int x =
static_cast<int>(std::lround(400.0f * scaleX));
230 x = std::max(0,
static_cast<int>(
swapchain_extent.width) - textWidth - rightMargin);
232 printText(text, x,
static_cast<int>(std::lround(TEXT_OFFSET_Y * scaleY)), SDL_Color{255, 255, 255, 255});
235 void resetTourFromMousePosition(
float mouseX,
float mouseY) {
237 int windowHeight = 0;
238 SDL_GetWindowSize(
getSDLWindow(), &windowWidth, &windowHeight);
239 if (windowWidth <= 0 || windowHeight <= 0) {
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);
250 std::srand(
static_cast<unsigned int>(std::time(
nullptr)));
257 void Tour::clearBoard() {
258 for (
auto &row :
board) {
259 std::fill(row.begin(), row.end(), 0);
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; }
265 int Tour::getDegree(
const Position &position)
const {
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))) {
277 bool Tour::solveKnightsTour(
Position position,
int moveCount) {
278 if (moveCount == TOTAL_MOVES) {
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);
290 std::sort(nextMoves.begin(), nextMoves.end(), [](
const auto &left,
const auto &right) { return left.first < right.first; });
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);
297 if (solveKnightsTour(nextPosition, moveCount + 1)) {
301 board[nextPosition.row][nextPosition.col] = 0;
302 moveSequence.pop_back();
310 if (startRow < 0 || startRow >= BOARD_SIZE || startCol < 0 || startCol >= BOARD_SIZE) {
315 knightPos = Position(startRow, startCol);
316 board[knightPos.row][knightPos.col] = 1;
317 moveSequence.clear();
318 moveSequence.push_back(knightPos);
319 solveKnightsTour(knightPos, 2);
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) {
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) {
341 if (tourOver ||
static_cast<std::size_t
>(moves) >= moveSequence.size()) {
345 const Position nextPosition = moveSequence[
static_cast<std::size_t
>(moves)];
346 board[knightPos.row][knightPos.col] = -1;
347 knightPos = nextPosition;
349 board[knightPos.row][knightPos.col] = moves;
350 tourOver =
static_cast<std::size_t
>(moves) == moveSequence.size();
354 for (
int row = 0; row < BOARD_SIZE; ++row) {
355 for (
int col = 0; col < BOARD_SIZE; ++col) {
357 if (board[row][col] == -1) {
359 }
else if ((row + col) % 2 == 0) {
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)));
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))); }
373int main(
int argc,
char **argv) {
383 std::cerr << std::format(
"mxvk: Exception: {}\n", exception.
text());
386 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).