๐ŸŽฎArcadeLab

๐ŸŽฎ Tic-Tac-Toe ยท 1P + Online Random

by SonicBear35
802 lines29.5 KB
โ–ถ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>๐ŸŽฎ Tic-Tac-Toe ยท 1P + Online Random</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            user-select: none;
        }
        body {
            background: linear-gradient(145deg, #0b1628, #1a2f44);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
            padding: 1rem;
            margin: 0;
        }
        .game-container {
            background: rgba(18, 32, 50, 0.8);
            backdrop-filter: blur(6px);
            border-radius: 3.5rem;
            padding: 1.8rem 2rem 2.5rem;
            box-shadow: 0 25px 45px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05);
            max-width: 660px;
            width: 100%;
            transition: 0.2s;
        }
        h1 {
            text-align: center;
            font-weight: 500;
            letter-spacing: 2px;
            font-size: 2rem;
            color: #e8effa;
            text-shadow: 0 2px 0 #0b111e;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 0.4rem;
            flex-wrap: wrap;
        }
        h1 span {
            background: #2d405b;
            padding: 0.15rem 1.2rem;
            border-radius: 60px;
            font-size: 1rem;
            font-weight: 600;
            color: #b7cbee;
            box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
        }
        .mode-panel {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: center;
            gap: 0.7rem;
            background: #141f32;
            padding: 0.8rem 1.2rem;
            border-radius: 80px;
            margin: 0.6rem 0 1rem;
            border: 1px solid #3d5270;
            box-shadow: inset 0 1px 6px #0a111f;
        }
        .mode-panel .btn {
            background: #2b3f5c;
            border: none;
            border-radius: 60px;
            padding: 0.6rem 1.6rem;
            font-weight: 600;
            font-size: 1rem;
            color: #eef3fc;
            box-shadow: 0 5px 0 #0e1729, 0 4px 12px rgba(0, 0, 0, 0.6);
            cursor: pointer;
            transition: 0.06s linear;
            border: 1px solid #4d688b;
            letter-spacing: 0.6px;
            white-space: nowrap;
        }
        .mode-panel .btn:active {
            transform: translateY(4px);
            box-shadow: 0 1px 0 #0e1729;
        }
        .mode-panel .btn.primary {
            background: #2f5a6b;
            border-color: #5f8aa3;
            flex: 1;
            min-width: 120px;
        }
        .mode-panel .btn.primary:hover {
            background: #3e7388;
        }
        .mode-panel .btn.secondary {
            background: #4a4a6a;
            border-color: #7a7aa0;
        }
        .mode-panel .btn.secondary:hover {
            background: #5e5e82;
        }
        .mode-panel .btn:disabled {
            opacity: 0.5;
            transform: translateY(2px);
            box-shadow: 0 2px 0 #0e1729;
            pointer-events: none;
        }
        .status-badge {
            background: #1f2d44;
            border-radius: 40px;
            padding: 0.3rem 1.2rem;
            font-size: 0.9rem;
            font-weight: 500;
            color: #b7ccec;
            border: 1px solid #3a5070;
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
        }
        .turn-indicator {
            background: #1a273d;
            border-radius: 100px;
            padding: 0.4rem 1.2rem;
            margin: 0.4rem auto 1rem;
            display: inline-flex;
            align-items: center;
            gap: 0.8rem;
            font-weight: 600;
            font-size: 1.2rem;
            color: #d3def5;
            box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6);
            border: 1px solid #3f5678;
        }
        .turn-indicator .player-badge {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 2.2rem;
            height: 2.2rem;
            border-radius: 60px;
            background: #0f1a2d;
            font-weight: 700;
            font-size: 1.6rem;
            box-shadow: 0 2px 0 #364d6b;
            color: #f1f5f9;
        }
        .turn-indicator .player-badge.x {
            color: #7aa9ff;
        }
        .turn-indicator .player-badge.o {
            color: #f7b87e;
        }
        .board {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 0.8rem;
            aspect-ratio: 1 / 1;
            width: 100%;
            max-width: 440px;
            margin: 0.6rem auto 1.2rem;
        }
        .cell {
            background: #1b2a40;
            border-radius: 28px;
            box-shadow: inset 0 -6px 0 #0b1322, 0 8px 16px rgba(0, 0, 0, 0.6);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 4.5rem;
            font-weight: 700;
            color: #eef3fc;
            transition: 0.08s ease;
            cursor: pointer;
            aspect-ratio: 1 / 1;
            text-shadow: 0 3px 0 #0a111f;
            border: 1px solid #3a5172;
        }
        .cell:active {
            transform: scale(0.94);
            box-shadow: inset 0 -2px 0 #0b1322;
        }
        .cell.x-move {
            color: #7aa9ff;
            text-shadow: 0 0 14px #3f7bff77;
        }
        .cell.o-move {
            color: #f7b87e;
            text-shadow: 0 0 14px #f7a54b77;
        }
        .cell.win-highlight {
            background: #2f4b5a;
            box-shadow: 0 0 20px #f5c54290, inset 0 -4px 0 #9e7d2e;
            border-color: #f5c542;
            transform: scale(1.02);
        }
        .cell.disabled-cell {
            cursor: default;
            opacity: 0.9;
        }
        .status-area {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            gap: 0.8rem;
            margin-top: 0.5rem;
        }
        .message {
            background: #111f31;
            padding: 0.5rem 1.6rem;
            border-radius: 60px;
            font-weight: 500;
            font-size: 1.1rem;
            color: #dae4f5;
            box-shadow: inset 0 2px 6px #060c17;
            border: 1px solid #344b69;
            flex: 1;
            min-width: 140px;
            text-align: center;
        }
        .message.win {
            background: #2f4b3f;
            border-color: #82c99a;
            color: #d4f5d4;
        }
        .message.waiting {
            background: #3f3a2b;
            border-color: #b89f5a;
            color: #f5e9c4;
        }
        .footer {
            display: flex;
            justify-content: center;
            margin-top: 1rem;
            color: #6b7f9c;
            font-size: 0.8rem;
            gap: 0.8rem;
            flex-wrap: wrap;
        }
        .footer span {
            background: #19273e;
            padding: 0.15rem 1rem;
            border-radius: 40px;
        }
        .peer-id-box {
            background: #0f1a2b;
            padding: 0.2rem 1.2rem;
            border-radius: 40px;
            font-family: monospace;
            font-size: 0.9rem;
            color: #aec3e8;
            border: 1px solid #384f70;
        }
        .queue-badge {
            background: #2a3f2a;
            border-radius: 40px;
            padding: 0.2rem 1rem;
            font-size: 0.8rem;
            color: #b5e6b5;
            border: 1px solid #5b8a5b;
            display: inline-block;
        }
        @media (max-width: 480px) {
            .game-container {
                padding: 1.2rem 1rem;
            }
            .cell {
                font-size: 3rem;
                border-radius: 18px;
            }
            .turn-indicator {
                font-size: 1rem;
                padding: 0.2rem 1rem;
            }
            .mode-panel .btn {
                padding: 0.5rem 1rem;
                font-size: 0.9rem;
            }
            .mode-panel {
                padding: 0.6rem 0.8rem;
            }
        }
    </style>
