22 TicTacToeWindow(
const std::string &assetPath,
int width,
int height,
bool fullscreen,
bool enable_vsync) :
mxvk::
VK_Window(
"-[ MXVK Tic-Tac-Toe ]-", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), rng(std::random_device{}()) {
23 assetRoot = assetPath.empty() ? std::string(tictactoe_ASSET_DIR) : assetPath;
24 const std::string fontPath = assetPath.empty() ? std::string(tictactoe_FONT_PATH) : assetRoot +
"/data/font.ttf";
26 titleFont.reset(fontPath, 30);
27 uiFont.reset(fontPath, 18);
34 void event(SDL_Event &e)
override {
35 if (e.type == SDL_EVENT_KEY_DOWN) {
36 if (e.key.key == SDLK_ESCAPE) {
38 }
else if (e.key.key == SDLK_R) {
44 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
45 handleClick(e.button.x, e.button.y);
58 enum class Outcome { Running, UserWon, ComputerWon, Draw };
60 static constexpr int gridSize = 360;
61 static constexpr int cellSize = gridSize / 3;
62 static constexpr int lineThickness = 6;
63 static constexpr float pi = 3.14159265358979323846f;
65 std::array<char, 9> board{};
66 std::string assetRoot =
".";
67 mxvk::Font titleFont{};
69 mxvk::VK_Sprite *background =
nullptr;
70 mxvk::VK_Sprite *boardPixel =
nullptr;
71 mxvk::VK_Sprite *xPixel =
nullptr;
72 mxvk::VK_Sprite *oPixel =
nullptr;
74 Outcome outcome = Outcome::Running;
78 void makePixelSprite() {
79 boardPixel = makeSolidPixel(235, 240, 255, 255);
80 xPixel = makeSolidPixel(95, 205, 255, 255);
81 oPixel = makeSolidPixel(255, 140, 140, 255);
84 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
85 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
93 outcome = Outcome::Running;
96 void updateBoardLayout() {
99 boardX = std::max(24, (windowWidth - gridSize) / 2);
100 boardY = std::max(100, (windowHeight - gridSize) / 2 + 20);
103 void handleClick(
float windowMouseX,
float windowMouseY) {
104 if (outcome != Outcome::Running) {
110 const auto [mouseX, mouseY] = mouseToRenderCoordinates(windowMouseX, windowMouseY);
111 const int localX = mouseX - boardX;
112 const int localY = mouseY - boardY;
113 if (localX < 0 || localY < 0 || localX >= gridSize || localY >= gridSize) {
117 const int col = localX / cellSize;
118 const int row = localY / cellSize;
119 const int index = row * 3 + col;
120 if (board[
static_cast<std::size_t
>(index)] !=
' ') {
124 board[
static_cast<std::size_t
>(index)] =
'X';
126 if (outcome == Outcome::Running) {
132 std::pair<int, int> mouseToRenderCoordinates(
float mouseX,
float mouseY)
const {
133 int logicalWidth = 0;
134 int logicalHeight = 0;
137 SDL_GetWindowSize(
getSDLWindow(), &logicalWidth, &logicalHeight);
138 SDL_GetWindowSizeInPixels(
getSDLWindow(), &pixelWidth, &pixelHeight);
141 return {
static_cast<int>(std::lround(mouseX)),
static_cast<int>(std::lround(mouseY))};
144 const bool mouseLooksLogical = mouseX >= 0.0f && mouseY >= 0.0f && mouseX <= static_cast<float>(logicalWidth) + 0.5f && mouseY <= static_cast<float>(logicalHeight) + 0.5f;
146 if (mouseLooksLogical && (pixelWidth != logicalWidth || pixelHeight != logicalHeight)) {
147 mouseX *=
static_cast<float>(
swapchain_extent.width) /
static_cast<float>(logicalWidth);
148 mouseY *=
static_cast<float>(
swapchain_extent.height) /
static_cast<float>(logicalHeight);
151 return {
static_cast<int>(std::lround(mouseX)),
static_cast<int>(std::lround(mouseY))};
154 void makeComputerMove() {
155 if (playImmediateMove(
'O')) {
158 if (playImmediateMove(
'X')) {
161 if (board[4] ==
' ') {
166 std::vector<int> openCells;
167 for (
int i = 0; i < static_cast<int>(board.size()); ++i) {
168 if (board[
static_cast<std::size_t
>(i)] ==
' ') {
169 openCells.push_back(i);
173 if (!openCells.empty()) {
174 std::shuffle(openCells.begin(), openCells.end(), rng);
175 board[
static_cast<std::size_t
>(openCells.front())] =
'O';
179 bool playImmediateMove(
char side) {
180 for (
int i = 0; i < static_cast<int>(board.size()); ++i) {
181 if (board[
static_cast<std::size_t
>(i)] !=
' ') {
185 board[
static_cast<std::size_t
>(i)] = side;
186 const bool completesLine = winner() == side;
187 board[
static_cast<std::size_t
>(i)] =
' ';
189 board[
static_cast<std::size_t
>(i)] =
'O';
196 void updateOutcome() {
197 const char winningSide = winner();
198 if (winningSide ==
'X') {
199 outcome = Outcome::UserWon;
200 }
else if (winningSide ==
'O') {
201 outcome = Outcome::ComputerWon;
202 }
else if (std::ranges::none_of(board, [](
char c) {
return c ==
' '; })) {
203 outcome = Outcome::Draw;
207 char winner()
const {
208 static constexpr std::array<std::array<int, 3>, 8> lines{{
219 for (
const auto &line : lines) {
220 const char first = board[
static_cast<std::size_t
>(line[0])];
221 if (first !=
' ' && first == board[
static_cast<std::size_t
>(line[1])] && first == board[
static_cast<std::size_t
>(line[2])]) {
229 if (boardPixel ==
nullptr) {
233 const int boardEnd = boardX + gridSize;
234 for (
int i = 1; i <= 2; ++i) {
235 const int pos = boardX + i * cellSize - lineThickness / 2;
236 boardPixel->drawSpriteRect(pos, boardY, lineThickness, gridSize);
237 boardPixel->drawSpriteRect(boardX, boardY + i * cellSize - lineThickness / 2, gridSize, lineThickness);
240 boardPixel->drawSpriteRect(boardX - lineThickness, boardY - lineThickness, gridSize + lineThickness * 2, lineThickness);
241 boardPixel->drawSpriteRect(boardX - lineThickness, boardY + gridSize, gridSize + lineThickness * 2, lineThickness);
242 boardPixel->drawSpriteRect(boardX - lineThickness, boardY, lineThickness, gridSize);
243 boardPixel->drawSpriteRect(boardEnd, boardY, lineThickness, gridSize);
246 void drawBackground() {
247 if (background ==
nullptr) {
254 if (xPixel ==
nullptr || oPixel ==
nullptr) {
258 const int markSize = std::max(24, cellSize * 58 / 100);
259 const int thickness = std::max(4, cellSize / 18);
261 for (
int row = 0; row < 3; ++row) {
262 for (
int col = 0; col < 3; ++col) {
263 const int index = row * 3 + col;
264 const char mark = board[
static_cast<std::size_t
>(index)];
269 const int centerX = boardX + col * cellSize + cellSize / 2;
270 const int centerY = boardY + row * cellSize + cellSize / 2;
273 drawX(centerX, centerY, markSize, thickness);
275 drawO(centerX, centerY, markSize, thickness);
281 void drawX(
int centerX,
int centerY,
int size,
int thickness) {
282 const int half = size / 2;
283 drawLine(*xPixel, centerX - half, centerY - half, centerX + half, centerY + half, thickness);
284 drawLine(*xPixel, centerX + half, centerY - half, centerX - half, centerY + half, thickness);
287 void drawO(
int centerX,
int centerY,
int size,
int thickness) {
288 const int radius = size / 2;
289 constexpr int segments = 96;
290 int previousX = centerX + radius;
291 int previousY = centerY;
293 for (
int i = 1; i <= segments; ++i) {
294 const float angle = (
static_cast<float>(i) /
static_cast<float>(segments)) * 2.0f * pi;
295 const int x = centerX +
static_cast<int>(std::lround(std::cos(angle) *
static_cast<float>(radius)));
296 const int y = centerY +
static_cast<int>(std::lround(std::sin(angle) *
static_cast<float>(radius)));
297 drawLine(*oPixel, previousX, previousY, x, y, thickness);
303 void drawLine(mxvk::VK_Sprite &sprite,
int x0,
int y0,
int x1,
int y1,
int thickness) {
304 const int dx = x1 - x0;
305 const int dy = y1 - y0;
306 const int steps = std::max(std::abs(dx), std::abs(dy));
308 drawDot(sprite, x0, y0, thickness);
312 for (
int i = 0; i <= steps; ++i) {
313 const float t =
static_cast<float>(i) /
static_cast<float>(steps);
314 const int x =
static_cast<int>(std::lround(
static_cast<float>(x0) +
static_cast<float>(dx) * t));
315 const int y =
static_cast<int>(std::lround(
static_cast<float>(y0) +
static_cast<float>(dy) * t));
316 drawDot(sprite, x, y, thickness);
320 void drawDot(mxvk::VK_Sprite &sprite,
int x,
int y,
int size) { sprite.
drawSpriteRect(x - size / 2, y - size / 2, size, size); }
323 printText(
"Tic-Tac-Toe", boardX, 30, SDL_Color{235, 240, 255, 255}, titleFont);
324 printText(statusText(), boardX, boardY - 135, SDL_Color{190, 210, 255, 255}, uiFont);
325 printText(
"Click a square. R resets. Esc quits.", boardX, boardY + gridSize + 18, SDL_Color{180, 185, 200, 255}, uiFont);
328 std::string statusText()
const {
330 case Outcome::Running:
331 return "You are X. The computer is O.";
332 case Outcome::UserWon:
333 return "You won. Click anywhere to play again.";
334 case Outcome::ComputerWon:
335 return "Computer won. Click anywhere to play again.";
337 return "Draw. Click anywhere to play again.";