🎮ArcadeLab

Крестики-Нолики: Ты vs Бот

by TurboGamer88
304 lines8.6 KB
▶ Play
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Крестики-Нолики: Ты vs Бот</title>
<style>
  body { margin: 0; background: #0a0a12; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial; overflow: hidden; touch-action: none; }
  canvas { background: #1a1a2e; border: 2px solid #e94560; border-radius: 8px; box-shadow: 0 0 25px rgba(233,69,96,0.4); touch-action: none; }
  #hud { font-size: 16px; margin-bottom: 8px; color: #e94560; font-weight: bold; text-align: center; text-shadow: 0 0 8px #e94560; }
  #restart { position: absolute; bottom: 20px; padding: 12px 30px; background: #e94560; color: #fff; border: none; border-radius: 10px; font-size: 18px; font-weight: bold; cursor: pointer; box-shadow: 0 0 15px rgba(233,69,96,0.6); }
</style>
</head>
<body>
<div id="hud">Ты: ❍ | Бот: ✕</div>
<canvas id="game" width="360" height="360"></canvas>
<button id="restart">🔄 Новая игра</button>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const hud = document.getElementById('hud');

const SIZE = 120;
const CELL = SIZE / 3;
let board = ['', '', '', '', '', '', '', '', ''];
let gameOver = false;
let winner = null; // 'player', 'bot', 'draw'
let playerTurn = true;
let winLine = null;
let botThinking = false;
let animTime = 0;

// --- ЗВУК ---
let audioCtx = null;
function initAudio() {
  if (audioCtx) return;
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
}
function playSound(freq, dur, type, vol) {
  if (!audioCtx) return;
  try {
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
    o.type = type || 'sine'; o.frequency.value = freq;
    g.gain.setValueAtTime(vol || 0.08, audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
    o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime + dur);
  } catch(e) {}
}
function sfxPlace() { playSound(600, 0.1, 'sine', 0.08); }
function sfxWin() { playSound(523, 0.15, 'sine', 0.12); setTimeout(() => playSound(659, 0.15, 'sine', 0.12), 150); setTimeout(() => playSound(784, 0.3, 'sine', 0.12), 300); }
function sfxLose() { playSound(300, 0.3, 'sawtooth', 0.12); setTimeout(() => playSound(200, 0.4, 'sawtooth', 0.1), 200); }
function sfxDraw() { playSound(400, 0.2, 'triangle', 0.1); }
function sfxClick() { playSound(800, 0.05, 'square', 0.06); }

// --- ЛОГИКА ---
function checkWinner() {
  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]
  ];
  for (let line of lines) {
    const [a,b,c] = line;
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      winLine = line;
      return board[a] === 'O' ? 'player' : 'bot';
    }
  }
  if (board.every(c => c !== '')) return 'draw';
  return null;
}

function botMove() {
  if (gameOver) return;
  botThinking = true;
  setTimeout(() => {
    // Простой ИИ: выиграть, блокировать, центр, угол, любая
    const empty = board.map((c, i) => c === '' ? i : null).filter(i => i !== null);
    if (empty.length === 0) return;

    // 1. Выиграть
    for (let i of empty) {
      board[i] = 'X';
      if (checkWinner() === 'bot') { placeBot(i); return; }
      board[i] = '';
    }
    // 2. Блокировать
    for (let i of empty) {
      board[i] = 'O';
      if (checkWinner() === 'player') { board[i] = ''; placeBot(i); return; }
      board[i] = '';
    }
    // 3. Центр
    if (board[4] === '') { placeBot(4); return; }
    // 4. Угол
    const corners = [0,2,6,8].filter(i => board[i] === '');
    if (corners.length > 0) { placeBot(corners[Math.floor(Math.random() * corners.length)]); return; }
    // 5. Любая
    placeBot(empty[Math.floor(Math.random() * empty.length)]);
  }, 400);
}

function placeBot(i) {
  board[i] = 'X';
  botThinking = false;
  sfxPlace();
  const w = checkWinner();
  if (w) { endGame(w); return; }
  playerTurn = true;
  updateHUD();
}

function placePlayer(i) {
  if (gameOver || !playerTurn || board[i] !== '') return;
  board[i] = 'O';
  sfxPlace();
  const w = checkWinner();
  if (w) { endGame(w); return; }
  playerTurn = false;
  updateHUD();
  botMove();
}

function endGame(w) {
  gameOver = true;
  winner = w;
  if (w === 'player') { sfxWin(); }
  else if (w === 'bot') { sfxLose(); }
  else { sfxDraw(); }
  updateHUD();
}

function updateHUD() {
  if (winner === 'player') hud.textContent = '🎉 Ты победил!';
  else if (winner === 'bot') hud.textContent = '🤖 Бот победил!';
  else if (winner === 'draw') hud.textContent = '🤝 Ничья!';
  else hud.textContent = playerTurn ? 'Твой ход (❍)' : 'Бот думает...';
}