</head>
<body>
    <div class="game-container">
        <h1>โœ–๏ธ Tic-Tac-Toe <span>1P ยท online</span></h1>

        <!-- mode panel -->
        <div class="mode-panel">
            <button class="btn primary" id="aiBtn">๐Ÿค– vs AI</button>
            <button class="btn primary" id="onlineBtn">๐ŸŒ Find random</button>
            <div class="peer-id-box" id="peerIdDisplay">๐Ÿ”‘ your-id</div>
        </div>

        <div style="display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.2rem;">
            <div class="turn-indicator" id="turnIndicator">
                <span>Turn</span>
                <span class="player-badge x" id="turnBadge">X</span>
            </div>
            <div id="queueStatus" class="queue-badge" style="display: none;">โณ searching...</div>
        </div>

        <div class="board" id="board"></div>

        <div class="status-area">
            <div class="message" id="statusMessage">๐ŸŽฏ Choose a mode</div>
            <button class="btn reset" id="resetBtn" style="background:#3d3f66;border-color:#6b7ba0;">โŸณ Reset</button>
        </div>
        <div class="footer">
            <span id="modeLabel">โšก single</span>
            <span id="connectionStatus">โšช disconnected</span>
        </div>
    </div>

    <!-- PeerJS -->
    <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js">
    </script>
    <script>
        (function() {
            // ---------- DOM refs ----------
            const boardEl = document.getElementById('board');
            const statusMsg = document.getElementById('statusMessage');
            const turnBadge = document.getElementById('turnBadge');
            const turnIndicator = document.getElementById('turnIndicator');
            const resetBtn = document.getElementById('resetBtn');
            const aiBtn = document.getElementById('aiBtn');
            const onlineBtn = document.getElementById('onlineBtn');
            const peerIdDisplay = document.getElementById('peerIdDisplay');
            const connectionStatus = document.getElementById('connectionStatus');
            const queueStatus = document.getElementById('queueStatus');
            const modeLabel = document.getElementById('modeLabel');

            // ---------- Game state ----------
            let board = Array(9).fill(null);
            let currentPlayer = 'X'; // local player's mark
            let gameActive = false;
            let winCombo = null;
            let localMark = 'X';
            let turn = 'X'; // global turn
            let isAIMode = false;
            let isOnlineMode = false;
            let isHost = false;

            // ---------- PeerJS ----------
            let peer = null;
            let conn = null;
            let myId = null;

            // ---------- Render ----------
            function renderBoard() {
                boardEl.innerHTML = '';
                for (let i = 0; i < 9; i++) {
                    const cell = document.createElement('div');
                    cell.classList.add('cell');
                    const isMyTurn = (gameActive && turn === localMark && board[i] === null);
                    if (!isMyTurn || !gameActive) {
                        cell.classList.add('disabled-cell');
                    }
                    cell.dataset.index = i;
                    cell.textContent = board[i] || '';
                    if (board[i] === 'X') cell.classList.add('x-move');
                    else if (board[i] === 'O') cell.classList.add('o-move');

                    if (winCombo && winCombo.includes(i)) {
                        cell.classList.add('win-highlight');
                    }

                    cell.addEventListener('click', cellClickHandler);
                    boardEl.appendChild(cell);
                }
                updateUI();
            }

            // ---------- Click handler ----------
            function cellClickHandler(e) {
                const index = parseInt(e.currentTarget.dataset.index, 10);
                if (!gameActive || board[index] !== null) return;
                if (isOnlineMode) {
                    if (!conn || turn !== localMark) return;
                    makeMove(index, localMark);
                } else {
                    // AI mode: player is always X
                    if (localMark !== 'X') return;
                    if (turn !== 'X') return;
                    makeMove(index, 'X');
                }
            }

            // execute move
            function makeMove(index, player) {
                if (board[index] !== null || !gameActive || player !== turn) return false;

                board[index] = player;
                const win = checkWinner();
                if (win) {
                    winCombo = win;
                    gameActive = false;
                    renderBoard();
                    statusMsg.textContent = `๐Ÿ† ${player} wins!`;
                    statusMsg.classList.add('win');
                    statusMsg.classList.remove('waiting');
                    turnBadge.textContent = '๐Ÿ';
                    turnBadge.className = 'player-badge';
                    if (isOnlineMode && conn) {
                        conn.send({ type: 'win', winner: player, combo: win });
                    }
                    return true;
                } else if (board.every(c => c !== null)) {
                    gameActive = false;
                    winCombo = null;
                    renderBoard();
                    statusMsg.textContent = `๐Ÿค It's a draw!`;
                    statusMsg.classList.add('win');
                    statusMsg.classList.remove('waiting');
                    turnBadge.textContent = 'โš–๏ธ';
                    turnBadge.className = 'player-badge';
                    if (isOnlineMode && conn) {
                        conn.send({ type: 'draw' });
                    }
                    return true;
                }

                turn = (turn === 'X' ? 'O' : 'X');
                renderBoard();
                statusMsg.textContent = `โ–ถ๏ธ ${turn}'s turn`;
                statusMsg.classList.remove('win', 'waiting');

                if (isOnlineMode && conn) {
                    conn.send({ type: 'move', index, player, turn: turn });
                }

                // AI move if AI mode and it's O's turn
                if (isAIMode && gameActive && turn === 'O') {
                    setTimeout(() => aiMove(), 300);
                }
                return true;
            }

            // ---------- AI (simple) ----------
            function aiMove() {
                if (!gameActive || turn !== 'O' || !isAIMode) return;
                // find best move: win, block, center, random
                const empty = board.map((v, i) => v === null ? i : null).filter(v => v !== null);
                if (empty.length === 0) return;

                // check if AI can win
                for (let i of empty) {
                    board[i] = 'O';
                    if (checkWinner()) {
                        board[i] = null;
                        makeMove(i, 'O');
                        return;
                    }
                    board[i] = null;
                }
                // block player
                for (let i of empty) {
                    board[i] = 'X';
                    if (checkWinner()) {
                        board[i] = null;
                        makeMove(i, 'O');
                        return;
                    }
                    board[i] = null;
                }
                // center
                if (board[4] === null) {
                    makeMove(4, 'O');
                    return;
                }
                // random
                const rand = empty[Math.floor(Math.random() * empty.length)];
                makeMove(rand, 'O');
            }

            function checkWinner() {
                const patterns = [
                    [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 p of patterns) {
                    const [a, b, c] = p;
                    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
                        return p;
                    }
                }
                return null;
            }

            function updateUI() {
                if (!gameActive) return;
                turnBadge.textContent = turn;
                turnBadge.className = 'player-badge ' + (turn === 'X' ? 'x' : 'o');
            }

            // ---------- Reset ----------
            function resetGame() {
                board = Array(9).fill(null);
                winCombo = null;
                gameActive = true;
                turn = 'X';
                statusMsg.classList.remove('win', 'waiting');
                statusMsg.textContent = `โœจ New game ยท ${turn}'s turn`;
                turnBadge.textContent = 'X';
                turnBadge.className = 'player-badge x';
                renderBoard();
                if (isOnlineMode && conn) {
                    conn.send({ type: 'reset' });
                }
                if (isAIMode && turn === 'O') {
                    setTimeout(() => aiMove(), 400);
                }
            }

            // ---------- AI Mode ----------
            function startAIMode() {
                cleanupPeer();
                isAIMode = true;
                isOnlineMode = false;
                localMark = 'X';
                gameActive = true;
                turn = 'X';
                modeLabel.textContent = '๐Ÿค– vs AI';
                connectionStatus.textContent = 'โšช single player';
                connectionStatus.style.color = '#6b7f9c';
                peerIdDisplay.textContent = '๐Ÿ”‘ AI mode';
                queueStatus.style.display = 'none';
                statusMsg.textContent = '๐ŸŽฎ You are X, AI is O';
                statusMsg.classList.remove('win', 'waiting');
                resetGame();
                onlineBtn.disabled = false;
                aiBtn.disabled = true;
            }

            // ---------- Online Mode ----------
            function startOnlineMode() {
                cleanupPeer();
                isAIMode = false;
                isOnlineMode = true;
                localMark = 'X';
                gameActive = false;
                modeLabel.textContent = '๐ŸŒ online';
                connectionStatus.textContent = '๐ŸŸก matching...';
                connectionStatus.style.color = '#f5c542';
                queueStatus.style.display = 'inline-block';
                queueStatus.textContent = 'โณ searching...';
                statusMsg.textContent = '๐Ÿ” Finding random opponent...';
                statusMsg.classList.add('waiting');
                statusMsg.classList.remove('win');
                peerIdDisplay.textContent = '๐Ÿ”‘ connecting...';
                aiBtn.disabled = false;
                onlineBtn.disabled = true;

                const id = 'ttt-' + Math.random().toString(36).substring(2, 10);
                peer = new Peer(id, { debug: 1 });

                peer.on('open', (pid) => {
                    myId = pid;
                    peerIdDisplay.textContent = `๐Ÿ”‘ ${pid}`;
                    // Try to connect to a well-known "lobby" peer
                    // If fails, become host
                    const lobbyId = 'ttt-lobby';
                    const lobbyConn = peer.connect(lobbyId, { reliable: true });
                    let connectedToLobby = false;
                    lobbyConn.on('open', () => {
                        connectedToLobby = true;
                        // We connected to lobby, send our ID
                        lobbyConn.send({ type: 'register', id: pid });
                        // Lobby will pair us with another player
                        // But we need a lobby server. Since we don't have one,
                        // we'll use a simpler approach: we'll become host and wait.
                        // Let's just become host.
                        becomeHost();
                    });
                    lobbyConn.on('error', () => {
                        // Lobby not available, become host
                        if (!connectedToLobby) {
                            becomeHost();
                        }
                    });
                    // Also listen for incoming connections (in case we become host)
                    peer.on('connection', (incoming) => {
                        if (conn) {
                            incoming.close();
                            return;
                        }
                        conn = incoming;
                        setupConnection(conn, 'host');
                        queueStatus.style.display = 'none';
                        statusMsg.textContent = 'โœ… Opponent connected!';
                        statusMsg.classList.remove('waiting', 'win');
                        onlineBtn.disabled = false;
                    });
                    // Timeout: if no connection after 5s, we become host
                    setTimeout(() => {
                        if (!conn && !connectedToLobby) {
                            becomeHost();
                        }
                    }, 3000);
                });

                peer.on('error', (err) => {
                    console.warn('Peer error:', err);
                    onlineBtn.disabled = false;
                    queueStatus.style.display = 'none';
                    statusMsg.textContent = 'โš ๏ธ Error: ' + err.message;
                    statusMsg.classList.remove('waiting');
                    connectionStatus.textContent = 'โŒ error';
                    connectionStatus.style.color = '#f28b82';
                });
            }

            function becomeHost() {
                isHost = true;
                localMark = 'X';
                gameActive = true;
                turn = 'X';
                statusMsg.textContent = '๐ŸŸข Hosting. Share your ID!';
                statusMsg.classList.remove('waiting');
                statusMsg.classList.add('win');
                queueStatus.style.display = 'none';
                connectionStatus.textContent = '๐ŸŸข hosting';
                connectionStatus.style.color = '#81c995';
                peerIdDisplay.textContent = `๐Ÿ”‘ ${myId}`;
                // listen for incoming (already set)
                resetGame();
            }

            // ---------- Setup connection ----------
            function setupConnection(connection, role) {
                connection.on('open', () => {
                    connectionStatus.textContent = '๐ŸŸข connected';
                    connectionStatus.style.color = '#81c995';
                    statusMsg.textContent = 'โœ… Connected! X goes first.';
                    statusMsg.classList.remove('waiting', 'win');
                    if (role === 'host') {
                        localMark = 'X';
                    } else {
                        localMark = 'O';
                    }
                    turn = 'X';
                    gameActive = true;
                    resetGame();
                    connection.send({ type: 'handshake', mark: localMark, turn: 'X' });
                    renderBoard();
                    queueStatus.style.display = 'none';
                    onlineBtn.disabled = false;
                });

                connection.on('data', (data) => {
                    handleRemoteData(data);
                });

                connection.on('close', () => {
                    connectionStatus.textContent = '๐Ÿ”ด disconnected';
                    connectionStatus.style.color = '#f28b82';
                    statusMsg.textContent = 'โš ๏ธ Opponent left';
                    gameActive = false;
                    conn = null;
                    renderBoard();
                    queueStatus.style.display = 'none';
                    onlineBtn.disabled = false;
                });

                connection.on('error', (e) => {
                    console.warn('conn error', e);
                    connectionStatus.textContent = 'โš ๏ธ connection error';
                    connectionStatus.style.color = '#f28b82';
                });
            }

            // ---------- Handle remote data ----------
            function handleRemoteData(data) {
                if (!data) return;
                switch (data.type) {
                    case 'handshake': {
                        const remoteMark = (data.mark === 'X' ? 'O' : 'X');
                        localMark = remoteMark === 'X' ? 'O' : 'X';
                        turn = data.turn || 'X';
                        gameActive = true;
                        resetGame();
                        break;
                    }
                    case 'move': {
                        if (!gameActive) break;
                        const { index, player } = data;
                        if (board[index] !== null) break;
                        board[index] = player;
                        const win = checkWinner();
                        if (win) {
                            winCombo = win;
                            gameActive = false;
                            renderBoard();
                            statusMsg.textContent = `๐Ÿ† ${player} wins!`;
                            statusMsg.classList.add('win');
                            statusMsg.classList.remove('waiting');
                            turnBadge.textContent = '๐Ÿ';
                            turnBadge.className = 'player-badge';
                            return;
                        } else if (board.every(c => c !== null)) {
                            gameActive = false;
                            winCombo = null;
                            renderBoard();
                            statusMsg.textContent = `๐Ÿค It's a draw!`;
                            statusMsg.classList.add('win');
                            statusMsg.classList.remove('waiting');
                            turnBadge.textContent = 'โš–๏ธ';
                            turnBadge.className = 'player-badge';
                            return;
                        }
                        if (data.turn) turn = data.turn;
                        else turn = (turn === 'X' ? 'O' : 'X');
                        renderBoard();
                        statusMsg.textContent = `โ–ถ๏ธ ${turn}'s turn`;
                        statusMsg.classList.remove('win', 'waiting');
                        break;
                    }
                    case 'reset':
                        resetGame();
                        break;
                    case 'win': {
                        gameActive = false;
                        winCombo = data.combo || null;
                        renderBoard();
                        statusMsg.textContent = `๐Ÿ† ${data.winner} wins!`;
                        statusMsg.classList.add('win');
                        statusMsg.classList.remove('waiting');
                        turnBadge.textContent = '๐Ÿ';
                        turnBadge.className = 'player-badge';
                        break;
                    }
                    case 'draw': {
                        gameActive = false;
                        winCombo = null;
                        renderBoard();
                        statusMsg.textContent = `๐Ÿค It's a draw!`;
                        statusMsg.classList.add('win');
                        statusMsg.classList.remove('waiting');
                        turnBadge.textContent = 'โš–๏ธ';
                        turnBadge.className = 'player-badge';
                        break;
                    }
                    default:
                        break;
                }
            }

            // ---------- Cleanup ----------
            function cleanupPeer() {
                if (peer) {
                    peer.destroy();
                    peer = null;
                }
                if (conn) {
                    conn.close();
                    conn = null;
                }
                gameActive = false;
                queueStatus.style.display = 'none';
                onlineBtn.disabled = false;
                aiBtn.disabled = false;
            }

            // ---------- Buttons ----------
            aiBtn.addEventListener('click', startAIMode);

            onlineBtn.addEventListener('click', startOnlineMode);

            resetBtn.addEventListener('click', () => {
                if (isOnlineMode && !conn) {
                    statusMsg.textContent = 'โ›” No opponent connected';
                    return;
                }
                resetGame();
            });

            // initial state
            renderBoard();
            statusMsg.textContent = '๐ŸŽฏ Choose a mode';
            connectionStatus.textContent = 'โšช disconnected';
            connectionStatus.style.color = '#6b7f9c';
            queueStatus.style.display = 'none';
            modeLabel.textContent = 'โšก idle';
            peerIdDisplay.textContent = '๐Ÿ”‘ ready';

            // cleanup on unload
            window.addEventListener('beforeunload', () => {
                if (peer) peer.destroy();
                if (conn) conn.close();
            });
        })();
    </script>
</body>
</html>

Game Source: ๐ŸŽฎ Tic-Tac-Toe ยท 1P + Online Random

Creator: SonicBear35

Libraries: none

Complexity: complex (802 lines, 29.5 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-1p-online-random-sonicbear35" to link back to the original. Then publish at arcadelab.ai/publish.