Untitled

Anonymous
plain_text
09/14/2026 7:47 AM
26.8 KB
2
Indexable
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Neon Tic-Tac-Toe</title>

  <style>
    :root {
      --bg: #09090b;
      --panel: rgba(24, 24, 27, 0.82);
      --panel-light: rgba(39, 39, 42, 0.8);
      --border: rgba(255, 255, 255, 0.1);
      --text: #f4f4f5;
      --muted: #a1a1aa;
      --x: #22d3ee;
      --x-glow: rgba(34, 211, 238, 0.55);
      --o: #fb7185;
      --o-glow: rgba(251, 113, 133, 0.55);
      --success: #a3e635;
    }

    * {
      box-sizing: border-box;
    }

    body {
      min-height: 100vh;
      margin: 0;
      display: grid;
      place-items: center;
      padding: 24px;
      color: var(--text);
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
        "Segoe UI", sans-serif;
      background:
        radial-gradient(circle at 15% 15%, rgba(34, 211, 238, 0.13), transparent 28%),
        radial-gradient(circle at 85% 85%, rgba(251, 113, 133, 0.12), transparent 28%),
        var(--bg);
      overflow-x: hidden;
    }

    body::before {
      content: "";
      position: fixed;
      inset: 0;
      pointer-events: none;
      opacity: 0.16;
      background-image:
        linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
      background-size: 36px 36px;
      mask-image: linear-gradient(to bottom, black, transparent);
    }

    .app {
      width: min(100%, 760px);
      position: relative;
    }

    .header {
      text-align: center;
      margin-bottom: 24px;
    }

    .eyebrow {
      margin: 0 0 8px;
      color: var(--x);
      font-size: 0.72rem;
      font-weight: 800;
      letter-spacing: 0.22em;
      text-transform: uppercase;
    }

    h1 {
      margin: 0;
      font-size: clamp(2.2rem, 7vw, 4.2rem);
      line-height: 0.95;
      letter-spacing: -0.07em;
      background: linear-gradient(110deg, #fff 20%, var(--x) 48%, var(--o) 80%);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }

    .subtitle {
      margin: 13px 0 0;
      color: var(--muted);
      font-size: 0.98rem;
    }

    .game-shell {
      padding: clamp(18px, 4vw, 30px);
      border: 1px solid var(--border);
      border-radius: 28px;
      background: linear-gradient(145deg, rgba(39,39,42,.84), rgba(9,9,11,.88));
      box-shadow:
        0 30px 90px rgba(0, 0, 0, .45),
        inset 0 1px 0 rgba(255,255,255,.07);
      backdrop-filter: blur(20px);
    }

    .toolbar {
      display: flex;
      flex-wrap: wrap;
      gap: 12px;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 22px;
    }

    .mode-switcher {
      display: flex;
      padding: 4px;
      gap: 4px;
      border-radius: 13px;
      background: rgba(0, 0, 0, .24);
      border: 1px solid var(--border);
    }

    button {
      border: 0;
      color: var(--text);
      font: inherit;
      cursor: pointer;
    }

    .mode-button,
    .action-button {
      padding: 10px 14px;
      border-radius: 9px;
      font-size: .86rem;
      font-weight: 700;
      transition: .2s ease;
    }

    .mode-button {
      color: var(--muted);
      background: transparent;
    }

    .mode-button:hover {
      color: var(--text);
      background: rgba(255,255,255,.06);
    }

    .mode-button.active {
      color: #09090b;
      background: #f4f4f5;
      box-shadow: 0 4px 18px rgba(255,255,255,.15);
    }

    .action-button {
      background: rgba(255,255,255,.08);
      border: 1px solid var(--border);
    }

    .action-button:hover {
      transform: translateY(-2px);
      background: rgba(255,255,255,.14);
    }

    .turn-status {
      min-height: 56px;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-bottom: 22px;
      padding: 13px 18px;
      border: 1px solid var(--border);
      border-radius: 15px;
      color: var(--muted);
      background: rgba(0,0,0,.18);
      text-align: center;
      font-weight: 700;
    }

    .turn-status strong {
      margin-left: 6px;
    }

    .turn-x strong {
      color: var(--x);
      text-shadow: 0 0 18px var(--x-glow);
    }

    .turn-o strong {
      color: var(--o);
      text-shadow: 0 0 18px var(--o-glow);
    }

    .turn-win {
      color: var(--success);
      border-color: rgba(163, 230, 53, .3);
      background: rgba(163, 230, 53, .08);
    }

    .turn-tie {
      color: #facc15;
      border-color: rgba(250, 204, 21, .25);
      background: rgba(250, 204, 21, .07);
    }

    .board-wrap {
      position: relative;
      width: min(100%, 510px);
      aspect-ratio: 1;
      margin: 0 auto;
    }

    .board {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: clamp(8px, 2vw, 14px);
      width: 100%;
      height: 100%;
    }

    .cell {
      position: relative;
      display: grid;
      place-items: center;
      min-width: 0;
      border: 1px solid rgba(255,255,255,.1);
      border-radius: 20px;
      background:
        linear-gradient(145deg, rgba(63,63,70,.65), rgba(24,24,27,.9));
      box-shadow:
        inset 0 1px 0 rgba(255,255,255,.06),
        0 7px 22px rgba(0,0,0,.2);
      transition: transform .2s ease, border-color .2s ease, background .2s ease;
    }

    .cell:not(.occupied):not(.disabled):hover {
      transform: translateY(-4px) scale(1.025);
      border-color: rgba(255,255,255,.3);
      background: linear-gradient(145deg, rgba(82,82,91,.7), rgba(39,39,42,.92));
    }

    .cell:not(.occupied):not(.disabled):active {
      transform: scale(.97);
    }

    .cell.disabled {
      cursor: default;
    }

    .cell.occupied {
      cursor: default;
    }

    .mark {
      animation: markIn .34s cubic-bezier(.2, .9, .25, 1.3) both;
      font-size: clamp(3.1rem, 14vw, 6.4rem);
      font-weight: 900;
      line-height: 1;
      user-select: none;
    }

    .mark.x {
      color: var(--x);
      text-shadow: 0 0 12px var(--x-glow), 0 0 38px var(--x-glow);
    }

    .mark.o {
      color: var(--o);
      text-shadow: 0 0 12px var(--o-glow), 0 0 38px var(--o-glow);
    }

    .cell.winner {
      animation: winnerPulse 1s ease-in-out infinite alternate;
      z-index: 1;
    }

    .cell.winner.x {
      border-color: var(--x);
      background: rgba(34,211,238,.12);
    }

    .cell.winner.o {
      border-color: var(--o);
      background: rgba(251,113,133,.12);
    }

    .winning-line {
      position: absolute;
      z-index: 3;
      height: 6px;
      width: 0;
      opacity: 0;
      border-radius: 999px;
      transform-origin: left center;
      pointer-events: none;
    }

    .winning-line.visible {
      width: 94%;
      opacity: 1;
      animation: lineStrike .55s cubic-bezier(.2,.8,.2,1) forwards;
    }

    .winning-line.x {
      background: var(--x);
      box-shadow: 0 0 12px var(--x-glow), 0 0 28px var(--x-glow);
    }

    .winning-line.o {
      background: var(--o);
      box-shadow: 0 0 12px var(--o-glow), 0 0 28px var(--o-glow);
    }

    .scoreboard {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      margin-top: 25px;
    }

    .score-card {
      padding: 15px 10px;
      text-align: center;
      border: 1px solid var(--border);
      border-radius: 15px;
      background: rgba(0,0,0,.2);
    }

    .score-card .label {
      color: var(--muted);
      font-size: .72rem;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
    }

    .score-card .value {
      display: block;
      margin-top: 5px;
      font-size: 1.65rem;
      font-weight: 900;
    }

    .score-card.x .value { color: var(--x); }
    .score-card.o .value { color: var(--o); }
    .score-card.tie .value { color: #facc15; }

    .footer-actions {
      display: flex;
      justify-content: center;
      gap: 12px;
      margin-top: 22px;
    }

    .primary {
      padding: 13px 20px;
      border-radius: 12px;
      color: #09090b;
      background: linear-gradient(135deg, var(--x), #67e8f9);
      box-shadow: 0 8px 25px rgba(34,211,238,.2);
      font-weight: 900;
      transition: .2s ease;
    }

    .primary:hover {
      transform: translateY(-2px);
      box-shadow: 0 12px 30px rgba(34,211,238,.35);
    }

    .primary:active {
      transform: scale(.97);
    }

    @keyframes markIn {
      0% { opacity: 0; transform: scale(.25) rotate(-18deg); }
      70% { transform: scale(1.13) rotate(4deg); }
      100% { opacity: 1; transform: scale(1) rotate(0); }
    }

    @keyframes winnerPulse {
      from { transform: scale(1); }
      to { transform: scale(1.035); }
    }

    @keyframes lineStrike {
      from { clip-path: inset(0 100% 0 0); }
      to { clip-path: inset(0 0 0 0); }
    }

    @media (max-width: 540px) {
      body {
        padding: 14px;
      }

      .toolbar {
        justify-content: center;
      }

      .mode-switcher {
        width: 100%;
      }

      .mode-button {
        flex: 1;
        padding-inline: 8px;
      }

      .action-button {
        width: 100%;
      }

      .cell {
        border-radius: 14px;
      }
    }
  </style>
</head>

<body>
  <main class="app">
    <header class="header">
      <p class="eyebrow">Local Multiplayer</p>
      <h1>Tic-Tac-Toe</h1>
      <p class="subtitle">Two players. One board. No distractions.</p>
    </header>

    <section class="game-shell">
      <div class="toolbar">
        <div class="mode-switcher" aria-label="Game mode">
          <button class="mode-button active" data-mode="pvp">2 Players</button>
          <button class="mode-button" data-mode="ai">Play vs AI</button>
        </div>

        <button class="action-button" id="resetScoresButton">
          Reset Scores
        </button>
      </div>

      <div class="turn-status turn-x" id="turnStatus" aria-live="polite">
        <span id="statusText">Player turn:</span>
        <strong id="currentPlayer">X</strong>
      </div>

      <div class="board-wrap">
        <div class="board" id="board" aria-label="Tic-Tac-Toe board">
          <button class="cell" data-index="0" aria-label="Top left"></button>
          <button class="cell" data-index="1" aria-label="Top middle"></button>
          <button class="cell" data-index="2" aria-label="Top right"></button>
          <button class="cell" data-index="3" aria-label="Middle left"></button>
          <button class="cell" data-index="4" aria-label="Center"></button>
          <button class="cell" data-index="5" aria-label="Middle right"></button>
          <button class="cell" data-index="6" aria-label="Bottom left"></button>
          <button class="cell" data-index="7" aria-label="Bottom middle"></button>
          <button class="cell" data-index="8" aria-label="Bottom right"></button>
        </div>
        <div class="winning-line" id="winningLine"></div>
      </div>

      <div class="scoreboard" aria-label="Scoreboard">
        <div class="score-card x">
          <span class="label">X Wins</span>
          <span class="value" id="xScore">0</span>
        </div>
        <div class="score-card tie">
          <span class="label">Ties</span>
          <span class="value" id="tieScore">0</span>
        </div>
        <div class="score-card o">
          <span class="label">O Wins</span>
          <span class="value" id="oScore">0</span>
        </div>
      </div>

      <div class="footer-actions">
        <button class="primary" id="restartButton">Restart Game</button>
      </div>
    </section>
  </main>

  <script>
    const boardElement = document.getElementById("board");
    const cells = [...document.querySelectorAll(".cell")];
    const winningLine = document.getElementById("winningLine");
    const turnStatus = document.getElementById("turnStatus");
    const statusText = document.getElementById("statusText");
    const currentPlayerElement = document.getElementById("currentPlayer");
    const restartButton = document.getElementById("restartButton");
    const resetScoresButton = document.getElementById("resetScoresButton");
    const xScoreElement = document.getElementById("xScore");
    const oScoreElement = document.getElementById("oScore");
    const tieScoreElement = document.getElementById("tieScore");
    const modeButtons = [...document.querySelectorAll(".mode-button")];

    const winningCombinations = [
      [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 board = Array(9).fill("");
    let currentPlayer = "X";
    let gameOver = false;
    let gameMode = "pvp";

    let scores = {
      X: 0,
      O: 0,
      ties: 0
    };

    cells.forEach(cell => {
      cell.addEventListener("click", () => {
        const index = Number(cell.dataset.index);
        makeMove(index);
      });
    });

    restartButton.addEventListener("click", restartGame);

    resetScoresButton.addEventListener("click", () => {
      scores = { X: 0, O: 0, ties: 0 };
      updateScores();
      restartGame();
    });

    modeButtons.forEach(button => {
      button.addEventListener("click", () => {
        gameMode = button.dataset.mode;

        modeButtons.forEach(item => {
          item.classList.toggle("active", item === button);
        });

        restartGame();
      });
    });

    function makeMove(index) {
      if (gameOver || board[index]) return;
      if (gameMode === "ai" && currentPlayer === "O") return;

      placeMark(index, currentPlayer);

      const result = evaluateBoard(board);

      if (result.winner) {
        finishGame(result.winner, result.combo);
        return;
      }

      if (result.tie) {
        finishTie();
        return;
      }

      currentPlayer = currentPlayer === "X" ? "O" : "X";
      updateTurnStatus();

      if (gameMode === "ai" && currentPlayer === "O") {
        setTimeout(() => {
          const aiMove = findBestMove(board);
          placeMark(aiMove, "O");

          const aiResult = evaluateBoard(board);

          if (aiResult.winner) {
            finishGame(aiResult.winner, aiResult.combo);
          } else if (aiResult.tie) {
            finishTie();
          } else {
            currentPlayer = "X";
            updateTurnStatus();
          }
        }, 380);
      }
    }

    function placeMark(index, player) {
      board[index] = player;

      const cell = cells[index];
      cell.classList.add("occupied", player.toLowerCase());
      cell.innerHTML = `<span class="mark ${player.toLowerCase()}">${player}</span>`;
      cell.setAttribute("aria-label", `${player} marked this square`);
    }

    function evaluateBoard(currentBoard) {
      for (const combo of winningCombinations) {
        const [a, b, c] = combo;

        if (
          currentBoard[a] &&
          currentBoard[a] === currentBoard[b] &&
          currentBoard[a] === currentBoard[c]
        ) {
          return {
            winner: currentBoard[a],
            combo,
            tie: false
          };
        }
      }

      return {
        winner: null,
        combo: null,
        tie: currentBoard.every(Boolean)
      };
    }

    function finishGame(winner, combo) {
      gameOver = true;
      scores[winner]++;
      updateScores();

      combo.forEach(index => {
        cells[index].classList.add("winner", winner.toLowerCase());
      });

      drawWinningLine(combo, winner);

      turnStatus.className = "turn-status turn-win";
      statusText.textContent = "Winner:";
      currentPlayerElement.textContent = winner;
    }

    function finishTie() {
      gameOver = true;
      scores.ties++;
      updateScores();

      turnStatus.className = "turn-status turn-tie";
      statusText.textContent = "It's a";
      currentPlayerElement.textContent = "Tie";
    }

    function updateTurnStatus() {
      turnStatus.className =
        currentPlayer === "X"
          ? "turn-status turn-x"
          : "turn-status turn-o";

      if (gameMode === "ai" && currentPlayer === "O") {
        statusText.textContent = "AI is thinking";
        currentPlayerElement.textContent = "…";
      } else {
        statusText.textContent =
          gameMode === "ai" ? "Your turn:" : "Player turn:";
        currentPlayerElement.textContent = currentPlayer;
      }
    }

    function updateScores() {
      xScoreElement.textContent = scores.X;
      oScoreElement.textContent = scores.O;
      tieScoreElement.textContent = scores.ties;
    }

    function restartGame() {
      board = Array(9).fill("");
      currentPlayer = "X";
      gameOver = false;

      cells.forEach(cell => {
        cell.className = "cell";
        cell.innerHTML = "";
      });

      winningLine.className = "winning-line";
      winningLine.removeAttribute("style");
      updateTurnStatus();
    }

    function drawWinningLine(combo, winner) {
      const boardRect = boardElement.getBoundingClientRect();
      const firstRect = cells[combo[0]].getBoundingClientRect();
      const lastRect = cells[combo[2]].getBoundingClientRect();

      const startX =
        firstRect.left - boardRect.left + firstRect.width / 2;
      const startY =
        firstRect.top - boardRect.top + firstRect.height / 2;
      const endX =
        lastRect.left - boardRect.left + lastRect.width / 2;
      const endY =
        lastRect.top - boardRect.top + lastRect.height / 2;

      const deltaX = endX - startX;
      const deltaY = endY - startY;
      const length = Math.sqrt(deltaX ** 2 + deltaY ** 2);
      const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;

      winningLine.style.left = `${startX}px`;
      winningLine.style.top = `${startY - 3}px`;
      winningLine.style.width = `${length}px`;
      winningLine.style.transform = `rotate(${angle}deg)`;
      winningLine.classList.add("visible", winner.toLowerCase());
    }

    // Minimax AI: O plays perfectly and X always starts.
    function findBestMove(currentBoard) {
      let bestScore = -Infinity;
      let move;

      for (let i = 0; i < currentBoard.length; i++) {
        if (!currentBoard[i]) {
          currentBoard[i] = "O";
          const score = minimax(currentBoard, false, 0);
          currentBoard[i] = "";

          if (score > bestScore) {
            bestScore = score;
            move = i;
          }
        }
      }

      return move;
    }

    function minimax(currentBoard, isMaximizing, depth) {
      const result = evaluateBoard(currentBoard);

      if (result.winner === "O") return 10 - depth;
      if (result.winner === "X") return depth - 10;
      if (result.tie) return 0;

      if (isMaximizing) {
        let bestScore = -Infinity;

        for (let i = 0; i < currentBoard.length; i++) {
          if (!currentBoard[i]) {
            currentBoard[i] = "O";
            bestScore = Math.max(
              bestScore,
              minimax(currentBoard, false, depth + 1)
            );
            currentBoard[i] = "";
          }
        }

        return bestScore;
      } else {
        let bestScore = Infinity;

        for (let i = 0; i < currentBoard.length; i++) {
          if (!currentBoard[i]) {
            currentBoard[i] = "X";
            bestScore = Math.min(
              bestScore,
              minimax(currentBoard, true, depth + 1)
            );
            currentBoard[i] = "";
          }
        }

        return bestScore;
      }
    }

    window.addEventListener("resize", () => {
      const result = evaluateBoard(board);
      if (result.winner) {
        drawWinningLine(result.combo, result.winner);
      }
    });

    updateScores();
    updateTurnStatus();
  </script>
</body>
</html>

Editor is loading...
Leave a Comment