🎮 Shahab Game
by LuckyOwl261259 lines46.3 KB
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎮 Shahab Game</title>
<!-- Firebase SDK -->
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0a0e1a;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
position: relative;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background:
radial-gradient(ellipse at 20% 50%, rgba(88, 50, 200, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 80% 50%, rgba(50, 100, 255, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 50% 100%, rgba(150, 50, 255, 0.1) 0%, transparent 40%);
animation: gradientShift 8s ease-in-out infinite alternate;
z-index: 0;
}
@keyframes gradientShift {
0% { transform: translate(0, 0) rotate(0deg); }
100% { transform: translate(5%, 5%) rotate(3deg); }
}
.particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.particle {
position: absolute;
width: 3px;
height: 3px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
animation: floatParticle 15s infinite linear;
}
@keyframes floatParticle {
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
}
.container {
position: relative;
z-index: 1;
background: linear-gradient(145deg, #1a1f35, #0f1428);
border-radius: 30px;
padding: 35px;
max-width: 750px;
width: 100%;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.8),
0 0 40px rgba(88, 50, 200, 0.15),
0 0 80px rgba(50, 100, 255, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
}
.header {
text-align: center;
margin-bottom: 25px;
}
.header h1 {
font-size: 2.2em;
background: linear-gradient(135deg, #a78bfa, #818cf8, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 40px rgba(167, 139, 250, 0.2);
letter-spacing: 2px;
}
.header .subtitle {
color: rgba(255, 255, 255, 0.4);
font-size: 0.9em;
margin-top: 5px;
letter-spacing: 4px;
}
/* لابی */
#lobby {
display: block;
}
.lobby-box {
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 25px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.lobby-box h2 {
color: rgba(255, 255, 255, 0.8);
text-align: center;
margin-bottom: 20px;
font-size: 1.3em;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 5px;
font-size: 14px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: white;
font-size: 16px;
transition: all 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: rgba(167, 139, 250, 0.5);
box-shadow: 0 0 20px rgba(167, 139, 250, 0.1);
}
.form-group input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.form-group select option {
background: #1a1f35;
color: white;
}
.btn-primary {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #7c3aed, #6d28d9);
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(124, 58, 237, 0.4);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.waiting-message {
text-align: center;
padding: 20px;
margin-top: 15px;
background: rgba(251, 191, 36, 0.1);
border-radius: 12px;
border: 1px solid rgba(251, 191, 36, 0.2);
display: none;
}
.waiting-message .spinner {
width: 40px;
height: 40px;
margin: 0 auto 15px;
border: 4px solid rgba(167, 139, 250, 0.1);
border-top: 4px solid #7c3aed;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.waiting-message p {
color: rgba(255, 255, 255, 0.7);
margin: 5px 0;
}
.waiting-message .hint {
font-size: 0.9em;
color: rgba(251, 191, 36, 0.6);
}
.room-code {
background: rgba(167, 139, 250, 0.1);
border: 1px solid rgba(167, 139, 250, 0.2);
border-radius: 10px;
padding: 12px;
text-align: center;
margin: 15px 0;
}
.room-code .code {
font-size: 24px;
font-weight: bold;
color: #a78bfa;
letter-spacing: 4px;
}
.room-code .label {
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
display: block;
margin-bottom: 5px;
}
/* بازی */
#gameArea {
display: none;
}
.game-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.05);
flex-wrap: wrap;
gap: 10px;
}
.players {
display: flex;
gap: 15px;
align-items: center;
}
.player-badge {
padding: 6px 16px;
border-radius: 12px;
font-weight: bold;
font-size: 14px;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.player-x {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border-color: rgba(239, 68, 68, 0.2);
}
.player-o {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
border-color: rgba(59, 130, 246, 0.2);
}
.player-active {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(167, 139, 250, 0.2);
border-color: rgba(167, 139, 250, 0.4) !important;
}
.player-x.player-active {
box-shadow: 0 0 25px rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.4) !important;
}
.player-o.player-active {
box-shadow: 0 0 25px rgba(59, 130, 246, 0.3);
border-color: rgba(59, 130, 246, 0.4) !important;
}
.turn-indicator {
padding: 6px 20px;
border-radius: 12px;
font-weight: bold;
font-size: 16px;
transition: all 0.3s ease;
background: rgba(167, 139, 250, 0.1);
color: #a78bfa;
border: 1px solid rgba(167, 139, 250, 0.2);
}
.turn-x {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border-color: rgba(239, 68, 68, 0.2);
box-shadow: 0 0 20px rgba(239, 68, 68, 0.1);
}
.turn-o {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
border-color: rgba(59, 130, 246, 0.2);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.1);
}
.btn {
padding: 8px 22px;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-reset {
background: linear-gradient(135deg, #059669, #047857);
box-shadow: 0 4px 15px rgba(5, 150, 105, 0.3);
}
.btn-reset:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(5, 150, 105, 0.4);
}
.btn-leave {
background: linear-gradient(135deg, #dc2626, #b91c1c);
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.3);
}
.btn-leave:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(220, 38, 38, 0.4);
}
.board-wrapper {
background: linear-gradient(145deg, #2c1810, #3d2317);
border-radius: 20px;
padding: 12px;
box-shadow:
inset 0 2px 20px rgba(0, 0, 0, 0.6),
0 10px 40px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.05);
position: relative;
}
.board-wrapper::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 22px;
padding: 2px;
background: linear-gradient(135deg, rgba(167, 139, 250, 0.1), rgba(96, 165, 250, 0.1));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
#boardCanvas {
cursor: pointer;
border-radius: 14px;
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
background: #d4b48c;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.2);
}
.status {
margin-top: 15px;
padding: 10px;
text-align: center;
font-size: 16px;
color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.03);
min-height: 45px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.winner-text {
font-size: 22px;
font-weight: bold;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: glowPulse 1s ease-in-out infinite alternate;
}
@keyframes glowPulse {
0% { filter: drop-shadow(0 0 10px rgba(251, 191, 36, 0.3)); }
100% { filter: drop-shadow(0 0 30px rgba(251, 191, 36, 0.6)); }
}
.footer {
margin-top: 15px;
display: flex;
justify-content: space-between;
align-items: center;
color: rgba(255, 255, 255, 0.2);
font-size: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.03);
padding-top: 12px;
flex-wrap: wrap;
gap: 5px;
}
.online-status {
display: flex;
align-items: center;
gap: 8px;
}
.online-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.online-dot.online {
background: #51cf66;
box-shadow: 0 0 10px rgba(81, 207, 102, 0.5);
}
.online-dot.offline {
background: #ff6b6b;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}
@media (max-width: 768px) {
.container { padding: 20px; border-radius: 20px; }
.header h1 { font-size: 1.6em; }
.game-info { flex-direction: column; align-items: stretch; gap: 8px; }
.players { justify-content: center; }
.turn-indicator { text-align: center; }
.btn { width: 100%; padding: 10px; }
.footer { flex-direction: column; text-align: center; }
}
@media (max-width: 480px) {
.container { padding: 12px; border-radius: 16px; }
.header h1 { font-size: 1.3em; }
.player-badge { font-size: 12px; padding: 4px 12px; }
.turn-indicator { font-size: 14px; padding: 4px 16px; }
.status { font-size: 14px; min-height: 38px; }
}
</style>
</head>
<body>
<div class="particles" id="particles"></div>
<div class="container">
<div class="header">
<h1>🌐 Shahab Game</h1>
<div class="subtitle">✦ بازی با دوستان از هر کجا ✦</div>
</div>
<!-- ========== لابی ========== -->
<div id="lobby">
<div class="lobby-box">
<h2>🚀 ورود به بازی</h2>
<div class="form-group">
<label>👤 نام شما:</label>
<input type="text" id="playerName" placeholder="نام خود را وارد کنید" value="بازیکن">
</div>
<div class="form-group">
<label>🔑 کد اتاق:</label>
<input type="text" id="roomCode" placeholder="کد اتاق را وارد کنید" value="game123">
</div>
<div class="form-group">
<label>🎯 نقش:</label>
<select id="playerRole">
<option value="auto">⚡ خودکار</option>
<option value="X">❌ بازیکن ۱ (X)</option>
<option value="O">⭕ بازیکن ۲ (O)</option>
</select>
</div>
<button class="btn-primary" onclick="joinGame()">🎮 ورود به بازی</button>
<div class="waiting-message" id="waitingMessage">
<div class="spinner"></div>
<p>⏳ در انتظار بازیکن دوم...</p>
<p class="hint">💡 کد اتاق را به دوست خود بدهید</p>
<div class="room-code">
<span class="label">📋 کد اتاق شما</span>
<span class="code" id="displayRoomCode">game123</span>
</div>
</div>
</div>
</div>
<!-- ========== صفحه بازی ========== -->
<div id="gameArea">
<div class="game-info">
<div class="players">
<span class="player-badge player-x" id="playerXBadge">❌ در انتظار...</span>
<span class="player-badge player-o" id="playerOBadge">⭕ در انتظار...</span>
</div>
<div class="turn-indicator turn-x" id="turnDisplay">⏳ در حال اتصال...</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn btn-reset" onclick="resetGame()">🔄 جدید</button>
<button class="btn btn-leave" onclick="leaveGame()">🚪 خروج</button>
</div>
</div>
<div class="board-wrapper">
<canvas id="boardCanvas"></canvas>
</div>
<div class="status" id="statusDisplay">⏳ در حال اتصال به سرور...</div>
<div class="footer">
<span>🎯 هر بازیکن به نوبت مهره میگذارد</span>
<span class="online-status">
<span class="online-dot offline" id="onlineDot"></span>
<span id="onlineText">آفلاین</span>
</span>
</div>
</div>
</div>
<script>
// ============================================
// پیکربندی Firebase
// ============================================
const firebaseConfig = {
apiKey: "AIzaSyC-vlEw3aP5Lw5r0pQxqLvJqY9XzXnZxXk",
authDomain: "gomoku-online-4a3b2.firebaseapp.com",
databaseURL: "https://gomoku-online-4a3b2-default-rtdb.firebaseio.com",
projectId: "gomoku-online-4a3b2",
storageBucket: "gomoku-online-4a3b2.appspot.com",
messagingSenderId: "123456789012",
appId: "1:123456789012:web:abcdef123456"
};
// ============================================
// مقداردهی Firebase
// ============================================
firebase.initializeApp(firebaseConfig);
const database = firebase.database();
// ============================================
// ذرات پسزمینه
// ============================================
(function createParticles() {
const container = document.getElementById('particles');
for (let i = 0; i < 25; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.left = Math.random() * 100 + '%';
p.style.width = (Math.random() * 3 + 1) + 'px';
p.style.height = p.style.width;
p.style.animationDuration = (Math.random() * 20 + 10) + 's';
p.style.animationDelay = (Math.random() * 20) + 's';
p.style.opacity = Math.random() * 0.4 + 0.1;
container.appendChild(p);
}
})();
// ============================================
// تنظیمات بازی
// ============================================
const BOARD_SIZE = 15;
const MARGIN = 25;
const PIECE_RADIUS = 15;
let board = [];
let currentPlayer = 'X';
let gameOver = false;
let winner = null;
let winningCells = [];
let moveCount = 0;
let myLetter = null;
let roomCode = '';
let playerName = '';
let gameRef = null;
let isWaiting = false;
const canvas = document.getElementById('boardCanvas');
const ctx = canvas.getContext('2d');
const turnDisplay = document.getElementById('turnDisplay');
const statusDisplay = document.getElementById('statusDisplay');
const playerXBadge = document.getElementById('playerXBadge');
const playerOBadge = document.getElementById('playerOBadge');
const onlineDot = document.getElementById('onlineDot');
const onlineText = document.getElementById('onlineText');
function resizeCanvas() {
const containerWidth = canvas.parentElement.clientWidth - 24;
const size = Math.min(containerWidth, 600);
const cellSize = Math.floor((size - MARGIN * 2) / BOARD_SIZE);
const totalSize = cellSize * BOARD_SIZE + MARGIN * 2;
canvas.width = totalSize;
canvas.height = totalSize;
window.currentCellSize = cellSize;
drawBoard();
}
// ============================================
// توابع بازی
// ============================================
function initBoard() {
board = [];
for (let i = 0; i < BOARD_SIZE; i++) {
board.push(new Array(BOARD_SIZE).fill(' '));
}
}
function checkWinner(row, col, letter) {
const directions = [[0,1],[1,0],[1,1],[1,-1]];
for (const [dr, dc] of directions) {
const cells = [[row, col]];
for (let i = 1; i < 5; i++) {
const r = row + dr * i, c = col + dc * i;
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === letter) {
cells.push([r, c]);
} else break;
}
for (let i = 1; i < 5; i++) {
const r = row - dr * i, c = col - dc * i;
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === letter) {
cells.push([r, c]);
} else break;
}
if (cells.length >= 5) {
winningCells = cells.slice(0, 5);
return true;
}
}
return false;
}
// ============================================
// رسم تخته
// ============================================
function drawBoard() {
const cellSize = window.currentCellSize || 38;
const width = canvas.width;
const height = canvas.height;
ctx.clearRect(0, 0, width, height);
const woodGradient = ctx.createLinearGradient(0, 0, width, height);
woodGradient.addColorStop(0, '#d4b48c');
woodGradient.addColorStop(0.3, '#c9a87c');
woodGradient.addColorStop(0.5, '#d4b48c');
woodGradient.addColorStop(0.7, '#bfa07a');
woodGradient.addColorStop(1, '#d4b48c');
ctx.fillStyle = woodGradient;
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.7)';
ctx.lineWidth = 1.2;
for (let i = 0; i < BOARD_SIZE; i++) {
const x = MARGIN + i * cellSize + cellSize / 2;
const y = MARGIN + i * cellSize + cellSize / 2;
ctx.beginPath();
ctx.moveTo(x, MARGIN);
ctx.lineTo(x, MARGIN + BOARD_SIZE * cellSize);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN, y);
ctx.lineTo(MARGIN + BOARD_SIZE * cellSize, y);
ctx.stroke();
}
const stars = [3, 7, 11];
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
for (const r of stars) {
for (const c of stars) {
const x = MARGIN + c * cellSize + cellSize / 2;
const y = MARGIN + r * cellSize + cellSize / 2;
ctx.shadowColor = 'rgba(0,0,0,0.2)';
ctx.shadowBlur = 5;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
ctx.shadowBlur = 0;
}
}
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (board[i][j] === ' ') continue;
const x = MARGIN + j * cellSize + cellSize / 2;
const y = MARGIN + i * cellSize + cellSize / 2;
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 3;
if (board[i][j] === 'X') {
const gradientX = ctx.createLinearGradient(x - 15, y - 15, x + 15, y + 15);
gradientX.addColorStop(0, '#f87171');
gradientX.addColorStop(0.5, '#ef4444');
gradientX.addColorStop(1, '#dc2626');
ctx.shadowColor = 'rgba(239, 68, 68, 0.4)';
ctx.shadowBlur = 15;
ctx.strokeStyle = gradientX;
ctx.lineWidth = 5;
ctx.lineCap = 'round';
const offset = PIECE_RADIUS;
ctx.beginPath();
ctx.moveTo(x - offset, y - offset);
ctx.lineTo(x + offset, y + offset);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + offset, y - offset);
ctx.lineTo(x - offset, y + offset);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 2;
const hOff = PIECE_RADIUS - 2;
ctx.beginPath();
ctx.moveTo(x - hOff, y - hOff);
ctx.lineTo(x + hOff, y + hOff);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + hOff, y - hOff);
ctx.lineTo(x - hOff, y + hOff);
ctx.stroke();
} else {
const gradientO = ctx.createRadialGradient(x - 5, y - 5, 0, x, y, PIECE_RADIUS);
gradientO.addColorStop(0, '#93c5fd');
gradientO.addColorStop(0.4, '#60a5fa');
gradientO.addColorStop(1, '#2563eb');
ctx.shadowColor = 'rgba(59, 130, 246, 0.4)';
ctx.shadowBlur = 15;
ctx.strokeStyle = gradientO;
ctx.lineWidth = 5;
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS, 0, 2 * Math.PI);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS - 2, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
ctx.beginPath();
ctx.arc(x - 5, y - 6, 4, 0, 2 * Math.PI);
ctx.fill();
}
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
}
if (winningCells.length > 0) {
ctx.shadowColor = 'rgba(251, 191, 36, 0.6)';
ctx.shadowBlur = 25;
ctx.strokeStyle = '#fbbf24';
ctx.lineWidth = 8;
ctx.lineCap = 'round';
for (let i = 0; i < winningCells.length - 1; i++) {
const [r1, c1] = winningCells[i];
const [r2, c2] = winningCells[i + 1];
const x1 = MARGIN + c1 * cellSize + cellSize / 2;
const y1 = MARGIN + r1 * cellSize + cellSize / 2;
const x2 = MARGIN + c2 * cellSize + cellSize / 2;
const y2 = MARGIN + r2 * cellSize + cellSize / 2;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 3;
for (let i = 0; i < winningCells.length - 1; i++) {
const [r1, c1] = winningCells[i];
const [r2, c2] = winningCells[i + 1];
const x1 = MARGIN + c1 * cellSize + cellSize / 2;
const y1 = MARGIN + r1 * cellSize + cellSize / 2;
const x2 = MARGIN + c2 * cellSize + cellSize / 2;
const y2 = MARGIN + r2 * cellSize + cellSize / 2;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.shadowBlur = 0;
}
}
// ============================================
// بروزرسانی UI
// ============================================
function updateUI() {
if (!myLetter) return;
if (currentPlayer === 'X') {
turnDisplay.textContent = 'نوبت: ❌';
turnDisplay.className = 'turn-indicator turn-x';
playerXBadge.classList.add('player-active');
playerOBadge.classList.remove('player-active');
} else {
turnDisplay.textContent = 'نوبت: ⭕';
turnDisplay.className = 'turn-indicator turn-o';
playerOBadge.classList.add('player-active');
playerXBadge.classList.remove('player-active');
}
const isMyTurn = (myLetter === currentPlayer && !gameOver);
canvas.style.cursor = isMyTurn ? 'pointer' : 'default';
if (gameOver) {
if (winner === myLetter) {
statusDisplay.innerHTML = '🎉 <span class="winner-text">شما برنده شدید!</span> 🎉';
} else if (winner) {
statusDisplay.innerHTML = '😢 <span style="color: #f87171;">حریف برنده شد!</span>';
} else {
statusDisplay.innerHTML = '🤝 بازی مساوی شد!';
}
return;
}
if (isMyTurn) {
statusDisplay.innerHTML = '🎯 نوبت شماست';
statusDisplay.style.color = 'rgba(167, 139, 250, 0.8)';
} else {
statusDisplay.innerHTML = '⏳ منتظر حرکت حریف...';
statusDisplay.style.color = 'rgba(255,255,255,0.4)';
}
}
// ============================================
// اتصال به Firebase
// ============================================
function joinGame() {
playerName = document.getElementById('playerName').value.trim() || 'بازیکن';
roomCode = document.getElementById('roomCode').value.trim() || 'game123';
const role = document.getElementById('playerRole').value;
if (!roomCode) {
alert('لطفاً کد اتاق را وارد کنید!');
return;
}
document.getElementById('waitingMessage').style.display = 'block';
document.querySelector('.btn-primary').disabled = true;
document.querySelector('.btn-primary').textContent = '⏳ در حال اتصال...';
gameRef = database.ref('games/' + roomCode);
gameRef.once('value', (snapshot) => {
const data = snapshot.val();
let assignedLetter = null;
if (!data) {
// بازی جدید
if (role === 'O') {
assignedLetter = 'O';
} else {
assignedLetter = 'X';
}
} else {
const players = data.players || {};
const playerCount = Object.keys(players).length;
if (playerCount >= 2) {
alert('❌ این اتاق پر است!');
resetLobby();
return;
}
if (role === 'X' && !players.X) {
assignedLetter = 'X';
} else if (role === 'O' && !players.O) {
assignedLetter = 'O';
} else if (role === 'auto') {
assignedLetter = !players.X ? 'X' : 'O';
} else {
alert('❌ نقش انتخاب شده پر است!');
resetLobby();
return;
}
}
myLetter = assignedLetter;
document.getElementById('displayRoomCode').textContent = roomCode;
// ذخیره در Firebase
const updateData = {};
updateData['players/' + assignedLetter] = {
name: playerName,
joined: Date.now()
};
if (!data) {
updateData.board = Array(15).fill().map(() => Array(15).fill(' '));
updateData.currentPlayer = 'X';
updateData.gameOver = false;
updateData.winner = null;
updateData.winningCells = [];
updateData.moveCount = 0;
}
gameRef.update(updateData);
document.getElementById('lobby').style.display = 'none';
document.getElementById('gameArea').style.display = 'block';
document.querySelector('.btn-primary').disabled = false;
document.querySelector('.btn-primary').textContent = '🎮 ورود به بازی';
initBoard();
listenToGame();
setOnlineStatus(true);
resizeCanvas();
statusDisplay.innerHTML = '⏳ در حال دریافت اطلاعات بازی...';
});
}
function resetLobby() {
document.getElementById('waitingMessage').style.display = 'none';
document.querySelector('.btn-primary').disabled = false;
document.querySelector('.btn-primary').textContent = '🎮 ورود به بازی';
}
// ============================================
// گوش دادن به تغییرات Firebase
// ============================================
function listenToGame() {
gameRef.on('value', (snapshot) => {
const data = snapshot.val();
if (!data) {
statusDisplay.innerHTML = '⚠️ بازی حذف شد!';
return;
}
// بروزرسانی تخته
if (data.board) {
board = data.board;
}
if (data.currentPlayer !== undefined) {
currentPlayer = data.currentPlayer;
}
if (data.gameOver !== undefined) {
gameOver = data.gameOver;
}
if (data.winner !== undefined) {
winner = data.winner;
}
if (data.winningCells) {
winningCells = data.winningCells || [];
}
if (data.moveCount !== undefined) {
moveCount = data.moveCount || 0;
}
// بروزرسانی بازیکنان
if (data.players) {
const players = data.players;
const playerCount = Object.keys(players).length;
playerXBadge.textContent = players.X ? `❌ ${players.X.name || 'بازیکن ۱'}` : '❌ در انتظار...';
playerOBadge.textContent = players.O ? `⭕ ${players.O.name || 'بازیکن ۲'}` : '⭕ در انتظار...';
if (playerCount < 2 && !gameOver) {
statusDisplay.innerHTML = '⏳ در انتظار بازیکن دوم...';
statusDisplay.style.color = 'rgba(251, 191, 36, 0.6)';
}
}
drawBoard();
updateUI();
// چک کردن برنده
if (gameOver && winner) {
setTimeout(() => {
const winnerName = winner === myLetter ? 'شما' : 'حریف';
if (confirm(`🎉 ${winnerName} برنده شد! بازی جدید؟`)) {
resetGame();
}
}, 500);
}
});
}
// ============================================
// حرکت دادن
// ============================================
function makeMove(row, col) {
if (gameOver) {
statusDisplay.innerHTML = '⛔ بازی تمام شده!';
return;
}
if (myLetter !== currentPlayer) {
statusDisplay.innerHTML = '⏳ نوبت شما نیست!';
return;
}
if (board[row][col] !== ' ') {
statusDisplay.innerHTML = '❌ این خانه پر است!';
return;
}
// ارسال حرکت به Firebase
const updates = {};
updates['board/' + row + '/' + col] = myLetter;
// بررسی برنده
const tempBoard = board.map(row => [...row]);
tempBoard[row][col] = myLetter;
// بررسی موقت برنده
const tempWin = checkWinnerTemp(row, col, myLetter, tempBoard);
if (tempWin) {
updates.gameOver = true;
updates.winner = myLetter;
updates.winningCells = winningCells;
} else {
const totalMoves = moveCount + 1;
if (totalMoves === BOARD_SIZE * BOARD_SIZE) {
updates.gameOver = true;
updates.winner = null;
} else {
updates.currentPlayer = (myLetter === 'X') ? 'O' : 'X';
}
updates.moveCount = totalMoves;
}
gameRef.update(updates);
}
function checkWinnerTemp(row, col, letter, tempBoard) {
const directions = [[0,1],[1,0],[1,1],[1,-1]];
for (const [dr, dc] of directions) {
const cells = [[row, col]];
for (let i = 1; i < 5; i++) {
const r = row + dr * i, c = col + dc * i;
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && tempBoard[r][c] === letter) {
cells.push([r, c]);
} else break;
}
for (let i = 1; i < 5; i++) {
const r = row - dr * i, c = col - dc * i;
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && tempBoard[r][c] === letter) {
cells.push([r, c]);
} else break;
}
if (cells.length >= 5) {
winningCells = cells.slice(0, 5);
return true;
}
}
return false;
}
// ============================================
// ریست بازی
// ============================================
function resetGame() {
if (!gameRef) return;
const updates = {};
updates.board = Array(15).fill().map(() => Array(15).fill(' '));
updates.currentPlayer = 'X';
updates.gameOver = false;
updates.winner = null;
updates.winningCells = [];
updates.moveCount = 0;
gameRef.update(updates);
statusDisplay.innerHTML = '🔄 بازی ریست شد!';
statusDisplay.style.color = 'rgba(255,255,255,0.6)';
}
// ============================================
// خروج از بازی
// ============================================
function leaveGame() {
if (gameRef && myLetter) {
const removeData = {};
removeData['players/' + myLetter] = null;
gameRef.update(removeData);
}
if (gameRef) {
gameRef.off();
gameRef = null;
}
document.getElementById('gameArea').style.display = 'none';
document.getElementById('lobby').style.display = 'block';
setOnlineStatus(false);
resetLobby();
myLetter = null;
}
// ============================================
// وضعیت آنلاین
// ============================================
function setOnlineStatus(isOnline) {
if (isOnline) {
onlineDot.className = 'online-dot online';
onlineText.textContent = 'آنلاین';
} else {
onlineDot.className = 'online-dot offline';
onlineText.textContent = 'آفلاین';
}
}
// ============================================
// رویدادهای کلیک
// ============================================
canvas.addEventListener('click', (e) => {
if (!myLetter || gameOver || myLetter !== currentPlayer) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const cellSize = window.currentCellSize || 38;
const col = Math.floor((x - MARGIN) / cellSize);
const row = Math.floor((y - MARGIN) / cellSize);
if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
makeMove(row, col);
}
});
// ============================================
// Hover Effect
// ============================================
canvas.addEventListener('mousemove', (e) => {
if (!myLetter || gameOver || myLetter !== currentPlayer) {
canvas.style.cursor = 'default';
return;
}
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const cellSize = window.currentCellSize || 38;
const col = Math.floor((x - MARGIN) / cellSize);
const row = Math.floor((y - MARGIN) / cellSize);
if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
if (board[row][col] === ' ') {
canvas.style.cursor = 'pointer';
drawBoard();
const cx = MARGIN + col * cellSize + cellSize / 2;
const cy = MARGIN + row * cellSize + cellSize / 2;
const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, PIECE_RADIUS + 8);
const color = currentPlayer === 'X' ? 'rgba(239, 68, 68, 0.15)' : 'rgba(59, 130, 246, 0.15)';
glow.addColorStop(0, color);
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(cx, cy, PIECE_RADIUS + 8, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = currentPlayer === 'X' ? 'rgba(239, 68, 68, 0.3)' : 'rgba(59, 130, 246, 0.3)';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.arc(cx, cy, PIECE_RADIUS + 2, 0, 2 * Math.PI);
ctx.stroke();
ctx.setLineDash([]);
} else {
canvas.style.cursor = 'not-allowed';
}
} else {
canvas.style.cursor = 'default';
}
});
canvas.addEventListener('mouseleave', () => {
drawBoard();
});
// ============================================
// شروع
// ============================================
window.addEventListener('resize', resizeCanvas);
initBoard();
resizeCanvas();
console.log('🌐 Gomoku Online loaded!');
</script>
</body>
</html>Game Source: 🎮 Shahab Game
Creator: LuckyOwl26
Libraries: none
Complexity: complex (1259 lines, 46.3 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: shahab-game-luckyowl26" to link back to the original. Then publish at arcadelab.ai/publish.