function restart() {
  board = ['', '', '', '', '', '', '', '', ''];
  gameOver = false;
  winner = null;
  playerTurn = true;
  winLine = null;
  botThinking = false;
  sfxClick();
  updateHUD();
}

// --- РИСОВАНИЕ ---
function drawGrid() {
  ctx.strokeStyle = '#e94560';
  ctx.lineWidth = 4;
  ctx.shadowColor = '#e94560';
  ctx.shadowBlur = 10;
  for (let i = 1; i < 3; i++) {
    ctx.beginPath();
    ctx.moveTo(i * CELL, 10);
    ctx.lineTo(i * CELL, SIZE - 10);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(10, i * CELL);
    ctx.lineTo(SIZE - 10, i * CELL);
    ctx.stroke();
  }
  ctx.shadowBlur = 0;
}

function drawX(cx, cy) {
  ctx.strokeStyle = '#3498db';
  ctx.lineWidth = 6;
  ctx.lineCap = 'round';
  ctx.shadowColor = '#3498db';
  ctx.shadowBlur = 12;
  const s = CELL * 0.3;
  ctx.beginPath();
  ctx.moveTo(cx - s, cy - s);
  ctx.lineTo(cx + s, cy + s);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(cx + s, cy - s);
  ctx.lineTo(cx - s, cy + s);
  ctx.stroke();
  ctx.shadowBlur = 0;
}

function drawO(cx, cy, progress = 1) {
  ctx.strokeStyle = '#e94560';
  ctx.lineWidth = 6;
  ctx.lineCap = 'round';
  ctx.shadowColor = '#e94560';
  ctx.shadowBlur = 12;
  const s = CELL * 0.3;
  ctx.beginPath();
  ctx.arc(cx, cy, s, -Math.PI/2, -Math.PI/2 + Math.PI * 2 * progress);
  ctx.stroke();
  ctx.shadowBlur = 0;
}

function drawWinLine() {
  if (!winLine) return;
  const [a,b,c] = winLine;
  const ax = (a % 3) * CELL + CELL/2;
  const ay = Math.floor(a / 3) * CELL + CELL/2;
  const cx = (c % 3) * CELL + CELL/2;
  const cy = Math.floor(c / 3) * CELL + CELL/2;
  ctx.strokeStyle = '#ffd700';
  ctx.lineWidth = 8;
  ctx.lineCap = 'round';
  ctx.shadowColor = '#ffd700';
  ctx.shadowBlur = 20;
  ctx.beginPath();
  ctx.moveTo(ax, ay);
  ctx.lineTo(cx, cy);
  ctx.stroke();
  ctx.shadowBlur = 0;
}

function draw() {
  animTime += 0.05;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
  grad.addColorStop(0, '#1a1a2e');
  grad.addColorStop(1, '#2a2a4e');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  drawGrid();

  for (let i = 0; i < 9; i++) {
    const cx = (i % 3) * CELL + CELL/2;
    const cy = Math.floor(i / 3) * CELL + CELL/2;
    if (board[i] === 'X') {
      drawX(cx, cy);
    } else if (board[i] === 'O') {
      drawO(cx, cy, 1);
    }
  }

  drawWinLine();

  if (!gameOver && !botThinking) {
    // Подсказка — куда можно ходить
    for (let i = 0; i < 9; i++) {
      if (board[i] === '') {
        const cx = (i % 3) * CELL + CELL/2;
        const cy = Math.floor(i / 3) * CELL + CELL/2;
        ctx.fillStyle = 'rgba(255,255,255,0.05)';
        ctx.beginPath();
        ctx.arc(cx, cy, CELL * 0.35, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }

  requestAnimationFrame(draw);
}

// --- УПРАВЛЕНИЕ ---
function getPos(e) {
  const rect = canvas.getBoundingClientRect();
  const t = e.touches ? e.touches[0] : e;
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  return {
    x: (t.clientX - rect.left) * scaleX,
    y: (t.clientY - rect.top) * scaleY
  };
}

function handleClick(x, y) {
  initAudio();
  if (gameOver) return;
  const col = Math.floor(x / CELL);
  const row = Math.floor(y / CELL);
  if (col < 0 || col > 2 || row < 0 || row > 2) return;
  const idx = row * 3 + col;
  placePlayer(idx);
}

canvas.addEventListener('touchstart', e => {
  e.preventDefault();
  const p = getPos(e);
  handleClick(p.x, p.y);
}, { passive: false });
canvas.addEventListener('mousedown', e => {
  e.preventDefault();
  const p = getPos(e);
  handleClick(p.x, p.y);
});

document.getElementById('restart').addEventListener('click', () => {
  initAudio();
  restart();
});

// Запуск
updateHUD();
draw();
</script>
</body>
</html>

Game Source: Крестики-Нолики: Ты vs Бот

Creator: TurboGamer88

Libraries: none

Complexity: complex (304 lines, 8.6 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: vs-turbogamer88" to link back to the original. Then publish at arcadelab.ai/publish.