I wrote a Tic-Tac-Toe game that unlike the normal TTT only saves the last 3 moves. That means, that the first move will be removed on the 4th move and so on.
To not necessarily extend the description I do not post my full requirements. The important requirements to understand the code for review are:
- It must be responsive for FHD to UHD desktop monitor screens either in landscape or portrait mode.
- It must not support screen readers. In the reference, partial hints were added to introduce accessibility in a later lesson.
- It must support Chrome and Firefox with updates of 1st of June 2024 or newer.
- Code must be readable enough to be understandable without comments
- Code must be updatable to support later addition of other game options (like adding a chess clock)
My requests for review are:
- Is the code readable
- Can conventions be recognized (Students know what conventions are and should spot at least some of them)
- HTML semantics
- simplicity
- no unnecessary repetitive code
const BUTTON_LABELS = [
'Oben Links',
'Oben Mitte',
'Oben Rechts',
'Mitte Links',
'Zentrum',
'Mitte Rechts',
'Unten Links',
'Unten Mitte',
'Unten Rechts'
];
const PLAYERS = ['X', 'O'];
const WIN_PATTERN = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
let moves = {
X: [],
O: []
};
let turn;
window.addEventListener('DOMContentLoaded', init);
function init() {
randomPlayer();
BUTTON_LABELS.forEach((label, index) => {
const BUTTON = document.createElement('button');
BUTTON.addEventListener('click', move);
BUTTON.ariaLabel = label;
BUTTON.dataset.id = index;
BOARD.appendChild(BUTTON);
})
announceTurn();
NEW_GAME.addEventListener('click', resetGame);
}
function randomPlayer() {
turn = Math.floor(Math.random() * PLAYERS.length)
}
function move() {
if (!this.textContent.length) {
this.textContent = PLAYERS[turn];
this.classList.add(PLAYERS[turn]);
saveMove(this.dataset.id);
}
}
function saveMove(dataId) {
moves[PLAYERS[turn]].push(dataId);
deleteMove([PLAYERS[turn]]);
}
function deleteMove(PLAYER) {
while (moves[PLAYER].length > 3) {
const FIELD = document.querySelector(`[data-id="${moves[PLAYER][0]}"]`);
FIELD.className = '';
FIELD.textContent = '';
moves[PLAYER].shift();
}
checkForWinner();
}
function checkForWinner() {
let winner = victoryCheck();
if (winner) {
announceWinner(winner);
lockBoard();
} else {
switchPlayer();
}
}
function victoryCheck() {
let winner = false;
WIN_PATTERN.forEach(pattern => {
let line = [];
pattern.forEach(field => line.push(document.querySelector(`[data-id="${field}"]`).textContent));
if (line[0].length && line.every(val => val === line[0])) {
winner = line[0];
}
});
return winner;
}
function announceWinner(player) {
document.querySelectorAll(`.${player}`).forEach(btn => btn.classList.add('winner'));
NOTICE.textContent = `Spieler ${player} gewinnt!`;
NOTICE.className = `${player}`;
}
function lockBoard() {
BOARD.dataset.turn = 'none';
document.querySelectorAll('#BOARD button').forEach(btn => btn.disabled = true);
}
function switchPlayer() {
turn = (turn + 1) % PLAYERS.length;
announceTurn();
}
function announceTurn() {
let player = PLAYERS[turn];
BOARD.dataset.turn = player;
NOTICE.textContent = `Spieler ${player} ist am Zug.`;
NOTICE.className = `${player}`;
}
function resetGame() {
randomPlayer();
const BUTTONS = document.querySelectorAll('#BOARD button');
BUTTONS.forEach(btn => {
btn.removeAttribute('class');
btn.disabled = false;
btn.textContent = '';
})
for (let key in moves) {
moves[key] = [];
}
announceTurn();
}
:root {
--blue: rgb(40, 100, 140);
--border-thickness: 0.2rem;
--border-radius: 0.4rem;
--gray: rgb(85, 85, 85);
--green: rgb(100, 179, 43);
--red: rgb(222, 39, 79);
}
* {
box-sizing: border-box;
color: var(--gray);
font-family: Arial, Helvetica, sans-serif;
}
body {
align-items: center;
display: flex;
flex-direction: column;
height: 100vh;
justify-content: space-between;
margin: 0;
padding: 0.5rem;
}
main {
display: flex;
flex-direction: column;
gap: 1em;
}
button {
border: var(--border-thickness) solid var(--gray);
border-radius: var(--border-radius);
background-color: white;
color: var(--gray);
&:hover {
border-color: var(--green);
color: var(--green);
cursor: pointer;
}
}
#NOTICE {
font-size: 2em;
text-align: center;
&.X {
color: var(--blue);
}
&.O {
color: var(--red);
}
}
#BOARD {
--highlight: var(--gray);
display: grid;
gap: 0.5em;
grid-template-columns: repeat(3, 20vmin);
&[data-turn="X"] {
--highlight: var(--blue)
}
&[data-turn="O"] {
--highlight: var(--red)
}
button {
align-items: center;
aspect-ratio: 1;
display: flex;
font-size: 15vmin;
justify-content: center;
&.X {
border-color: var(--blue);
color: var(--blue);
}
&.O {
border-color: var(--red);
color: var(--red);
}
&.winner {
animation: 0.8s linear infinite alternate winner;
border-color: var(--green);
color: var(--green);
}
&:not([class]):hover {
border-color: var(--highlight);
}
&:disabled:hover {
cursor: not-allowed;
}
}
}
@keyframes winner {
from {
opacity: 1;
}
to {
opacity: 0.4;
}
}
menu {
padding: 0;
text-align: center;
}
#NEW_GAME {
font-size: 2em;
margin: 0;
padding: 0.5em 1em;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Tic Tac Toe Advanced</title>
<link rel="stylesheet" href="tictactoe.css">
<script src="tictactoe.js"></script>
</head>
<body>
<header>
<h1>Tic Tac Toe Advanced</h1>
</header>
<main>
<output id="NOTICE"></output>
<div id="BOARD"></div>
<menu>
<button id="NEW_GAME">Neues Spiel</button>
</menu>
</main>
<footer>
© 2024 - Name des Autors
</footer>
</body>
</html>