Programmieren lernen mit Spaß: Tic-Tac-Toe mit JavaScript, HTML und CSS
Lerne, wie du mit HTML, CSS und JavaScript ein einfaches Tic-Tac-Toe-Spiel erstellst. Dieses Tutorial führt dich Schritt für Schritt durch die Spiellogik, das Event-Handling mit JavaScript und den Aufbau des HTML-Templates.
Aufbau der HTML-Struktur
Um die Grundlage unseres Tic Tac Toe-Spiels zu schaffen, beginnen wir mit einerm einfachen HTML-Template , welches nur zwei Elemente enthält: das Spielbrett und eine Anzeige für den Spielerzug. Der Kürze halber haben wir alle nicht wesentlichen Teile weggelassen:
<div id="board"></div>
<p id="player-turn">Player X's turn</p>
<script src="index.js"></script>
- Spielfeld (
<div id="board">
): Dieses Div fungiert als Container für die neun Spielzellen, die dynamisch über JavaScript hinzugefügt werden. - Spielstandanzeiger (
<p id="player-turn">
): Diesesp
-Absatzelement zeigt den aktuellen Zug des Spielers an. - Die JavaScript-Datei (
index.js
) beinhaltet die Spiel-Logik. Dasscript
-Tag wird innerhalb des<body>
-Elements (am Ende) platziert, um sicherzustellen, dass alle HTML-Elemente, auf die der JavaScript-Code verweist, bereits definiert sind, wenn das Skript ausgeführt wird.
Styling mit CSS
Wie bereits erwähnt, werden im <div id="board">
per JavaScript neun div
-Elemente für die einzelnen Spielfelder eingefügt.
Doch wie entsteht daraus ein 3×3-Spielbrett? Hier kommt das CSS-Grid-Layout ins Spiel – mit display: grid
und grid-template-columns
lassen sich die Zellen wie in einer Tabelle anordnen.
#board {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 0;
width: 300px;
height: 300px;
border-top: 1px solid black;
border-right: 1px solid black;
padding: 0;
}
#board > div {
background-color: white;
border-left: 1px solid black;
border-bottom: 1px solid black;
font-size: 40px;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: calc(300px / 3);
height: calc(300px / 3);
box-sizing: border-box;
}
Das Grid-Layout display: grid
zusammen mit grid-template-columns: repeat(3, 1fr)
erstellt ein strukturiertes Layout mit 3 Spalten. Da wir insgesamt 9 div
-Elemente verwenden, ergibt sich daraus ein 3x3 Spielbrett.
Der Wert 1fr
steht für "fraction of the available space" - Bruchteil des verfügbaren Platzes - allerdings setzen wir später für die einzelnen div
-s die Breite explizit auf calc(300px / 3)
(das kann man noch bestimmt eleganter lösen).
Die Zellen haben an der linken und unteren Seite eine Rahmenlinie (border), während das Spielfeld selbst an der oberen und rechten Seite eine besitzt. So entsteht ein vollständiges Gitter.
Dank box-sizing: border-box
wird die Rahmenbreite in die Gesamtgröße der Elemente einberechnet. Dadurch entfällt die Notwendigkeit, das Spielfeld manuell anzupassen
(z. B. es auf 302 px statt 300 px zu setzen - jeweils 2 Mal 1px für den Rahmen).
Initialisierung des Spielbretts mit JavaScript
JavaScript wird verwendet, um das Spielbrett zu generieren und Interaktiv zu machen.
const board = document.getElementById('board');
const boardCells = [];
for (let i = 0; i < 9; i++) {
const cell = document.createElement('div');
cell.textContent = '';
cell.addEventListener('click', handleCellClick);
cell.setAttribute('data-index', i);
board.appendChild(cell);
boardCells.push(cell);
}
Es werden dynamisch neun <div>
-Elemente erzeugt, die die Spielfelder darstellen. Diese werden im Array boardCells
gespeichert, um später leicht darauf zugreifen zu können.
Jede Zelle erhält einen Click-Event
-Listener, der die Funktion handleCellClick
aufruft und so die Spielzüge verwaltet.
let blockClick = false;
function handleCellClick(event) {
if (blockClick || event.target.textContent !== '') return;
let index = event.target.getAttribute('data-index');
boardCells[index].textContent = currentPlayer;
checkWinner();
switchTurn();
}
Wenn auf eine Zelle geklickt wird, wird sie mit dem Symbol des aktuellen Spielers ('X' oder 'O') markiert.
Über den Aufruf der Funktionen checkWinner
und switchTurn
überprüfen wir ob das Spiel endet und wechseln den aktuellen Spieler.
let currentPlayer = 'X';
const playerTurn = document.getElementById('player-turn');
function switchTurn() {
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
playerTurn.textContent = `Zug des Spielers ${currentPlayer}`;
}
Der aktuelle Spieler wird in der globalen Variable currentPlayer
gespeichert.
Die Funktion switchTurn
wechselt den Spieler abwechselnd zwischen 'X' und 'O' und aktualisiert den Spielstatus im playerTurn
-Element.
Wer hat gewonnen?
function checkWinner() {
const winningCombinations = [
[0, 1, 2], // horizontal
[3, 4, 5],
[6, 7, 8],
[0, 3, 6], // vertikal
[1, 4, 7],
[2, 5, 8],
[0, 4, 8], // diagonal
[2, 4, 6]
];
for (let combination of winningCombinations) {
if (
boardCells[combination[0]].textContent &&
boardCells[combination[0]].textContent === boardCells[combination[1]].textContent &&
boardCells[combination[0]].textContent === boardCells[combination[2]].textContent
) {
alert(`Spieler ${boardCells[combination[0]].textContent} gewinnt!`);
resetGame();
return;
}
}
if (countEmptyCells() === 0) {
alert('Unentschieden!');
resetGame();
}
}
Das Spiel überprüft vordefinierte Gewinnkombinationen, die im Array winningCombinations
gespeichert sind.
Jede Kombination repräsentiert ein Set von drei Zellindizes, die eine gewinnende Reihe, Spalte oder Diagonale bilden.
Wenn alle drei Zellen in einer Kombination dasselbe Symbol ('X' oder 'O') enthalten, wird der entsprechende Spieler zum Gewinner erklärt.
Dies wird durch die Verwendung einer for
-Schleife über das Array winningCombinations
implementiert.
Im if
-Statement überprüfen wir zuerst, ob die Zelle nicht leer ist boardCells[combination[0]].textContent && ...
und vergleichen dann die erste Zelle mit der zweiten und der dritten Zelle.
Da eine Gewinnkombination nur gebildet werden kann, wenn alle drei Zellen dasselbe Symbol haben, müssen wir nur überprüfen, dass die ersten beiden Zellen mit der dritten übereinstimmen.
Wenn keine Gewinnkombination gefunden wird und alle Zellen gefüllt sind, gibt das Spiel ein Unentschieden bekannt.
Um zu überprüfen, ob noch Zellen auf dem Board verbleiben, verwenden wir die Funktion countEmptyCells
:
function countEmptyCells() {
return boardCells.filter(cell => cell.textContent === '').length;
}
Die Methode filter
erstellt ein neues Array mit allen Elementen, die die Bedingung der Callback-Funktion erfüllen (hier: cell => cell.textContent === ''
).
Sind keine leeren Zellen mehr vorhanden, beträgt die Länge (length
) des Ergebnis-Arrays 0
.
Das war's – unser Tic-Tac-Toe-Spiel ist fertig!
Zum Abschluss findest du hier noch die vollständigen Quellcodes für index.html
und index.js
, falls du sie dir im Ganzen ansehen möchtest:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JBerries - Tic Tac Toe</title>
<style>
#board {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 0;
width: 300px;
height: 300px;
border-top: 1px solid black; /* Füge oberen Rand zum Container hinzu */
border-right: 1px solid black; /* Füge rechten Rand zum Container hinzu */
padding: 0;
}
#board > div {
background-color: white;
border-left: 1px solid black; /* Nur linker Rand für Zellen */
border-bottom: 1px solid black; /* Nur unterer Rand für Zellen */
font-size: 40px;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: calc(300px / 3);
height: calc(300px / 3);
box-sizing: border-box;
}
#player-turn {
font-size: 24px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div id="board"></div>
<p id="player-turn">Zug des Spielers X</p>
<script src="index.js"></script>
</body>
</html>
JavaScript
const board = document.getElementById('board');
const playerTurn = document.getElementById('player-turn');
let currentPlayer = 'X';
// Initialisiere das Board mit leeren Zellen
// [0][1][2]
// [3][4][5]
// [6][7][8]
const boardCells = [];
for (let i = 0; i < 9; i++) {
const cell = document.createElement('div');
cell.textContent = '';
cell.addEventListener('click', handleCellClick);
cell.setAttribute('data-index', i);
board.appendChild(cell);
boardCells.push(cell);
}
let blockClick = false;
function resetGame() {
blockClick = true;
setTimeout(() => {
for (let i = 0; i < 9; i++) {
boardCells[i].textContent = '';
}
blockClick = false;
}, 1000)
}
function handleCellClick(event) {
if (blockClick) return;
if (event.target.textContent !== '') return;
let index = event.target.getAttribute('data-index');
if (currentPlayer === 'X') {
boardCells[index].textContent = 'X';
checkWinner();
switchTurn();
} else {
boardCells[index].textContent = 'O';
checkWinner();
switchTurn();
}
}
function switchTurn() {
currentPlayer = (currentPlayer === 'X') ? 'O' : 'X';
playerTurn.textContent = `Zug des Spielers ${currentPlayer}`;
}
function checkWinner() {
const winningCombinations = [
[0, 1, 2], // horizontal
[3, 4, 5],
[6, 7, 8],
[0, 3, 6], // vertikal
[1, 4, 7],
[2, 5, 8],
[0, 4, 8], // diagonal
[2, 4, 6]
];
for (let combination of winningCombinations) {
if (
boardCells[combination[0]].textContent &&
boardCells[combination[0]].textContent ===
boardCells[combination[1]].textContent &&
boardCells[combination[2]].textContent ===
boardCells[combination[0]].textContent
) {
alert(`Spieler ${boardCells[combination[0]].textContent} gewinnt!`);
resetGame();
return;
}
}
if (countEmptyCells() === 0) {
alert('Es ist ein Unentschieden!');
resetGame();
}
}
function countEmptyCells() {
return boardCells.filter(cell => cell.textContent === '').length;
}