🎮ArcadeLab

Remix of Tic Tac Toe vs Roboter

by CrystalPilot53
738 lines16.2 KB
▶ Play
<!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: Remix of Tic Tac Toe vs Roboter

Creator: CrystalPilot53

Libraries: none

Complexity: complex (738 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: remix-of-tic-tac-toe-vs-roboter-crystalpilot53" to link back to the original. Then publish at arcadelab.ai/publish.