๐ฎ Tic-Tac-Toe ยท 1P + Online Random
by SonicBear35802 lines29.5 KB
<!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.