๐ Christmas Tic Tac Toe ยท Online ๐
by SonicBear35949 lines36.6 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>๐ Christmas Tic Tac Toe ยท Online ๐
</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
min-height: 100vh;
font-family: 'Segoe UI', 'Arial', system-ui, sans-serif;
touch-action: manipulation;
overflow: hidden;
background: #0a1a0a;
}
.video-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
object-fit: cover;
background: #0a1a0a;
}
.snow-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.3) 100%);
}
.game-wrapper {
position: relative;
z-index: 2;
background: rgba(10, 26, 10, 0.88);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
padding: 16px 14px 20px;
border-radius: 48px 48px 32px 32px;
box-shadow: 0 20px 40px rgba(0,0,0,0.9);
max-width: 520px;
width: 100%;
margin: 10px auto;
border: 2px solid rgba(42, 90, 42, 0.6);
max-height: 98vh;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px 10px 4px;
color: #f0e8d5;
font-weight: bold;
flex-wrap: wrap;
gap: 6px;
border-bottom: 2px solid rgba(42, 90, 42, 0.5);
padding-bottom: 10px;
}
.title {
font-size: 1.2rem;
background: linear-gradient(90deg, #ff4444, #ff6b6b, #ffd700, #ff6b6b, #ff4444);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmer 3s linear infinite;
font-weight: 900;
letter-spacing: 1px;
}
@keyframes shimmer {
0% { background-position: 0% center; }
100% { background-position: 200% center; }
}
.turn-indicator {
background: rgba(13, 31, 13, 0.8);
padding: 4px 12px;
border-radius: 40px;
font-size: 0.95rem;
border: 1px solid rgba(42, 90, 42, 0.5);
color: #f0e8d5;
}
.turn-indicator span {
color: #ffd700;
}
.score-display {
display: flex;
gap: 8px;
background: rgba(13, 31, 13, 0.8);
padding: 4px 10px;
border-radius: 40px;
border: 1px solid rgba(42, 90, 42, 0.5);
}
.score-item {
display: flex;
align-items: center;
gap: 3px;
font-size: 0.85rem;
color: #c8d8c8;
}
.score-item .score-num {
color: #ffd700;
font-weight: bold;
}
.mode-selector {
display: flex;
justify-content: center;
gap: 8px;
margin: 10px 0 12px 0;
flex-wrap: wrap;
}
.mode-btn {
background: rgba(13, 31, 13, 0.7);
border: 2px solid rgba(42, 90, 42, 0.5);
color: #8aaa8a;
padding: 5px 14px;
border-radius: 30px;
font-size: 0.8rem;
font-weight: bold;
cursor: pointer;
transition: all 0.25s ease;
touch-action: manipulation;
}
.mode-btn.active {
background: rgba(42, 74, 42, 0.8);
border-color: #ffd700;
color: #ffd700;
box-shadow: 0 0 25px rgba(255, 215, 0, 0.15);
}
.mode-btn:active {
transform: scale(0.95);
}
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
aspect-ratio: 1 / 1;
width: 100%;
background: rgba(13, 31, 13, 0.6);
border-radius: 28px;
padding: 10px;
box-shadow: inset 0 0 0 3px rgba(42, 90, 42, 0.5), 0 8px 0 rgba(10, 26, 10, 0.5);
}
.cell {
background: rgba(26, 48, 26, 0.7);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: bold;
cursor: pointer;
transition: all 0.15s ease;
touch-action: manipulation;
box-shadow: inset 0 -4px 0 rgba(10, 26, 10, 0.5);
border: 1px solid rgba(42, 90, 42, 0.4);
aspect-ratio: 1 / 1;
color: #f0e8d5;
}
.cell:active {
transform: scale(0.92);
}
.cell.x {
color: #5fc3e4;
text-shadow: 0 0 30px rgba(95, 195, 228, 0.4);
}
.cell.o {
color: #ffd700;
text-shadow: 0 0 30px rgba(255, 215, 0, 0.4);
}
.cell.win-cell {
background: rgba(42, 90, 42, 0.7);
box-shadow: 0 0 40px rgba(255, 215, 0, 0.2);
border-color: #ffd700;
}
.cell.draw-cell {
background: rgba(58, 42, 42, 0.6);
border-color: rgba(138, 106, 106, 0.5);
}
.cell.disabled {
cursor: default;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 12px;
flex-wrap: wrap;
}
.ctrl-btn {
background: rgba(42, 74, 42, 0.8);
border: none;
border-bottom: 5px solid rgba(10, 26, 10, 0.6);
color: #f0e8d5;
font-size: 0.9rem;
padding: 8px 18px;
border-radius: 40px;
box-shadow: 0 3px 0 rgba(10, 26, 10, 0.5);
transition: all 0.06s ease;
touch-action: manipulation;
cursor: pointer;
font-weight: bold;
min-width: 70px;
text-align: center;
border: 1px solid rgba(58, 106, 58, 0.4);
}
.ctrl-btn:active {
transform: translateY(3px);
border-bottom-width: 2px;
}
.ctrl-btn.reset-btn {
background: rgba(74, 42, 42, 0.8);
border-color: rgba(106, 58, 58, 0.5);
color: #ffdbb5;
}
.ctrl-btn.online-btn {
background: rgba(42, 74, 74, 0.8);
border-color: rgba(58, 106, 106, 0.5);
color: #b5ffdb;
}
.message {
text-align: center;
font-size: 1rem;
font-weight: bold;
color: #ffd700;
margin-top: 8px;
min-height: 2rem;
padding: 0 6px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.15);
}
.online-status {
text-align: center;
font-size: 0.8rem;
color: #8aaa8a;
padding: 4px;
margin-top: 4px;
background: rgba(13, 31, 13, 0.5);
border-radius: 20px;
}
.online-status .connected {
color: #7bed9f;
}
.online-status .disconnected {
color: #ff6b6b;
}
.cup-display {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 6px;
padding: 6px;
background: rgba(13, 31, 13, 0.5);
border-radius: 20px;
border: 1px solid rgba(42, 90, 42, 0.3);
}
.cup-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.85rem;
color: #c8d8c8;
}
.cup-item .cup-emoji {
font-size: 1.4rem;
}
.cup-item .cup-score {
color: #ffd700;
font-weight: bold;
}
.cup-item.winner {
background: rgba(255, 215, 0, 0.15);
padding: 2px 12px;
border-radius: 20px;
border: 1px solid #ffd700;
}
.audio-btn {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 10;
background: rgba(13, 31, 13, 0.85);
border: 2px solid rgba(42, 90, 42, 0.5);
color: #ffd700;
font-size: 1.6rem;
padding: 10px 14px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
touch-action: manipulation;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
}
.audio-btn:active {
transform: scale(0.9);
}
.room-input {
display: flex;
gap: 6px;
justify-content: center;
margin: 6px 0;
flex-wrap: wrap;
}
.room-input input {
background: rgba(13, 31, 13, 0.8);
border: 2px solid rgba(42, 90, 42, 0.5);
color: #f0e8d5;
padding: 6px 12px;
border-radius: 30px;
font-size: 0.85rem;
width: 120px;
text-align: center;
outline: none;
}
.room-input input:focus {
border-color: #ffd700;
}
.room-input .ctrl-btn {
padding: 6px 14px;
font-size: 0.8rem;
min-width: 60px;
}
@media (max-width: 480px) {
.cell { font-size: 2.4rem; }
.header { font-size: 0.8rem; }
.turn-indicator { font-size: 0.85rem; padding: 3px 10px; }
.ctrl-btn { padding: 6px 12px; font-size: 0.8rem; min-width: 55px; }
.title { font-size: 1rem; }
.mode-btn { padding: 4px 10px; font-size: 0.7rem; }
.game-wrapper { padding: 12px 10px 16px; }
.audio-btn { width: 44px; height: 44px; font-size: 1.2rem; bottom: 10px; right: 10px; }
.board { padding: 8px; gap: 6px; }
.cup-item { font-size: 0.75rem; }
.cup-item .cup-emoji { font-size: 1.1rem; }
}
@media (max-width: 380px) {
.cell { font-size: 2rem; }
.board { gap: 5px; padding: 6px; }
}
</style>
</head>
<body>
<video class="video-bg" id="bgVideo" autoplay loop muted playsinline>
<source src="https://assets.mixkit.co/videos/preview/mixkit-snowy-christmas-tree-and-lights-3287-large.mp4" type="video/mp4">
</video>
<div class="snow-overlay"></div>
<audio id="jingleBells" loop preload="auto">
<source src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" type="audio/mpeg">
</audio>
<button class="audio-btn" id="audioToggle">๐</button>
<div class="game-wrapper">
<div class="header">
<div class="title">๐ X & O ยท Christmas ๐
</div>
<div class="turn-indicator">๐ฏ <span id="turnDisplay">X's turn</span></div>
<div class="score-display">
<div class="score-item">โ <span class="score-num" id="scoreX">0</span></div>
<div class="score-item">โญ <span class="score-num" id="scoreO">0</span></div>
<div class="score-item">๐ <span class="score-num" id="scoreDraw">0</span></div>
</div>
</div>
<div class="mode-selector">
<button class="mode-btn active" id="modeOffline" data-mode="offline">๐ฑ Offline</button>
<button class="mode-btn" id="modeOnline" data-mode="online">๐ Online</button>
</div>
<div id="onlinePanel" style="display:none;">
<div class="room-input">
<input type="text" id="roomInput" placeholder="Room code" value="xmas2024">
<button class="ctrl-btn online-btn" id="joinBtn">Join</button>
<button class="ctrl-btn online-btn" id="hostBtn">Host</button>
</div>
<div class="online-status" id="onlineStatus">๐ด Disconnected ยท Enter room code</div>
</div>
<div class="board" id="board">
<div class="cell" data-index="0"></div>
<div class="cell" data-index="1"></div>
<div class="cell" data-index="2"></div>
<div class="cell" data-index="3"></div>
<div class="cell" data-index="4"></div>
<div class="cell" data-index="5"></div>
<div class="cell" data-index="6"></div>
<div class="cell" data-index="7"></div>
<div class="cell" data-index="8"></div>
</div>
<div id="messageBox" class="message">๐ X goes first! Merry Christmas! ๐</div>
<div class="cup-display" id="cupDisplay">
<div class="cup-item" id="cupX">๐ <span class="cup-emoji">โ</span> <span class="cup-score" id="cupScoreX">0</span></div>
<div class="cup-item" id="cupO">๐ <span class="cup-emoji">โญ</span> <span class="cup-score" id="cupScoreO">0</span></div>
</div>
<div class="controls">
<button class="ctrl-btn reset-btn" id="resetBtn">๐ New Game</button>
<button class="ctrl-btn reset-btn" id="resetCupBtn">๐ Reset Cup</button>
</div>
</div>
<script>
(function() {
// ---------- Audio ----------
const audio = document.getElementById('jingleBells');
const audioToggle = document.getElementById('audioToggle');
let audioPlaying = false;
audio.load();
audioToggle.addEventListener('click', (e) => {
e.stopPropagation();
if (audioPlaying) {
audio.pause();
audioPlaying = false;
audioToggle.textContent = '๐';
} else {
audio.play().then(() => {
audioPlaying = true;
audioToggle.textContent = '๐';
}).catch(() => {});
}
});
function tryAutoPlay() {
if (!audioPlaying && audio.paused) {
audio.play().then(() => {
audioPlaying = true;
audioToggle.textContent = '๐';
}).catch(() => {});
}
}
document.addEventListener('click', tryAutoPlay, { once: true });
document.addEventListener('touchstart', tryAutoPlay, { once: true });
// ---------- DOM ----------
const cells = document.querySelectorAll('.cell');
const turnDisplay = document.getElementById('turnDisplay');
const messageBox = document.getElementById('messageBox');
const scoreX = document.getElementById('scoreX');
const scoreO = document.getElementById('scoreO');
const scoreDraw = document.getElementById('scoreDraw');
const resetBtn = document.getElementById('resetBtn');
const resetCupBtn = document.getElementById('resetCupBtn');
const modeOffline = document.getElementById('modeOffline');
const modeOnline = document.getElementById('modeOnline');
const onlinePanel = document.getElementById('onlinePanel');
const onlineStatus = document.getElementById('onlineStatus');
const roomInput = document.getElementById('roomInput');
const joinBtn = document.getElementById('joinBtn');
const hostBtn = document.getElementById('hostBtn');
const cupScoreX = document.getElementById('cupScoreX');
const cupScoreO = document.getElementById('cupScoreO');
const cupX = document.getElementById('cupX');
const cupO = document.getElementById('cupO');
// ---------- Game State ----------
let board = ['', '', '', '', '', '', '', '', ''];
let currentPlayer = 'X';
let gameActive = true;
let winner = null;
let scores = { X: 0, O: 0, draw: 0 };
let cupScores = { X: 0, O: 0 };
let gameMode = 'offline';
let aiThinking = false;
let isOnline = false;
let isHost = false;
let peerConnection = null;
let dataChannel = null;
let roomCode = '';
const winPatterns = [
[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 christmasMessages = [
'๐ Merry Christmas!', '๐
Ho Ho Ho!', 'โ Let it snow!',
'๐ Star of wonder!', '๐ Gift of victory!', 'โจ Magical move!',
'โ๏ธ Winter wonderland!', '๐ Jingle bells!', 'โ๏ธ Frosty is happy!'
];
function getRandomChristmasMessage() {
return christmasMessages[Math.floor(Math.random() * christmasMessages.length)];
}
// ---------- Cup System ----------
function updateCupDisplay() {
cupScoreX.textContent = cupScores.X;
cupScoreO.textContent = cupScores.O;
cupX.classList.toggle('winner', cupScores.X >= 2);
cupO.classList.toggle('winner', cupScores.O >= 2);
if (cupScores.X >= 2) {
messageBox.textContent = `๐ X WINS THE CUP! ๐ ${getRandomChristmasMessage()}`;
} else if (cupScores.O >= 2) {
messageBox.textContent = `๐ O WINS THE CUP! ๐ ${getRandomChristmasMessage()}`;
}
}
function resetCup() {
cupScores = { X: 0, O: 0 };
updateCupDisplay();
if (!cupScores.X >= 2 && !cupScores.O >= 2) {
messageBox.textContent = `๐ Cup reset! First to 2 wins! ๐`;
}
}
function awardCupPoint(player) {
cupScores[player]++;
updateCupDisplay();
if (cupScores[player] >= 2) {
messageBox.textContent = `๐ ${player} WINS THE CUP! ๐ ${getRandomChristmasMessage()}`;
// Reset game but keep cup
}
// Send cup update to peer
if (isOnline && dataChannel && dataChannel.readyState === 'open') {
dataChannel.send(JSON.stringify({ type: 'cup', data: cupScores }));
}
}
// ---------- Render ----------
function renderBoard() {
cells.forEach((cell, index) => {
cell.textContent = board[index];
cell.className = 'cell';
if (board[index] === 'X') cell.classList.add('x');
if (board[index] === 'O') cell.classList.add('o');
if (!gameActive || board[index] !== '') {
cell.classList.add('disabled');
} else {
cell.classList.remove('disabled');
}
});
if (winner && winner !== 'draw') {
const winPattern = winPatterns.find(pattern => {
const [a, b, c] = pattern;
return board[a] === winner && board[b] === winner && board[c] === winner;
});
if (winPattern) {
winPattern.forEach(idx => cells[idx].classList.add('win-cell'));
}
} else if (winner === 'draw') {
cells.forEach(cell => cell.classList.add('draw-cell'));
}
if (!gameActive) {
if (winner === 'draw') {
turnDisplay.textContent = "๐ค Draw! ๐";
} else if (winner) {
turnDisplay.textContent = `๐ ${winner} wins! โญ`;
}
} else {
turnDisplay.textContent = `${currentPlayer}'s turn`;
}
scoreX.textContent = scores.X;
scoreO.textContent = scores.O;
scoreDraw.textContent = scores.draw;
updateCupDisplay();
// Also update message if not already showing cup win
if (!gameActive && winner && winner !== 'draw') {
messageBox.textContent = `๐ ${winner} wins! ${getRandomChristmasMessage()}`;
} else if (!gameActive && winner === 'draw') {
messageBox.textContent = "๐ค It's a draw! Click New Game ๐
";
} else if (gameActive) {
messageBox.textContent = `${currentPlayer}'s turn ยท ${getRandomChristmasMessage()}`;
}
}
// ---------- Game Logic ----------
function checkWinner() {
for (let pattern of winPatterns) {
const [a, b, c] = pattern;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return board[a];
}
}
if (board.every(cell => cell !== '')) return 'draw';
return null;
}
function makeMove(index) {
if (!gameActive || board[index] !== '' || aiThinking) return false;
if (isOnline) {
// In online mode, only allow if it's our turn and we're not waiting
if (currentPlayer !== (isHost ? 'X' : 'O')) return false;
}
board[index] = currentPlayer;
const result = checkWinner();
if (result) {
gameActive = false;
winner = result;
if (result === 'draw') {
scores.draw++;
} else {
scores[result]++;
awardCupPoint(result);
}
renderBoard();
// Send move to peer
if (isOnline && dataChannel && dataChannel.readyState === 'open') {
dataChannel.send(JSON.stringify({ type: 'move', index, player: currentPlayer, board, gameActive, winner, scores, cupScores }));
}
return true;
}
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
renderBoard();
// Send move to peer
if (isOnline && dataChannel && dataChannel.readyState === 'open') {
dataChannel.send(JSON.stringify({ type: 'move', index, player: currentPlayer === 'X' ? 'O' : 'X', board, gameActive, winner, scores, cupScores }));
}
// AI for offline mode
if (gameMode === 'offline' && gameActive && currentPlayer === 'O') {
scheduleAI();
}
return true;
}
// ---------- AI (Offline) ----------
function scheduleAI() {
if (aiThinking || !gameActive || currentPlayer !== 'O' || gameMode !== 'offline') return;
aiThinking = true;
setTimeout(() => {
if (!gameActive || currentPlayer !== 'O') {
aiThinking = false;
return;
}
const empty = board.map((v, i) => v === '' ? i : null).filter(v => v !== null);
if (empty.length === 0) { aiThinking = false; return; }
// Easy AI: random
const move = empty[Math.floor(Math.random() * empty.length)];
if (move !== null && board[move] === '') {
board[move] = 'O';
const result = checkWinner();
if (result) {
gameActive = false;
winner = result;
if (result === 'draw') scores.draw++;
else { scores[result]++; awardCupPoint(result); }
renderBoard();
aiThinking = false;
return;
}
currentPlayer = 'X';
renderBoard();
}
aiThinking = false;
}, 250);
}
// ---------- Reset ----------
function resetGame() {
board = ['', '', '', '', '', '', '', '', ''];
currentPlayer = 'X';
gameActive = true;
winner = null;
aiThinking = false;
renderBoard();
if (!cupScores.X >= 2 && !cupScores.O >= 2) {
messageBox.textContent = `๐ New game! X goes first! ${getRandomChristmasMessage()}`;
}
if (isOnline && dataChannel && dataChannel.readyState === 'open') {
dataChannel.send(JSON.stringify({ type: 'reset', board, currentPlayer, gameActive, winner, scores, cupScores }));
}
}
// ---------- Mode Switching ----------
function setMode(mode) {
gameMode = mode;
modeOffline.classList.toggle('active', mode === 'offline');
modeOnline.classList.toggle('active', mode === 'online');
onlinePanel.style.display = mode === 'online' ? 'block' : 'none';
if (mode === 'offline') {
isOnline = false;
if (peerConnection) { peerConnection.close(); peerConnection = null; }
if (dataChannel) { dataChannel.close(); dataChannel = null; }
onlineStatus.innerHTML = '๐ด Offline mode';
resetGame();
} else {
onlineStatus.innerHTML = '๐ด Disconnected ยท Enter room code';
}
}
// ---------- Online: WebRTC with simple signaling ----------
// Using a simple signaling approach with room codes
// For production, you'd use a signaling server. This uses a simple broadcast approach.
function createPeerConnection(room, isHosting) {
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
const pc = new RTCPeerConnection(configuration);
let dc = null;
if (isHosting) {
dc = pc.createDataChannel('game');
dc.onopen = () => {
onlineStatus.innerHTML = '๐ข Connected as Host (X)';
isOnline = true;
isHost = true;
currentPlayer = 'X';
resetGame();
// Send initial state
dc.send(JSON.stringify({ type: 'init', isHost: true, cupScores }));
};
dc.onmessage = (e) => handleDataMessage(e.data);
dc.onclose = () => {
onlineStatus.innerHTML = '๐ด Disconnected';
isOnline = false;
isHost = false;
};
} else {
pc.ondatachannel = (e) => {
dc = e.channel;
dc.onopen = () => {
onlineStatus.innerHTML = '๐ข Connected as Guest (O)';
isOnline = true;
isHost = false;
currentPlayer = 'O';
resetGame();
// Request state
dc.send(JSON.stringify({ type: 'init', isHost: false, cupScores }));
};
dc.onmessage = (e) => handleDataMessage(e.data);
dc.onclose = () => {
onlineStatus.innerHTML = '๐ด Disconnected';
isOnline = false;
isHost = false;
};
};
}
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'disconnected' || pc.iceConnectionState === 'failed') {
onlineStatus.innerHTML = '๐ด Connection lost';
isOnline = false;
}
};
return { pc, dc };
}
function handleDataMessage(data) {
try {
const msg = JSON.parse(data);
if (msg.type === 'move') {
// Update game state from peer
board = msg.board;
currentPlayer = msg.player;
gameActive = msg.gameActive;
winner = msg.winner;
scores = msg.scores;
cupScores = msg.cupScores || cupScores;
renderBoard();
if (!gameActive && winner && winner !== 'draw') {
messageBox.textContent = `๐ ${winner} wins! ${getRandomChristmasMessage()}`;
}
} else if (msg.type === 'reset') {
board = msg.board;
currentPlayer = msg.currentPlayer;
gameActive = msg.gameActive;
winner = msg.winner;
scores = msg.scores;
cupScores = msg.cupScores || cupScores;
renderBoard();
if (!cupScores.X >= 2 && !cupScores.O >= 2) {
messageBox.textContent = `๐ Game reset! ${getRandomChristmasMessage()}`;
}
} else if (msg.type === 'cup') {
cupScores = msg.data;
updateCupDisplay();
} else if (msg.type === 'init') {
cupScores = msg.cupScores || cupScores;
updateCupDisplay();
if (!msg.isHost) {
// Guest initial sync
resetGame();
}
}
} catch (e) {
console.log('Invalid message:', data);
}
}
// ---------- Signaling (simplified) ----------
// For a real implementation, you'd use a signaling server.
// This version uses a manual offer/answer exchange via copy-paste.
// But we'll implement a simplified version using a public broadcast channel.
// We'll use a simple approach: host creates offer, guest answers
let signalingChannel = null;
function startSignaling(room, isHosting) {
// Use BroadcastChannel for same-tab communication (demo)
// For real online play, use a WebSocket server
try {
signalingChannel = new BroadcastChannel('tictactoe_' + room);
} catch (e) {
onlineStatus.innerHTML = 'โ ๏ธ BroadcastChannel not supported. Use same tab or WebSocket server.';
return;
}
const { pc, dc } = createPeerConnection(room, isHosting);
peerConnection = pc;
dataChannel = dc;
if (isHosting) {
// Create offer
pc.createOffer().then(offer => pc.setLocalDescription(offer))
.then(() => {
signalingChannel.postMessage({ type: 'offer', sdp: pc.localDescription, room });
});
} else {
// Guest waits for offer
signalingChannel.onmessage = (e) => {
const msg = e.data;
if (msg.type === 'offer' && msg.room === room) {
pc.setRemoteDescription(new RTCSessionDescription(msg.sdp))
.then(() => pc.createAnswer())
.then(answer => pc.setLocalDescription(answer))
.then(() => {
signalingChannel.postMessage({ type: 'answer', sdp: pc.localDescription, room });
});
} else if (msg.type === 'answer' && msg.room === room) {
pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
}
};
}
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (peerConnection) peerConnection.close();
if (signalingChannel) signalingChannel.close();
});
}
// ---------- Online Actions ----------
function hostGame() {
const room = roomInput.value.trim() || 'xmas2024';
roomCode = room;
onlineStatus.innerHTML = '๐ก Hosting room: ' + room + ' (waiting for guest...)';
startSignaling(room, true);
}
function joinGame() {
const room = roomInput.value.trim() || 'xmas2024';
roomCode = room;
onlineStatus.innerHTML = '๐ก Joining room: ' + room + ' (waiting for host...)';
startSignaling(room, false);
}
// ---------- Events ----------
cells.forEach((cell, index) => {
const handler = (e) => {
e.preventDefault();
if (aiThinking) return;
if (isOnline && !dataChannel) return;
if (isOnline && dataChannel && dataChannel.readyState !== 'open') {
onlineStatus.innerHTML = '๐ด Not connected';
return;
}
if (isOnline && currentPlayer !== (isHost ? 'X' : 'O')) {
onlineStatus.innerHTML = 'โณ Waiting for opponent...';
return;
}
makeMove(index);
};
cell.addEventListener('click', handler);
cell.addEventListener('touchstart', handler, { passive: false });
});
resetBtn.addEventListener('click', resetGame);
resetCupBtn.addEventListener('click', resetCup);
modeOffline.addEventListener('click', () => setMode('offline'));
modeOnline.addEventListener('click', () => setMode('online'));
hostBtn.addEventListener('click', () => {
if (gameMode !== 'online') setMode('online');
hostGame();
});
joinBtn.addEventListener('click', () => {
if (gameMode !== 'online') setMode('online');
joinGame();
});
// ---------- Keyboard ----------
document.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') resetGame();
if (e.key === 'c' || e.key === 'C') resetCup();
if (e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key) - 1;
if (idx >= 0 && idx < 9 && gameActive) {
if (aiThinking) return;
if (isOnline && currentPlayer !== (isHost ? 'X' : 'O')) return;
makeMove(idx);
}
}
});
// ---------- Start ----------
setMode('offline');
renderBoard();
messageBox.textContent = "๐ X goes first! Merry Christmas! ๐";
})();
</script>
</body>
</html>Game Source: ๐ Christmas Tic Tac Toe ยท Online ๐
Creator: SonicBear35
Libraries: none
Complexity: complex (949 lines, 36.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: christmas-tic-tac-toe-sonicbear35" to link back to the original. Then publish at arcadelab.ai/publish.