22 TicTacToeWindow(
const std::string &assetPath,
int width,
int height,
bool fullscreen,
bool enable_vsync)
24 rng(std::random_device{}()) {
25 assetRoot = assetPath.empty() ? std::string(tictactoe_ASSET_DIR) : assetPath;
26 const std::string fontPath = assetPath.empty() ? std::string(tictactoe_FONT_PATH) : assetRoot +
"/data/font.ttf";
28 titleFont.reset(fontPath, 30);
29 uiFont.reset(fontPath, 18);
36 void event(SDL_Event &e)
override {
37 if (e.type == SDL_EVENT_KEY_DOWN) {
38 if (e.key.key == SDLK_ESCAPE) {
40 }
else if (e.key.key == SDLK_R) {
46 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
47 handleClick(e.button.x, e.button.y);
67 static constexpr int gridSize = 360;
68 static constexpr int cellSize = gridSize / 3;
69 static constexpr int lineThickness = 6;
70 static constexpr float pi = 3.14159265358979323846f;
72 std::array<char, 9> board{};
73 std::string assetRoot =
".";
74 mxvk::Font titleFont{};
76 mxvk::VK_Sprite *background =
nullptr;
77 mxvk::VK_Sprite *boardPixel =
nullptr;
78 mxvk::VK_Sprite *xPixel =
nullptr;
79 mxvk::VK_Sprite *oPixel =
nullptr;
81 Outcome outcome = Outcome::Running;
85 void makePixelSprite() {
86 boardPixel = makeSolidPixel(235, 240, 255, 255);
87 xPixel = makeSolidPixel(95, 205, 255, 255);
88 oPixel = makeSolidPixel(255, 140, 140, 255);
91 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
92 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
100 outcome = Outcome::Running;
103 void updateBoardLayout() {
106 boardX = std::max(24, (windowWidth - gridSize) / 2);
107 boardY = std::max(100, (windowHeight - gridSize) / 2 + 20);
110 void handleClick(
float windowMouseX,
float windowMouseY) {
111 if (outcome != Outcome::Running) {
117 const auto [mouseX, mouseY] = mouseToRenderCoordinates(windowMouseX, windowMouseY);
118 const int localX = mouseX - boardX;
119 const int localY = mouseY - boardY;
120 if (localX < 0 || localY < 0 || localX >= gridSize || localY >= gridSize) {
124 const int col = localX / cellSize;
125 const int row = localY / cellSize;
126 const int index = row * 3 + col;
127 if (board[
static_cast<std::size_t
>(index)] !=
' ') {
131 board[
static_cast<std::size_t
>(index)] =
'X';
133 if (outcome == Outcome::Running) {
139 std::pair<int, int> mouseToRenderCoordinates(
float mouseX,
float mouseY)
const {
140 int logicalWidth = 0;
141 int logicalHeight = 0;
144 SDL_GetWindowSize(
getSDLWindow(), &logicalWidth, &logicalHeight);
145 SDL_GetWindowSizeInPixels(
getSDLWindow(), &pixelWidth, &pixelHeight);
147 if (logicalWidth <= 0 || logicalHeight <= 0 || pixelWidth <= 0 || pixelHeight <= 0 ||
149 return {
static_cast<int>(std::lround(mouseX)),
static_cast<int>(std::lround(mouseY))};
152 const bool mouseLooksLogical =
153 mouseX >= 0.0f && mouseY >= 0.0f &&
154 mouseX <= static_cast<float>(logicalWidth) + 0.5f &&
155 mouseY <= static_cast<float>(logicalHeight) + 0.5f;
157 if (mouseLooksLogical && (pixelWidth != logicalWidth || pixelHeight != logicalHeight)) {
158 mouseX *=
static_cast<float>(
swapchain_extent.width) /
static_cast<float>(logicalWidth);
159 mouseY *=
static_cast<float>(
swapchain_extent.height) /
static_cast<float>(logicalHeight);
162 return {
static_cast<int>(std::lround(mouseX)),
static_cast<int>(std::lround(mouseY))};
165 void makeComputerMove() {
166 if (playImmediateMove(
'O')) {
169 if (playImmediateMove(
'X')) {
172 if (board[4] ==
' ') {
177 std::vector<int> openCells;
178 for (
int i = 0; i < static_cast<int>(board.size()); ++i) {
179 if (board[
static_cast<std::size_t
>(i)] ==
' ') {
180 openCells.push_back(i);
184 if (!openCells.empty()) {
185 std::shuffle(openCells.begin(), openCells.end(), rng);
186 board[
static_cast<std::size_t
>(openCells.front())] =
'O';
190 bool playImmediateMove(
char side) {
191 for (
int i = 0; i < static_cast<int>(board.size()); ++i) {
192 if (board[
static_cast<std::size_t
>(i)] !=
' ') {
196 board[
static_cast<std::size_t
>(i)] = side;
197 const bool completesLine = winner() == side;
198 board[
static_cast<std::size_t
>(i)] =
' ';
200 board[
static_cast<std::size_t
>(i)] =
'O';
207 void updateOutcome() {
208 const char winningSide = winner();
209 if (winningSide ==
'X') {
210 outcome = Outcome::UserWon;
211 }
else if (winningSide ==
'O') {
212 outcome = Outcome::ComputerWon;
213 }
else if (std::ranges::none_of(board, [](
char c) {
return c ==
' '; })) {
214 outcome = Outcome::Draw;
218 char winner()
const {
219 static constexpr std::array<std::array<int, 3>, 8> lines{{
230 for (
const auto &line : lines) {
231 const char first = board[
static_cast<std::size_t
>(line[0])];
233 first == board[
static_cast<std::size_t
>(line[1])] &&
234 first == board[
static_cast<std::size_t
>(line[2])]) {
242 if (boardPixel ==
nullptr) {
246 const int boardEnd = boardX + gridSize;
247 for (
int i = 1; i <= 2; ++i) {
248 const int pos = boardX + i * cellSize - lineThickness / 2;
249 boardPixel->drawSpriteRect(pos, boardY, lineThickness, gridSize);
250 boardPixel->drawSpriteRect(boardX, boardY + i * cellSize - lineThickness / 2, gridSize, lineThickness);
253 boardPixel->drawSpriteRect(boardX - lineThickness, boardY - lineThickness, gridSize + lineThickness * 2, lineThickness);
254 boardPixel->drawSpriteRect(boardX - lineThickness, boardY + gridSize, gridSize + lineThickness * 2, lineThickness);
255 boardPixel->drawSpriteRect(boardX - lineThickness, boardY, lineThickness, gridSize);
256 boardPixel->drawSpriteRect(boardEnd, boardY, lineThickness, gridSize);
259 void drawBackground() {
260 if (background ==
nullptr) {
267 if (xPixel ==
nullptr || oPixel ==
nullptr) {
271 const int markSize = std::max(24, cellSize * 58 / 100);
272 const int thickness = std::max(4, cellSize / 18);
274 for (
int row = 0; row < 3; ++row) {
275 for (
int col = 0; col < 3; ++col) {
276 const int index = row * 3 + col;
277 const char mark = board[
static_cast<std::size_t
>(index)];
282 const int centerX = boardX + col * cellSize + cellSize / 2;
283 const int centerY = boardY + row * cellSize + cellSize / 2;
286 drawX(centerX, centerY, markSize, thickness);
288 drawO(centerX, centerY, markSize, thickness);
294 void drawX(
int centerX,
int centerY,
int size,
int thickness) {
295 const int half = size / 2;
296 drawLine(*xPixel, centerX - half, centerY - half, centerX + half, centerY + half, thickness);
297 drawLine(*xPixel, centerX + half, centerY - half, centerX - half, centerY + half, thickness);
300 void drawO(
int centerX,
int centerY,
int size,
int thickness) {
301 const int radius = size / 2;
302 constexpr int segments = 96;
303 int previousX = centerX + radius;
304 int previousY = centerY;
306 for (
int i = 1; i <= segments; ++i) {
307 const float angle = (
static_cast<float>(i) /
static_cast<float>(segments)) * 2.0f * pi;
308 const int x = centerX +
static_cast<int>(std::lround(std::cos(angle) *
static_cast<float>(radius)));
309 const int y = centerY +
static_cast<int>(std::lround(std::sin(angle) *
static_cast<float>(radius)));
310 drawLine(*oPixel, previousX, previousY, x, y, thickness);
316 void drawLine(mxvk::VK_Sprite &sprite,
int x0,
int y0,
int x1,
int y1,
int thickness) {
317 const int dx = x1 - x0;
318 const int dy = y1 - y0;
319 const int steps = std::max(std::abs(dx), std::abs(dy));
321 drawDot(sprite, x0, y0, thickness);
325 for (
int i = 0; i <= steps; ++i) {
326 const float t =
static_cast<float>(i) /
static_cast<float>(steps);
327 const int x =
static_cast<int>(std::lround(
static_cast<float>(x0) +
static_cast<float>(dx) * t));
328 const int y =
static_cast<int>(std::lround(
static_cast<float>(y0) +
static_cast<float>(dy) * t));
329 drawDot(sprite, x, y, thickness);
333 void drawDot(mxvk::VK_Sprite &sprite,
int x,
int y,
int size) {
338 printText(
"Tic-Tac-Toe", boardX, 30, SDL_Color{235, 240, 255, 255}, titleFont);
339 printText(statusText(), boardX, boardY - 135, SDL_Color{190, 210, 255, 255}, uiFont);
340 printText(
"Click a square. R resets. Esc quits.", boardX, boardY + gridSize + 18, SDL_Color{180, 185, 200, 255}, uiFont);
343 std::string statusText()
const {
345 case Outcome::Running:
346 return "You are X. The computer is O.";
347 case Outcome::UserWon:
348 return "You won. Click anywhere to play again.";
349 case Outcome::ComputerWon:
350 return "Computer won. Click anywhere to play again.";
352 return "Draw. Click anywhere to play again.";