final int NONE = 0; final int CROSS = 1; final int CIRCLE = 2; final int SPLASHSCREEN = 0; final int PLAYING = 1; final int GAMEOVER = 2; final int[][] WINNING_COMBINATIONS = { { 0, 1, 2 }, // first row { 3, 4, 5 }, // second row { 6, 7, 8 }, // third row { 0, 3, 6 }, // first column { 1, 4, 7 }, // second column { 2, 5, 8 }, // third column { 0, 4, 8 }, // first diagonal { 2, 4, 6 } // second diagonal }; int[] game = { NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE }; int currentPlayer = NONE; int[] winnerIndices = null; boolean gameIsDraw = false; String highScoreText = ""; int gameState = SPLASHSCREEN; int size; int xOffset; int yOffset; int cellSize; PFont headingFont; PFont textFont; void setup() { size(500, 500); headingFont = createFont("Liberation Sans Bold", 32); textFont = createFont("Liberation Sans", 16); size = min(width, height); xOffset = (width - size) / 2; yOffset = (height - size) / 2; cellSize = size / 3; rectMode(CENTER); ellipseMode(CENTER); textAlign(CENTER, CENTER); } void draw() { switch (gameState) { case SPLASHSCREEN: background(#000000); fill(#FFFFFF); textFont(headingFont); text("TikTakToe", width / 2, height / 4); textFont(textFont); text("A two-player game\n\nPress any key or click anywhere to start playing\n\n© 2022 Christian Weber", width / 2, height / 2); break; case PLAYING: background(#CCCCCC); checkWinner(); drawWinnerIndices(); drawBoard(); drawPlayerMarkers(); break; case GAMEOVER: background(#CCCCCC); drawWinnerIndices(); drawBoard(); drawPlayerMarkers(); fill(#77000000); noStroke(); rect(xOffset, yOffset, 6 * cellSize, 6 * cellSize); fill(#FFFFFF); textFont(headingFont); text("Game Over", width / 2, height / 4); textFont(textFont); String text = "The player using " + (currentPlayer == CROSS ? "crosses" : "circles") + " has won the game."; if (gameIsDraw) { text = "The game is a draw."; } text(text + "\n\nPress any key or click anywhere to play again." + highScoreText, width / 2, height / 2); break; } } void drawBoard() { stroke(#000000); strokeWeight(5); line(xOffset + cellSize, yOffset, xOffset + cellSize, height - yOffset); line(xOffset + 2*cellSize, yOffset, xOffset + 2*cellSize, height - yOffset); line(xOffset, yOffset + cellSize, width - xOffset, yOffset + cellSize); line(xOffset, yOffset + 2*cellSize, width - xOffset, yOffset + 2*cellSize); } void drawWinnerIndices() { if (winnerIndices == null || winnerIndices.length <= 0) return; for (int i = 0; i < winnerIndices.length; i++) { int x = winnerIndices[i] % 3; int y = winnerIndices[i] / 3; noStroke(); fill(#AACCEE); rect(xOffset + x * cellSize + cellSize / 2, yOffset + y * cellSize + cellSize / 2, cellSize, cellSize); } } void drawPlayerMarkers() { for (int i = 0; i < game.length; i++) { int x = i % 3; int y = i / 3; int marker = game[i]; noFill(); strokeWeight(5); stroke(#000000); if (marker == CROSS) { line(xOffset + x * cellSize + cellSize / 6, yOffset + y * cellSize + cellSize / 6, xOffset + (x + 1) * cellSize - cellSize / 6, yOffset + (y + 1) * cellSize - cellSize / 6); line(xOffset + (x + 1) * cellSize - cellSize / 6, yOffset + y * cellSize + cellSize / 6, xOffset + x * cellSize + cellSize / 6, yOffset + (y + 1) * cellSize - cellSize / 6); } else if (marker == CIRCLE) { ellipse(xOffset + x * cellSize + cellSize / 2, yOffset + y * cellSize + cellSize / 2, 2*cellSize / 3, 2*cellSize / 3); } } } void checkWinner() { for (int[] indices : WINNING_COMBINATIONS) { int firstMarker = -1; boolean failed = false; for (int i = 0; i < indices.length; i++) { int marker = game[indices[i]]; if (firstMarker == -1) { firstMarker = marker; } else if (firstMarker != marker) { failed = true; } } if ((firstMarker == CROSS || firstMarker == CIRCLE) && !failed) { endGame(indices, firstMarker, false); return; } } int usedCells = 0; for (int i = 0; i < game.length; i++) { if (game[i] > NONE) { usedCells++; } } if (usedCells == game.length) { endGame(null, NONE, true); } } void endGame(int[] indices, int marker, boolean isDraw) { winnerIndices = indices; gameState = GAMEOVER; gameIsDraw = isDraw; StringList highScores = new StringList(); if (dataFile("highscores.txt").isFile()) { highScores.append(loadStrings("data/highscores.txt")); } String date = nf(year(), 4) + "-" + nf(month(), 2) + "-" + nf(day(), 2) + " " + nf(hour(), 2) + ":" + nf(minute(), 2) + ":" + nf(second(), 2); String text = isDraw ? "draw" : ((marker == CROSS ? "cross" : "circle") + "@" + join(nf(indices, 1), ",")); highScores.append(date + " / " + text); saveStrings("data/highscores.txt", highScores.array()); if (highScores.size() > 0) { int crossWins = 0; int circleWins = 0; int draw = 0; for (String s : highScores) { String winner = s.substring(22); if (winner.indexOf("@") > 0) { winner = winner.substring(0, winner.indexOf("@")); } if (winner.equals("cross")) crossWins++; else if (winner.equals("circle")) circleWins++; else if (winner.equals("draw")) draw++; } highScoreText = "\n\nHighScores:\nCross: " + crossWins + "\nCircle: " + circleWins + "\nDraw: " + draw; } } void startGame() { gameState = PLAYING; gameIsDraw = false; currentPlayer = NONE; game = new int[] { NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE }; winnerIndices = null; } void keyPressed() { switch (gameState) { case PLAYING: break; case GAMEOVER: case SPLASHSCREEN: startGame(); break; } } void mouseClicked() { switch (gameState) { case SPLASHSCREEN: case GAMEOVER: startGame(); break; case PLAYING: int x = (mouseX - xOffset) / cellSize; int y = (mouseY - yOffset) / cellSize; int idx = y * 3 + x; if (game[idx] != NONE) break; if (currentPlayer == NONE || currentPlayer == CIRCLE) { currentPlayer = CROSS; } else { currentPlayer = CIRCLE; } game[idx] = currentPlayer; break; } }