Tic Tac Toe vs Roboter
by CrystalPilot53739 lines16.2 KB
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#10152b">
<title>Tic Tac Toe vs Roboter</title>
<style>
:root {
--bg: #10152b;
--panel: #1c2340;
--cyan: #42e8f5;
--pink: #ff5fa2;
--text: #f5f7ff;
--muted: #aab4d4;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
min-height: 100dvh;
font-family: system-ui, Arial, sans-serif;
color: var(--text);
background:
radial-gradient(ellipse at top, #26365e 0%, transparent 60%),
var(--bg);
display: flex;
justify-content: center;
align-items: center;
padding: 18px;
}
.app {
width: 100%;
max-width: 430px;
text-align: center;
}
h1 {
font-size: clamp(27px, 8vw, 36px);
margin: 0 0 8px;
letter-spacing: 1px;
}
h1 span { color: var(--cyan); }
.subtitle {
color: var(--muted);
margin: 0 0 24px;
}
.panel {
background: rgba(28,35,64,.94);
border: 1px solid #384568;
border-radius: 23px;
padding: 23px 18px;
box-shadow: 0 18px 55px #0004;
}
.robot {
font-size: 66px;
margin: 0 0 6px;
filter: drop-shadow(0 0 15px #42e8f544);
}
.robot-name {
font-weight: 800;
font-size: 20px;
}
.robot-desc {
font-size: 14px;
color: var(--muted);
margin: 6px 0 22px;
}
label {
display: block;
text-align: left;
font-weight: 700;
margin-bottom: 9px;
}
select, button {
font: inherit;
-webkit-tap-highlight-color: transparent;
}
select {
width: 100%;
padding: 14px;
background: #111831;
color: white;
border: 1px solid #4a587e;
border-radius: 12px;
margin-bottom: 15px;
font-size: 16px;
}
.btn {
width: 100%;
min-height: 50px;
border: 0;
border-radius: 13px;
font-weight: 800;
font-size: 16px;
cursor: pointer;
transition: transform .12s, opacity .12s;
}
.btn:active, .cell:active {
transform: scale(.97);
}
.primary {
background: linear-gradient(100deg, var(--cyan), #8bfff2);
color: #111831;
}
.secondary {
background: #303a60;
color: white;
border: 1px solid #4b5a83;
}
.small-note {
color: var(--muted);
font-size: 12px;
margin-top: 15px;
line-height: 1.5;
}
.hidden { display: none !important; }
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.pill {
background: #252e4e;
border: 1px solid #3d4b70;
padding: 8px 11px;
border-radius: 30px;
font-size: 12px;
color: var(--muted);
}
.status {
min-height: 55px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
margin-bottom: 14px;
}
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 9px;
width: 100%;
aspect-ratio: 1;
margin-bottom: 19px;
}
.cell {
border: 1px solid #39496f;
border-radius: 17px;
background: linear-gradient(145deg, #252f51, #19213d);
color: white;
font-size: clamp(45px, 15vw, 65px);
font-weight: 900;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
touch-action: manipulation;
}
.cell.x {
color: var(--cyan);
text-shadow: 0 0 17px #42e8f555;
}
.cell.o {
color: var(--pink);
text-shadow: 0 0 17px #ff5fa255;
}
.cell:disabled { cursor: default; }
.cell.win {
background: #34504d;
border-color: #78f5d6;
}
.scoreboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 18px;
}
.score {
background: #131a32;
padding: 11px 4px;
border-radius: 12px;
border: 1px solid #303d60;
}
.score strong {
display: block;
font-size: 23px;
}
.score span {
color: var(--muted);
font-size: 11px;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.actions .btn { font-size: 14px; }
.thinking {
display: inline-block;
animation: pulse .7s infinite alternate;
}
@keyframes pulse {
from { opacity: .4; }
to { opacity: 1; }
}
@media (max-width: 360px) {
body { padding: 10px; }
.panel { padding: 18px 12px; }
.board { gap: 6px; }
.cell { border-radius: 12px; }
}
</style>
</head>
<body>
<main class="app">
<h1>CYBER <span>TIC TAC TOE</span></h1>
<p class="subtitle">Mensch gegen Maschine 🤖</p>
<!-- STARTMENÜ -->
<section class="panel" id="menu">
<div class="robot">🤖</div>
<div class="robot-name">ROBO-9000</div>
<p class="robot-desc">
Ich bin bereit! Glaubst du, dass du mich besiegen kannst?
</p>
<label for="difficulty">Wähle deinen Schwierigkeitsgrad:</label>
<select id="difficulty">
<option value="easy">🟢 Leicht – Anfänger</option>
<option value="medium" selected>🟡 Mittel – Herausforderer</option>
<option value="hard">🔴 Schwer – Taktik-Profi</option>
</select>
<button class="btn primary" id="startBtn">
🚀 SPIEL STARTEN
</button>
<div class="small-note">
Du spielst mit X. Der Roboter spielt mit O.<br>
Jeder Durchgang kann anders verlaufen!
</div>
</section>
<!-- SPIEL -->
<section class="panel hidden" id="game">
<div class="topbar">
<span class="pill" id="levelBadge">MITTEL</span>
<span class="pill">DU: <b style="color:var(--cyan)">X</b></span>
<span class="pill">KI: <b style="color:var(--pink)">O</b></span>
</div>
<div class="status" id="status" aria-live="polite">
Du bist am Zug!
</div>
<div class="board" id="board" aria-label="Tic-Tac-Toe-Spielfeld">
<!-- Felder werden mit JavaScript erzeugt -->
</div>
<div class="scoreboard">
<div class="score">
<strong id="playerScore">0</strong>
<span>DEINE SIEGE</span>
</div>
<div class="score">
<strong id="drawScore">0</strong>
<span>UNENTSCHIEDEN</span>
</div>
<div class="score">
<strong id="robotScore">0</strong>
<span>ROBOTER</span>
</div>
</div>
<div class="actions">
<button class="btn primary" id="againBtn">🔄 Neue Runde</button>
<button class="btn secondary" id="menuBtn">⚙️ Menü</button>
</div>
<div class="small-note" id="robotComment">
Robo-9000 beobachtet dich ...
</div>
</section>
</main>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
const menu = $("menu");
const game = $("game");
const boardEl = $("board");
const statusEl = $("status");
const commentEl = $("robotComment");
const lines = [
[0,1,2], [3,4,5], [6,7,8],
[0,3,6], [1,4,7], [2,5,8],
[0,4,8], [2,4,6]
];
const comments = [
"Interessant ... ich analysiere dich! 🧠",
"Mein Zufallsgenerator hat gesprochen! 🎲",
"Das war nicht mein einziger Plan! 🤖",
"Neuer Zug, neue Strategie!",
"Ich passe meine Taktik an ...",
"Du bist gar nicht so leicht zu besiegen!",
"Berechnung abgeschlossen. Vielleicht. 😎",
"Meine Schaltkreise sind bereit!",
"Mal sehen, was du jetzt machst ...",
"Strategie Nummer 427 aktiviert!"
];
let cells = Array(9).fill("");
let difficulty = "medium";
let active = false;
let robotTimer = null;
let roundId = 0;
let scores = { player: 0, draw: 0, robot: 0 };
function randomItem(array) {
return array[Math.floor(Math.random() * array.length)];
}
// Mischt die Reihenfolge der möglichen Züge.
function shuffle(array) {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
function emptyCells(state) {
return state
.map((v, i) => v === "" ? i : -1)
.filter(i => i !== -1);
}
function winner(state) {
for (const line of lines) {
const [a,b,c] = line;
if (state[a] && state[a] === state[b] &&
state[b] === state[c]) {
return { player: state[a], line };
}
}
return null;
}
function randomWinningMove(state, symbol) {
const moves = shuffle(emptyCells(state));
for (const i of moves) {
state[i] = symbol;
const won = winner(state)?.player === symbol;
state[i] = "";
if (won) return i;
}
return null;
}
function findForks(state, symbol) {
const forks = [];
for (const i of shuffle(emptyCells(state))) {
state[i] = symbol;
let winningThreats = 0;
for (const j of emptyCells(state)) {
state[j] = symbol;
if (winner(state)?.player === symbol) {
winningThreats++;
}
state[j] = "";
}
state[i] = "";
if (winningThreats >= 2) forks.push(i);
}
return forks;
}
// Minimax: bewertet mögliche zukünftige Spielverläufe.
// Bei gleich guten Zügen entscheidet der Zufall.
function minimax(state, isRobot, depth, alpha, beta) {
const result = winner(state);
if (result?.player === "O") return 10 - depth;
if (result?.player === "X") return depth - 10;
const available = emptyCells(state);
if (!available.length) return 0;
const moves = shuffle(available);
if (isRobot) {
let best = -Infinity;
for (const i of moves) {
state[i] = "O";
const score = minimax(state, false, depth + 1, alpha, beta);
state[i] = "";
best = Math.max(best, score);
alpha = Math.max(alpha, best);
if (beta <= alpha) break;
}
return best;
} else {
let best = Infinity;
for (const i of moves) {
state[i] = "X";
const score = minimax(state, true, depth + 1, alpha, beta);
state[i] = "";
best = Math.min(best, score);
beta = Math.min(beta, best);
if (beta <= alpha) break;
}
return best;
}
}
function bestHardMove(state) {
let bestScore = -Infinity;
const bestMoves = [];
for (const i of shuffle(emptyCells(state))) {
state[i] = "O";
const score = minimax(state, false, 0, -Infinity, Infinity);
state[i] = "";
if (score > bestScore) {
bestScore = score;
bestMoves.length = 0;
bestMoves.push(i);
} else if (score === bestScore) {
bestMoves.push(i);
}
}
return randomItem(bestMoves);
}
function chooseRobotMove() {
// Leicht: überwiegend zufällige Züge,
// mit gelegentlichen taktischen Entscheidungen.
if (difficulty === "easy") {
if (Math.random() < 0.35) {
const win = randomWinningMove(cells, "O");
if (win !== null) return win;
}
if (Math.random() < 0.25) {
const block = randomWinningMove(cells, "X");
if (block !== null) return block;
}
return randomItem(shuffle(emptyCells(cells)));
}
// Mittel: gewinnt, wenn möglich, blockiert dich,
// sucht Gabeln und wählt abwechslungsreiche Positionen.
if (difficulty === "medium") {
const win = randomWinningMove(cells, "O");
if (win !== null) return win;
const block = randomWinningMove(cells, "X");
if (block !== null && Math.random() < 0.9) return block;
const forks = findForks(cells, "O");
if (forks.length && Math.random() < 0.8) {
return randomItem(forks);
}
// Gabel des Spielers verhindern, wenn möglich.
const playerForks = findForks(cells, "X");
if (playerForks.length && Math.random() < 0.8) {
const blockingFork = playerForks.filter(i => cells[i] === "");
if (blockingFork.length) return randomItem(blockingFork);
}
const candidates = shuffle(emptyCells(cells));
// Bevorzugt häufig die Mitte, Ecken und Seiten,
// ohne stets dieselbe Priorität zu verwenden.
const center = candidates.filter(i => i === 4);
const corners = candidates.filter(i => [0,2,6,8].includes(i));
const sides = candidates.filter(i => [1,3,5,7].includes(i));
const pools = shuffle([
{ moves: center, weight: 3 },
{ moves: corners, weight: 4 },
{ moves: sides, weight: 2 }
]).filter(p => p.moves.length);
const total = pools.reduce((n, p) => n + p.weight, 0);
let roll = Math.random() * total;
for (const pool of pools) {
roll -= pool.weight;
if (roll < 0) return randomItem(pool.moves);
}
return randomItem(candidates);
}
// Schwer: optimale Strategie mit zufälliger Auswahl
// zwischen gleich guten Zügen.
return bestHardMove(cells);
}
function renderBoard(winLine = []) {
boardEl.innerHTML = "";
cells.forEach((value, i) => {
const button = document.createElement("button");
button.type = "button";
button.className = "cell";
if (value === "X") button.classList.add("x");
if (value === "O") button.classList.add("o");
if (winLine.includes(i)) button.classList.add("win");
button.textContent = value;
button.disabled = !active || value !== "";
button.setAttribute(
"aria-label",
value ? `Feld ${i + 1}: ${value}` : `Feld ${i + 1}: frei`
);
button.addEventListener("click", () => playerMove(i));
boardEl.appendChild(button);
});
}
function updateScores() {
$("playerScore").textContent = scores.player;
$("drawScore").textContent = scores.draw;
$("robotScore").textContent = scores.robot;
}
function finishRound(result) {
active = false;
clearTimeout(robotTimer);
const won = winner(cells);
if (result === "X") {
scores.player++;
statusEl.textContent = "🎉 Du hast gewonnen!";
commentEl.textContent = "Wow! Du hast Robo-9000 besiegt!";
} else if (result === "O") {
scores.robot++;
statusEl.textContent = "🤖 Der Roboter gewinnt!";
commentEl.textContent = randomItem([
"Mission erfüllt. 🤖",
"Ein guter Zug! Aber meiner war besser.",
"Meine Prozessoren feiern!",
"Noch eine Runde? Ich bin bereit!"
]);
} else {
scores.draw++;
statusEl.textContent = "🤝 Unentschieden!";
commentEl.textContent = "Gleich stark! Zeit für eine Revanche.";
}
renderBoard(won?.line || []);
updateScores();
}
function checkEnd() {
const result = winner(cells);
if (result) {
finishRound(result.player);
return true;
}
if (emptyCells(cells).length === 0) {
finishRound("draw");
return true;
}
return false;
}
function playerMove(i) {
if (!active || cells[i] !== "") return;
cells[i] = "X";
if (checkEnd()) return;
active = false;
statusEl.innerHTML = '<span class="thinking">🤖 Roboter denkt nach ...</span>';
commentEl.textContent = randomItem(comments);
renderBoard();
const thisRound = roundId;
// Kleine Pause für ein natürlicheres Spielgefühl.
robotTimer = setTimeout(() => {
if (thisRound !== roundId) return;
const move = chooseRobotMove();
if (move === undefined || move === null) return;
cells[move] = "O";
if (checkEnd()) return;
active = true;
statusEl.textContent = "Dein Zug! Setze ein X.";
commentEl.textContent = randomItem(comments);
renderBoard();
}, 400 + Math.random() * 550);
}
function newRound() {
clearTimeout(robotTimer);
roundId++;
cells = Array(9).fill("");
active = true;
statusEl.textContent = "Du bist am Zug! Setze ein X.";
commentEl.textContent = randomItem([
"Neue Runde, neue Möglichkeiten!",
"Ich habe meine Strategie aktualisiert!",
"Mal sehen, wie du diesmal spielst!",
"Meine Züge werden nicht immer gleich sein!"
]);
renderBoard();
}
function startGame() {
difficulty = $("difficulty").value;
const labels = {
easy: "🟢 LEICHT",
medium: "🟡 MITTEL",
hard: "🔴 SCHWER"
};
$("levelBadge").textContent = labels[difficulty];
menu.classList.add("hidden");
game.classList.remove("hidden");
scores = { player: 0, draw: 0, robot: 0 };
updateScores();
newRound();
}
function showMenu() {
clearTimeout(robotTimer);
roundId++;
active = false;
game.classList.add("hidden");
menu.classList.remove("hidden");
}
$("startBtn").addEventListener("click", startGame);
$("againBtn").addEventListener("click", newRound);
$("menuBtn").addEventListener("click", showMenu);
})();
</script>
</body>
</html>
Game Source: Tic Tac Toe vs Roboter
Creator: CrystalPilot53
Libraries: none
Complexity: complex (739 lines, 16.2 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: tic-tac-toe-vs-roboter-crystalpilot53" to link back to the original. Then publish at arcadelab.ai/publish.