Spaces:
Running
Running
from flask import Flask, render_template_string, request, redirect, url_for, session | |
import random | |
import string | |
import json | |
import os | |
from flask_socketio import SocketIO, join_room, leave_room, emit | |
import hashlib | |
app = Flask(__name__) | |
app.config['SECRET_KEY'] = 'your-very-secret-key-here' | |
socketio = SocketIO(app) | |
ROOMS_DB = os.path.join(app.root_path, 'rooms.json') | |
USERS_DB = os.path.join(app.root_path, 'users.json') | |
GAMES_DB = os.path.join(app.root_path, 'games.json') | |
def load_json(file_path, default={}): | |
try: | |
if os.path.exists(file_path): | |
with open(file_path, 'r', encoding='utf-8') as f: | |
return json.load(f) | |
return default | |
except (FileNotFoundError, json.JSONDecodeError) as e: | |
print(f"Ошибка загрузки JSON из {file_path}: {e}") | |
return default | |
def save_json(file_path, data): | |
try: | |
with open(file_path, 'w', encoding='utf-8') as f: | |
json.dump(data, f, indent=4, ensure_ascii=False) | |
except OSError as e: | |
print(f"Ошибка сохранения JSON в {file_path}: {e}") | |
rooms = load_json(ROOMS_DB) | |
users = load_json(USERS_DB) | |
games_data = load_json(GAMES_DB, default={ | |
"crocodile": { | |
"name": "Крокодил", | |
"description": "Один игрок (ведущий) получает слово и должен показать его жестами остальным игрокам, не произнося ни слова. Остальные игроки пытаются угадать слово.", | |
"min_players": 2, | |
"max_players": 5, | |
"state": {} | |
}, | |
"alias": { | |
"name": "Alias", | |
"description": "Один игрок (ведущий) получает слово и должен объяснить его другими словами, не используя однокоренные. Остальные игроки пытаются угадать слово.", | |
"min_players": 2, | |
"max_players": 5, | |
"state": {} | |
}, | |
"mafia": { | |
"name": "Мафия", | |
"description": "Игроки делятся на две команды: мафию и мирных жителей. Мафия пытается тайно убивать мирных жителей, а мирные жители пытаются вычислить и казнить мафию.", | |
"min_players": 4, | |
"max_players": 5, # Можно увеличить, но для теста оставим так | |
"state": {} | |
}, | |
"durak": { | |
"name": "Дурак", | |
"description": "Карточная игра, в которой игроки стараются избавиться от всех своих карт.", | |
"min_players": 2, | |
"max_players": 5, | |
"state": {} # Состояние игры | |
} | |
}) | |
save_json(GAMES_DB, games_data) | |
def generate_token(): | |
return ''.join(random.choices(string.ascii_letters + string.digits, k=15)) | |
def hash_password(password): | |
return hashlib.sha256(password.encode('utf-8')).hexdigest() | |
def index(): | |
if 'username' in session: | |
return redirect(url_for('dashboard')) | |
if request.method == 'POST': | |
action = request.form.get('action') | |
username = request.form.get('username') | |
password = request.form.get('password') | |
if action == 'register': | |
if username in users: | |
return "Пользователь уже существует", 400 | |
users[username] = hash_password(password) | |
save_json(USERS_DB, users) | |
session['username'] = username | |
return redirect(url_for('dashboard')) | |
elif action == 'login': | |
if username in users and users[username] == hash_password(password): | |
session['username'] = username | |
return redirect(url_for('dashboard')) | |
return "Неверный логин или пароль", 401 | |
return render_template_string('''<!DOCTYPE html> | |
<html lang="ru"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Видеоконференция</title> | |
<style> | |
:root { | |
--primary-color: #6200ee; | |
--secondary-color: #3700b3; | |
--background-color: #ffffff; | |
--surface-color: #f5f5f5; | |
--text-color: #333333; | |
--error-color: #b00020; | |
--font-family: 'Roboto', sans-serif; | |
--border-radius: 12px; | |
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); | |
} | |
body { | |
font-family: var(--font-family); | |
background-color: var(--background-color); | |
color: var(--text-color); | |
margin: 0; | |
padding: 0; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 100vh; | |
} | |
.container { | |
background-color: var(--surface-color); | |
padding: 2rem; | |
border-radius: var(--border-radius); | |
box-shadow: var(--box-shadow); | |
width: 90%; | |
max-width: 400px; | |
text-align: center; | |
} | |
h1 { | |
font-size: 2rem; | |
margin-bottom: 1.5rem; | |
color: var(--primary-color); | |
} | |
input, button { | |
display: block; | |
width: 100%; | |
padding: 0.75rem; | |
margin-bottom: 1rem; | |
border: 1px solid #ccc; | |
border-radius: var(--border-radius); | |
font-size: 1rem; | |
box-sizing: border-box; | |
transition: border-color 0.3s ease; | |
} | |
input:focus { | |
outline: none; | |
border-color: var(--primary-color); | |
} | |
button { | |
background-color: var(--primary-color); | |
color: white; | |
cursor: pointer; | |
border: none; | |
font-weight: 500; | |
transition: background-color 0.3s ease; | |
} | |
button:hover { | |
background-color: var(--secondary-color); | |
} | |
button:active { | |
opacity: 0.8; | |
} | |
.error-message { | |
color: var(--error-color); | |
margin-top: 0.5rem; | |
} | |
@media (prefers-color-scheme: dark) { | |
:root { | |
--background-color: #121212; | |
--surface-color: #1e1e1e; | |
--text-color: #ffffff; | |
} | |
} | |
</style> | |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Видеоконференция</h1> | |
<form method="post"> | |
<input type="text" name="username" placeholder="Логин" required> | |
<input type="password" name="password" placeholder="Пароль" required> | |
<button type="submit" name="action" value="login">Войти</button> | |
<button type="submit" name="action" value="register">Зарегистрироваться</button> | |
</form> | |
</div> | |
</body> | |
</html>''') | |
def dashboard(): | |
if 'username' not in session: | |
return redirect(url_for('index')) | |
if request.method == 'POST': | |
action = request.form.get('action') | |
if action == 'create': | |
token = generate_token() | |
rooms[token] = {'users': [], 'max_users': 5, 'admin': session['username'], 'current_game': None} | |
save_json(ROOMS_DB, rooms) | |
return redirect(url_for('room', token=token)) | |
elif action == 'join': | |
token = request.form.get('token') | |
if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']: | |
return redirect(url_for('room', token=token)) | |
return "Комната не найдена или переполнена", 404 | |
return render_template_string('''<!DOCTYPE html> | |
<html lang="ru"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Панель управления</title> | |
<style> | |
:root { | |
--primary-color: #6200ee; | |
--secondary-color: #3700b3; | |
--background-color: #ffffff; | |
--surface-color: #f5f5f5; | |
--text-color: #333333; | |
--accent-color: #03dac6; | |
--font-family: 'Roboto', sans-serif; | |
--border-radius: 12px; | |
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); | |
} | |
body { | |
font-family: var(--font-family); | |
background-color: var(--background-color); | |
color: var(--text-color); | |
margin: 0; | |
padding: 0; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
min-height: 100vh; | |
} | |
.container { | |
background-color: var(--surface-color); | |
padding: 2rem; | |
border-radius: var(--border-radius); | |
box-shadow: var(--box-shadow); | |
width: 90%; | |
max-width: 400px; | |
text-align: center; | |
margin-top: 2rem; | |
} | |
h1 { | |
font-size: 2rem; | |
margin-bottom: 1.5rem; | |
color: var(--primary-color); | |
} | |
input, button { | |
display: block; | |
width: 100%; | |
padding: 0.75rem; | |
margin-bottom: 1rem; | |
border: 1px solid #ccc; | |
border-radius: var(--border-radius); | |
font-size: 1rem; | |
box-sizing: border-box; | |
transition: border-color 0.3s ease; | |
} | |
input:focus { | |
outline: none; | |
border-color: var(--primary-color); | |
} | |
button { | |
background-color: var(--primary-color); | |
color: white; | |
cursor: pointer; | |
border: none; | |
font-weight: 500; | |
transition: background-color 0.3s ease; | |
} | |
button:hover { | |
background-color: var(--secondary-color); | |
} | |
button:active { | |
opacity: 0.8; | |
} | |
.logout-button { | |
background-color: var(--accent-color); | |
margin-top: 1rem; | |
transition: background-color 0.3s ease; | |
} | |
.logout-button:hover { | |
filter: brightness(0.9); | |
} | |
@media (prefers-color-scheme: dark) { | |
:root { | |
--background-color: #121212; | |
--surface-color: #1e1e1e; | |
--text-color: #ffffff; | |
} | |
} | |
</style> | |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Добро пожаловать, {{ session['username'] }}</h1> | |
<form method="post"> | |
<button type="submit" name="action" value="create">Создать комнату</button> | |
</form> | |
<form method="post"> | |
<input type="text" name="token" placeholder="Введите токен комнаты" required> | |
<button type="submit" name="action" value="join">Войти в комнату</button> | |
</form> | |
<form action="/logout" method="post"> | |
<button class="logout-button" type="submit">Выйти</button> | |
</form> | |
</div> | |
</body> | |
</html>''', session=session) | |
def logout(): | |
session.pop('username', None) | |
return redirect(url_for('index')) | |
def room(token): | |
if 'username' not in session: | |
return redirect(url_for('index')) | |
if token not in rooms: | |
return redirect(url_for('dashboard')) | |
is_admin = rooms[token]['admin'] == session['username'] | |
current_game = rooms[token].get('current_game') | |
return render_template_string(''' | |
<!DOCTYPE html> | |
<html lang="ru"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Комната {{ token }}</title> | |
<style> | |
:root { | |
--primary-color: #4CAF50; | |
--secondary-color: #388E3C; | |
--background-color: #f0f0f0; | |
--surface-color: #ffffff; | |
--text-color: #333333; | |
--font-family: 'Roboto', sans-serif; | |
--border-radius: 12px; | |
--box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15); | |
--control-bg-color: #e0e0e0; | |
--control-icon-color: #555555; | |
} | |
body { | |
font-family: var(--font-family); | |
background-color: var(--background-color); | |
color: var(--text-color); | |
margin: 0; | |
padding: 0; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
min-height: 100vh; | |
} | |
h1 { | |
font-size: 2rem; | |
text-align: center; | |
margin: 1.5rem 0; | |
color: var(--primary-color); | |
} | |
#users { | |
text-align: center; | |
margin-bottom: 1rem; | |
font-size: 1.1rem; | |
color: var(--secondary-color); | |
font-weight: 500; | |
} | |
.main-container { | |
display: flex; | |
flex-direction: row; | |
justify-content: space-between; | |
align-items: flex-start; | |
width: 95%; | |
max-width: 1400px; | |
margin: 0 auto; | |
gap: 2rem; | |
} | |
.video-section { | |
flex: 2; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
} | |
.video-grid-wrapper { | |
flex: 1; | |
overflow-y: auto; | |
max-height: 80vh; | |
padding: 1rem; | |
border-radius: var(--border-radius); | |
background-color: var(--surface-color); | |
box-shadow: var(--box-shadow); | |
} | |
.video-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | |
gap: 1rem; | |
width: 100%; | |
} | |
video { | |
width: 100%; | |
height: auto; | |
background: black; | |
border-radius: var(--border-radius); | |
box-shadow: var(--box-shadow); | |
object-fit: cover; | |
display: block; | |
transform: scaleX(1); | |
} | |
.mirrored { | |
transform: scaleX(-1); | |
} | |
.video-container { | |
position: relative; | |
overflow: hidden; | |
border-radius: var(--border-radius); | |
} | |
.user-indicator { | |
position: absolute; | |
bottom: 0.5rem; | |
left: 0.5rem; | |
background-color: rgba(0, 0, 0, 0.6); | |
color: white; | |
padding: 0.2rem 0.5rem; | |
border-radius: 5px; | |
font-size: 0.8rem; | |
z-index: 10; | |
} | |
.controls { | |
position: absolute; | |
top: 0.5rem; | |
right: 0.5rem; | |
display: flex; | |
gap: 0.5rem; | |
z-index: 20; | |
} | |
.controls button { | |
background-color: var(--control-bg-color); | |
color: var(--control-icon-color); | |
border: none; | |
border-radius: 50%; | |
padding: 0.4rem; | |
cursor: pointer; | |
font-size: 1rem; | |
width: 2rem; | |
height: 2rem; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
transition: background-color 0.2s, color 0.2s; | |
} | |
.controls button:hover { | |
background-color: var(--primary-color); | |
color: white; | |
} | |
.admin-controls { | |
position: absolute; | |
top: 0.5rem; | |
left: 0.5rem; | |
display: flex; | |
gap: 0.3rem; | |
z-index: 15; | |
} | |
.admin-controls button { | |
background-color: rgba(255, 0, 0, 0.7); | |
color: white; | |
border: none; | |
border-radius: 5px; | |
padding: 0.2rem 0.4rem; | |
font-size: 0.7rem; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
} | |
.admin-controls button:hover { | |
background-color: rgba(255, 0, 0, 0.9); | |
} | |
.leave-button { | |
display: block; | |
width: auto; | |
padding: 0.75rem 1.5rem; | |
margin: 1rem auto; | |
background-color: #f44336; | |
color: white; | |
border: none; | |
border-radius: var(--border-radius); | |
cursor: pointer; | |
font-weight: 500; | |
transition: background-color 0.3s ease; | |
white-space: nowrap; | |
} | |
.leave-button:hover { | |
background-color: #d32f2f; | |
} | |
.leave-button:active { | |
opacity: 0.8; | |
} | |
.games-section { | |
width: 100%; | |
margin-top: 20px; | |
text-align: center; | |
} | |
.game-list { | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 10px; | |
margin-top: 10px; | |
} | |
.game-card { | |
background-color: var(--surface-color); | |
border-radius: var(--border-radius); | |
box-shadow: var(--box-shadow); | |
padding: 10px; | |
width: 200px; | |
text-align: center; | |
} | |
.game-card h3 { | |
color: var(--primary-color); | |
margin-bottom: 5px; | |
} | |
.game-card p { | |
font-size: 0.9rem; | |
margin-bottom: 10px; | |
} | |
.start-game-button { | |
background-color: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: var(--border-radius); | |
padding: 5px 10px; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
} | |
.start-game-button:hover { | |
background-color: var(--secondary-color); | |
} | |
#game-display { | |
margin-top: 20px; | |
padding: 20px; | |
background-color: var(--surface-color); | |
border-radius: var(--border-radius); | |
box-shadow: var(--box-shadow); | |
text-align: center; | |
display: none; /* Скрыто по умолчанию */ | |
width: 80%; /* Ширина */ | |
max-width: 800px; /* Максимальная ширина */ | |
margin-left: auto; /* Центрирование по горизонтали */ | |
margin-right: auto; | |
} | |
#game-display h2 { | |
color: var(--primary-color); | |
margin-bottom: 10px; | |
} | |
#game-description { | |
margin-bottom: 15px; | |
font-size: 1.1rem; | |
color: var(--text-color); | |
} | |
#game-content { | |
display: flex; | |
flex-direction: column; | |
align-items: center; /* Центрирование элементов */ | |
gap: 10px; /* Расстояние между элементами */ | |
} | |
.game-input { | |
padding: 8px; | |
border: 1px solid #ccc; | |
border-radius: var(--border-radius); | |
font-size: 1rem; | |
width: 60%; /* Ширина инпутов */ | |
max-width: 300px; /* Максимальная ширина */ | |
} | |
.game-button { | |
background-color: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: var(--border-radius); | |
padding: 10px 15px; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
font-size: 1rem; | |
} | |
.game-button:hover { | |
background-color: var(--secondary-color); | |
} | |
#game-result { | |
margin-top: 15px; | |
font-size: 1rem; | |
color: var(--text-color); | |
width: 100%; /* Ширина */ | |
} | |
#game-result p{ | |
word-break: break-word; | |
} | |
#crocodile-timer { | |
font-size: 1.2rem; | |
font-weight: bold; | |
margin-top: 10px; | |
color: var(--primary-color); | |
} | |
/*Стили для карт*/ | |
.card { | |
width: 60px; | |
height: 90px; | |
border: 1px solid black; | |
border-radius: 5px; | |
display: inline-block; /* Чтобы карты шли в ряд */ | |
margin: 2px; | |
text-align: center; | |
font-size: 1rem; | |
background-color: white; | |
user-select: none; /* Предотвращает выделение текста */ | |
} | |
.card.selected { | |
border-color: blue; | |
box-shadow: 0 0 5px blue; | |
} | |
.card.back { | |
background-color: #ddd; /* Цвет рубашки */ | |
color: transparent; /* Скрываем текст на рубашке */ | |
} | |
.card-container{ | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 5px; | |
margin-bottom: 10px; | |
} | |
#durak-buttons{ | |
margin-top: 10px; | |
display: flex; | |
justify-content: center; | |
gap: 10px; | |
} | |
#durak-table{ | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 10px; | |
min-height: 100px; | |
border: 2px dashed #ccc; | |
padding: 10px; | |
margin-top: 10px; | |
margin-bottom: 10px; | |
} | |
#durak-info{ | |
font-size: 1rem; | |
font-weight: bold; | |
} | |
@media (max-width: 768px) { | |
.main-container { | |
flex-direction: column; | |
} | |
.video-grid-wrapper { | |
max-height: 40vh; | |
order: -1; | |
margin-bottom: 1rem; | |
} | |
.video-section { | |
width: 100%; | |
} | |
} | |
@media (prefers-color-scheme: dark) { | |
:root { | |
--background-color: #121212; | |
--surface-color: #1e1e1e; | |
--text-color: #ffffff; | |
--control-bg-color: #333; | |
--control-icon-color: #eee; | |
} | |
} | |
</style> | |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script> | |
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> | |
</head> | |
<body> | |
<h1>Комната: {{ token }}</h1> | |
<div id="users">Пользователи: {% for user in rooms[token]['users'] %}{{ user }}{% if not loop.last %}, {% endif %}{% endfor %}</div> | |
<div class="main-container"> | |
<div class="video-section"> | |
<button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button> | |
</div> | |
<div class="video-grid-wrapper"> | |
<div class="video-grid" id="video-grid"></div> | |
</div> | |
</div> | |
<div class="games-section"> | |
<h2>Доступные игры</h2> | |
<div class="game-list"> | |
{% for game_id, game_info in games_data.items() %} | |
{% if rooms[token]['users']|length >= game_info.min_players and rooms[token]['users']|length <= game_info.max_players %} | |
<div class="game-card"> | |
<h3>{{ game_info.name }}</h3> | |
<p>{{ game_info.description }}</p> | |
{% if is_admin %} | |
<button class="start-game-button" onclick="startGame('{{ game_id }}')">Начать {{ game_info.name }}</button> | |
{% endif %} | |
</div> | |
{% endif %} | |
{% endfor %} | |
</div> | |
</div> | |
<div id="game-display"> | |
<h2></h2> | |
<p id="game-description"></p> | |
<div id="game-content"></div> | |
</div> | |
<script> | |
const socket = io(); | |
const token = '{{ token }}'; | |
const username = '{{ session['username'] }}'; | |
const isAdmin = {{ is_admin|tojson }}; | |
let localStream; | |
const peers = {}; | |
const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; | |
socket.on('connect', () => { | |
console.log('Connected to server!'); | |
}); | |
socket.on('disconnect', () => { | |
console.log('Disconnected from server!'); | |
}); | |
function createControls(user, videoContainer) { | |
const controls = document.createElement('div'); | |
controls.classList.add('controls'); | |
const muteAudioButton = document.createElement('button'); | |
muteAudioButton.innerHTML = '<i class="material-icons">mic</i>'; | |
muteAudioButton.title = "Вкл/Выкл микрофон"; | |
muteAudioButton.dataset.muted = 'false'; | |
muteAudioButton.onclick = () => toggleAudio(user, muteAudioButton); | |
controls.appendChild(muteAudioButton); | |
const muteVideoButton = document.createElement('button'); | |
muteVideoButton.innerHTML = '<i class="material-icons">videocam</i>'; | |
muteVideoButton.title = "Вкл/Выкл камеру"; | |
muteVideoButton.dataset.videoMuted = 'false'; | |
muteVideoButton.onclick = () => toggleVideo(user, muteVideoButton); | |
controls.appendChild(muteVideoButton); | |
videoContainer.appendChild(controls); | |
if (isAdmin && user !== username) { | |
createAdminControls(user, videoContainer); | |
} | |
} | |
function createAdminControls(targetUser, videoContainer) { | |
const adminControls = document.createElement('div'); | |
adminControls.classList.add('admin-controls'); | |
const muteUserButton = document.createElement('button'); | |
muteUserButton.innerHTML = 'Заглушить'; | |
muteUserButton.title = `Заглушить ${targetUser}`; | |
muteUserButton.onclick = () => { | |
socket.emit('admin_mute', { token, targetUser, byUser: username }); | |
}; | |
adminControls.appendChild(muteUserButton); | |
videoContainer.appendChild(adminControls); | |
} | |
function toggleAudio(user, button) { | |
const muted = button.dataset.muted === 'true'; | |
button.dataset.muted = !muted; | |
button.innerHTML = `<i class="material-icons">${!muted ? 'mic' : 'mic_off'}</i>`; | |
if (user === username) { | |
localStream.getAudioTracks().forEach(track => track.enabled = !muted); | |
} | |
} | |
function toggleVideo(user, button) { | |
const videoMuted = button.dataset.videoMuted === 'true'; | |
button.dataset.videoMuted = !videoMuted; | |
button.innerHTML = `<i class="material-icons">${!videoMuted ? 'videocam' : 'videocam_off'}</i>`; | |
if (user === username) { | |
localStream.getVideoTracks().forEach(track => track.enabled = !videoMuted); | |
} | |
} | |
function addVideoStream(stream, user, isLocal = false) { | |
console.log('Adding video stream for', user); | |
const existingVideo = document.querySelector(`video[data-user="${user}"]`); | |
if (existingVideo) { | |
existingVideo.srcObject = stream; | |
return; | |
} | |
const videoContainer = document.createElement('div'); | |
videoContainer.classList.add('video-container'); | |
const video = document.createElement('video'); | |
video.srcObject = stream; | |
video.dataset.user = user; | |
video.setAttribute('playsinline', ''); | |
video.setAttribute('autoplay', ''); | |
if (isLocal) { | |
video.muted = true; | |
video.classList.add('mirrored'); | |
} | |
video.addEventListener('loadedmetadata', () => { | |
video.play().catch(e => console.error("Ошибка автовоспроизведения:", e)); | |
}); | |
const userIndicator = document.createElement('div'); | |
userIndicator.classList.add('user-indicator'); | |
userIndicator.textContent = user; | |
videoContainer.appendChild(userIndicator); | |
createControls(user, videoContainer); | |
videoContainer.appendChild(video); | |
document.getElementById('video-grid').appendChild(videoContainer); | |
} | |
navigator.mediaDevices.getUserMedia({ video: true, audio: true }) | |
.then(stream => { | |
localStream = stream; | |
addVideoStream(stream, username, true); | |
socket.emit('join', { token, username }); | |
}) | |
.catch(err => console.error("Ошибка доступа к медиаустройствам:", err)); | |
function createPeerConnection(remoteUser) { | |
if (peers[remoteUser]) { | |
return peers[remoteUser].peerConnection; | |
} | |
const peerConnection = new RTCPeerConnection(iceConfig); | |
peers[remoteUser] = { peerConnection: peerConnection, iceCandidates: [] }; | |
localStream.getTracks().forEach(track => { | |
peerConnection.addTrack(track, localStream); | |
}); | |
peerConnection.ontrack = event => { | |
console.log('Received remote stream from', remoteUser); | |
addVideoStream(event.streams[0], remoteUser); | |
}; | |
peerConnection.oniceconnectionstatechange = () => { | |
console.log(`ICE state change for ${remoteUser}: ${peerConnection.iceConnectionState}`); | |
if (peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'disconnected') { | |
if (peers[remoteUser]) { | |
peers[remoteUser].peerConnection.close(); | |
delete peers[remoteUser]; | |
const videoElement = document.querySelector(`video[data-user="${remoteUser}"]`); | |
if (videoElement) { | |
videoElement.parentElement.remove(); | |
} | |
} | |
} | |
}; | |
peerConnection.onicecandidate = event => { | |
if (event.candidate) { | |
socket.emit('signal', { | |
to: remoteUser, | |
from: username, | |
token: token, | |
signal: { type: 'candidate', candidate: event.candidate } | |
}); | |
} | |
}; | |
return peerConnection; | |
} | |
socket.on('signal', data => { | |
if (data.from === username) return; | |
let peerEntry = peers[data.from]; | |
if (!peerEntry) { | |
createPeerConnection(data.from); | |
peerEntry = peers[data.from]; | |
} | |
const peerConnection = peerEntry.peerConnection; | |
if (data.signal.type === 'offer') { | |
peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal)) | |
.then(() => peerConnection.createAnswer()) | |
.then(answer => peerConnection.setLocalDescription(answer)) | |
.then(() => { | |
socket.emit('signal', { | |
to: data.from, | |
from: username, | |
token: token, | |
signal: peerConnection.localDescription | |
}); | |
while(peerEntry.iceCandidates.length > 0){ | |
const candidate = peerEntry.iceCandidates.shift(); | |
peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) | |
.catch(err => console.error("Ошибка добавления буферизованного ICE-кандидата:", err)); | |
} | |
}) | |
.catch(err => console.error("Ошибка обработки предложения:", err)); | |
} else if (data.signal.type === 'answer') { | |
peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal)) | |
.then(() => { | |
while(peerEntry.iceCandidates.length > 0){ | |
const candidate = peerEntry.iceCandidates.shift(); | |
peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) | |
.catch(err => console.error("Ошибка добавления буферизованного ICE-кандидата:", err)); | |
} | |
}) | |
.catch(err => console.error("Ошибка обработки ответа:", err)); | |
} else if (data.signal.type === 'candidate') { | |
if (peerConnection.remoteDescription) { | |
peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate)) | |
.catch(err => console.error("Ошибка добавления ICE-кандидата:", err)); | |
} else { | |
peerEntry.iceCandidates.push(data.signal.candidate); | |
console.log("Кандидат буферизован для", data.from); | |
} | |
} | |
}); | |
socket.on('user_joined', data => { | |
console.log('User joined:', data.username); | |
document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', '); | |
if (data.username !== username) { | |
const peerConnection = createPeerConnection(data.username); | |
peerConnection.createOffer() | |
.then(offer => peerConnection.setLocalDescription(offer)) | |
.then(() => { | |
socket.emit('signal', { | |
to: data.username, | |
from: username, | |
token: token, | |
signal: peerConnection.localDescription | |
}); | |
}) | |
.catch(err => console.error("Ошибка создания предложения:", err)); | |
} | |
}); | |
socket.on('init_users', data => { | |
console.log("Initial users:", data.users); | |
data.users.forEach(user => { | |
if (user !== username) { | |
createPeerConnection(user); | |
} | |
}); | |
}); | |
socket.on('user_left', data => { | |
console.log('User left:', data.username); | |
document.getElementById('users').innerText = 'Пользователи: ' + data.users.join(', '); | |
if (peers[data.username]) { | |
peers[data.username].peerConnection.close(); | |
delete peers[data.username]; | |
const videoElement = document.querySelector(`video[data-user="${data.username}"]`); | |
if(videoElement){ | |
videoElement.parentElement.remove(); | |
} | |
} | |
}); | |
socket.on('admin_muted', data => { | |
if (data.targetUser === username) { | |
localStream.getAudioTracks().forEach(track => track.enabled = false); | |
const muteButton = document.querySelector(`.controls button[data-muted][data-user="${username}"]`); | |
if (muteButton) { | |
muteButton.dataset.muted = 'true'; | |
muteButton.innerHTML = '<i class="material-icons">mic_off</i>'; | |
} | |
} | |
}); | |
// Функции для игр | |
function startGame(gameId) { | |
console.log('Starting game:', gameId); | |
socket.emit('start_game', { token, game_id: gameId }); | |
} | |
socket.on('game_started', (data) => { | |
const gameId = data.game_id; | |
const gameInfo = {{ games_data|tojson }}[gameId]; | |
console.log('Game started:', gameInfo.name); | |
// Показываем div с игрой | |
const gameDisplay = document.getElementById('game-display'); | |
gameDisplay.style.display = 'block'; | |
document.getElementById('game-description').innerText = gameInfo.description; | |
document.querySelector('#game-display h2').innerText = gameInfo.name; | |
// Очищаем предыдущий контент игры | |
const gameContent = document.getElementById('game-content'); | |
gameContent.innerHTML = ''; | |
// Инициализация игры в зависимости от gameId | |
if (gameId === 'crocodile'){ | |
initCrocodile(gameId, gameInfo, gameContent); | |
} else if (gameId === 'alias'){ | |
initAlias(gameId, gameInfo, gameContent); | |
} else if(gameId === 'mafia'){ | |
initMafia(gameId, gameInfo, gameContent); | |
}else if(gameId === 'durak'){ | |
initDurak(gameId, gameInfo, gameContent); | |
} | |
}); | |
// Игра "Крокодил" | |
function initCrocodile(gameId, gameInfo, gameContent){ | |
const wordInput = document.createElement('input'); | |
wordInput.type = 'text'; | |
wordInput.id = 'crocodile-word-input'; | |
wordInput.placeholder = 'Введите слово'; | |
wordInput.classList.add('game-input'); | |
wordInput.disabled = true; // По умолчанию отключен | |
gameContent.appendChild(wordInput); | |
const startTurnButton = document.createElement('button'); | |
startTurnButton.textContent = 'Начать ход'; | |
startTurnButton.classList.add('game-button'); | |
startTurnButton.id = 'start-turn-button'; | |
gameContent.appendChild(startTurnButton); | |
const guessInput = document.createElement('input'); | |
guessInput.type = 'text'; | |
guessInput.classList.add('game-input'); | |
guessInput.id = 'crocodile-guess-input'; | |
guessInput.placeholder = 'Ваша догадка'; | |
guessInput.disabled = true; | |
gameContent.appendChild(guessInput); | |
const guessButton = document.createElement('button'); | |
guessButton.textContent = 'Угадать'; | |
guessButton.classList.add('game-button'); | |
guessButton.id = 'crocodile-guess-button'; | |
guessButton.disabled = true; | |
gameContent.appendChild(guessButton); | |
const resultDiv = document.createElement('div'); | |
resultDiv.id = 'crocodile-result'; | |
gameContent.appendChild(resultDiv); | |
const timerDisplay = document.createElement('div'); | |
timerDisplay.id = 'crocodile-timer'; | |
gameContent.appendChild(timerDisplay); | |
// Админ начинает игру и выбирает первого ведущего | |
if(isAdmin){ | |
wordInput.disabled = false; | |
startTurnButton.onclick = () => { | |
const word = wordInput.value.trim(); | |
if(word){ | |
// Выбираем случайного пользователя (кроме админа) | |
const users = {{ rooms[token]['users']|tojson }}; | |
const nonAdminUsers = users.filter(u => u !== username); | |
const presenter = nonAdminUsers.length > 0 ? nonAdminUsers[Math.floor(Math.random() * nonAdminUsers.length)] : null; | |
if(presenter) { | |
socket.emit('set_game_state', {token, game_id: gameId, state: { word: word, presenter: presenter, guesses: [], timer: 60, isRunning: true }}); | |
} else { | |
alert("Нет игроков для выбора ведущего."); | |
} | |
} else { | |
alert('Введите слово!'); | |
} | |
} | |
} | |
guessButton.onclick = () => { | |
const guess = guessInput.value.trim(); | |
if (guess) { | |
socket.emit('game_action', { token, game_id: gameId, action: 'guess', value: guess, user: username }); | |
guessInput.value = ''; // Очистка поля ввода | |
} | |
} | |
} | |
function updateCrocodileState(gameId, gameState){ | |
const resultDiv = document.getElementById('crocodile-result'); | |
const wordInput = document.getElementById('crocodile-word-input'); | |
const guessInput = document.getElementById('crocodile-guess-input'); | |
const guessButton = document.getElementById('crocodile-guess-button'); | |
const timerDisplay = document.getElementById('crocodile-timer'); | |
if(!resultDiv || !wordInput || !guessInput || !guessButton || !timerDisplay) return; | |
resultDiv.innerHTML = ''; // Очистка | |
if(gameState.isRunning){ | |
timerDisplay.textContent = `Время: ${gameState.timer}`; | |
// Показываем слово ведущему | |
if (username === gameState.presenter) { | |
wordInput.value = gameState.word; | |
wordInput.disabled = true; // Ведущий не может менять слово | |
} else{ | |
wordInput.value = ''; // Скрываем от остальных | |
wordInput.disabled = true; | |
} | |
// Разрешаем угадывать всем, кроме ведущего | |
guessInput.disabled = username === gameState.presenter; | |
guessButton.disabled = username === gameState.presenter; | |
// Выводим догадки | |
if (gameState.guesses && gameState.guesses.length > 0) { | |
gameState.guesses.forEach(guess => { | |
const p = document.createElement('p'); | |
p.textContent = `${guess.user}: ${guess.value} - ${guess.result}`; | |
resultDiv.appendChild(p); | |
}); | |
} | |
if (gameState.guesses.length>0 && gameState.guesses[gameState.guesses.length - 1].result === "Угадано!"){ | |
const winMessage = document.createElement('p'); | |
winMessage.textContent = `Победил ${gameState.guesses[gameState.guesses.length - 1].user}!`; | |
resultDiv.appendChild(winMessage); | |
guessInput.disabled = true; | |
guessButton.disabled = true; | |
timerDisplay.textContent = ''; | |
gameState.isRunning = false; // Остановка игры | |
//Очистка | |
wordInput.value = ''; | |
} | |
} | |
} | |
//игра "Alias" | |
function initAlias(gameId, gameInfo, gameContent){ | |
const wordInput = document.createElement('input'); | |
wordInput.type = 'text'; | |
wordInput.id = 'alias-word-input'; | |
wordInput.placeholder = 'Введите слово'; | |
wordInput.classList.add('game-input'); | |
wordInput.disabled = true; // По умолчанию отключен | |
gameContent.appendChild(wordInput); | |
const startTurnButton = document.createElement('button'); | |
startTurnButton.textContent = 'Начать ход'; | |
startTurnButton.classList.add('game-button'); | |
startTurnButton.id = 'start-turn-button'; | |
gameContent.appendChild(startTurnButton); | |
const guessInput = document.createElement('input'); | |
guessInput.type = 'text'; | |
guessInput.id = 'alias-guess-input'; | |
guessInput.classList.add('game-input'); | |
guessInput.placeholder = 'Ваша догадка'; | |
guessInput.disabled = true; | |
gameContent.appendChild(guessInput); | |
const guessButton = document.createElement('button'); | |
guessButton.textContent = 'Угадать'; | |
guessButton.classList.add('game-button'); | |
guessButton.id = 'alias-guess-button'; | |
guessButton.disabled = true; | |
gameContent.appendChild(guessButton); | |
const resultDiv = document.createElement('div'); | |
resultDiv.id = 'alias-result'; | |
gameContent.appendChild(resultDiv); | |
const timerDisplay = document.createElement('div'); | |
timerDisplay.id = 'alias-timer'; | |
gameContent.appendChild(timerDisplay); | |
if(isAdmin){ | |
wordInput.disabled = false; | |
startTurnButton.onclick = () => { | |
const word = wordInput.value.trim(); | |
if(word){ | |
const users = {{ rooms[token]['users']|tojson }}; | |
const nonAdminUsers = users.filter(u => u !== username); | |
const presenter = nonAdminUsers.length > 0 ? nonAdminUsers[Math.floor(Math.random() * nonAdminUsers.length)] : null; | |
if(presenter){ | |
socket.emit('set_game_state', {token, game_id: gameId, state: {word: word, presenter: presenter, guesses: [], timer: 60, isRunning: true}}); | |
}else{ | |
alert("Нет игроков для выбора ведущего"); | |
} | |
}else{ | |
alert("Введите слово"); | |
} | |
} | |
} | |
guessButton.onclick = () => { | |
const guess = guessInput.value.trim(); | |
if(guess){ | |
socket.emit('game_action', {token, game_id: gameId, action: 'guess', value: guess, user: username}); | |
guessInput.value = ''; | |
} | |
} | |
} | |
function updateAliasState(gameId, gameState){ | |
const resultDiv = document.getElementById('alias-result'); | |
const wordInput = document.getElementById('alias-word-input'); | |
const guessInput = document.getElementById('alias-guess-input'); | |
const guessButton = document.getElementById('alias-guess-button'); | |
const timerDisplay = document.getElementById('alias-timer'); | |
if(!resultDiv || !wordInput || !guessInput || !guessButton || !timerDisplay) return; | |
resultDiv.innerHTML = ''; | |
if(gameState.isRunning){ | |
timerDisplay.textContent = `Время: ${gameState.timer}`; | |
if(username === gameState.presenter){ | |
wordInput.value = gameState.word; | |
wordInput.disabled = true; | |
}else{ | |
wordInput.value = ''; | |
wordInput.disabled = true; | |
} | |
guessInput.disabled = username === gameState.presenter; | |
guessButton.disabled = username === gameState.presenter; | |
if(gameState.guesses && gameState.guesses.length > 0){ | |
gameState.guesses.forEach(guess => { | |
const p = document.createElement('p'); | |
p.textContent = `${guess.user}: ${guess.value} - ${guess.result}`; | |
resultDiv.appendChild(p); | |
}); | |
} | |
if (gameState.guesses.length>0 && gameState.guesses[gameState.guesses.length - 1].result === "Угадано!"){ | |
const winMessage = document.createElement('p'); | |
winMessage.textContent = `Победил ${gameState.guesses[gameState.guesses.length - 1].user}!`; | |
resultDiv.appendChild(winMessage); | |
guessInput.disabled = true; | |
guessButton.disabled = true; | |
timerDisplay.textContent = ''; | |
gameState.isRunning = false; // Остановка игры | |
//Очистка | |
wordInput.value = ''; | |
} | |
} | |
} | |
//Игра "Мафия" | |
function initMafia(gameId, gameInfo, gameContent) { | |
const startMafiaButton = document.createElement('button'); | |
startMafiaButton.textContent = 'Начать игру'; | |
startMafiaButton.classList.add('game-button'); | |
startMafiaButton.id = 'start-mafia-button'; | |
gameContent.appendChild(startMafiaButton); | |
const resultDiv = document.createElement('div'); | |
resultDiv.id = 'mafia-result'; | |
gameContent.appendChild(resultDiv); | |
const voteInput = document.createElement('input'); | |
voteInput.type = 'text'; | |
voteInput.id = 'mafia-vote-input'; | |
voteInput.placeholder = 'За кого голосуете?'; | |
voteInput.classList.add('game-input'); | |
voteInput.disabled = true; | |
gameContent.appendChild(voteInput); | |
const voteButton = document.createElement('button'); | |
voteButton.textContent = 'Голосовать'; | |
voteButton.classList.add('game-button'); | |
voteButton.id = 'mafia-vote-button'; | |
voteButton.disabled = true; | |
gameContent.appendChild(voteButton); | |
if (isAdmin) { | |
startMafiaButton.onclick = () => { | |
const users = {{ rooms[token]['users']|tojson }}; | |
// Раздача ролей | |
const roles = assignMafiaRoles(users); | |
socket.emit('set_game_state', { token, game_id: gameId, state: { roles: roles, phase: 'night', votes: {}, isRunning: true, killed: null, checked: null } }); | |
}; | |
} | |
voteButton.onclick = () => { | |
const vote = voteInput.value.trim(); | |
if (vote) { | |
socket.emit('game_action', { token, game_id: gameId, action: 'vote', value: vote, user: username }); | |
voteInput.value = ''; //Очищаем поле | |
} | |
}; | |
} | |
function assignMafiaRoles(users) { | |
const numPlayers = users.length; | |
let numMafia = 1; | |
if (numPlayers >= 5) { | |
numMafia = 2; | |
} | |
// Создаем массив ролей: сначала мафия, потом мирные | |
const roles = Array(numMafia).fill('mafia').concat(Array(numPlayers - numMafia).fill('civilian')); | |
// Перемешиваем роли | |
for (let i = roles.length - 1; i > 0; i--) { | |
const j = Math.floor(Math.random() * (i + 1)); | |
[roles[i], roles[j]] = [roles[j], roles[i]]; // ES6 деструктуризация для обмена | |
} | |
// Назначаем роли пользователям | |
const assignedRoles = {}; | |
for (let i = 0; i < numPlayers; i++) { | |
assignedRoles[users[i]] = roles[i]; | |
} | |
return assignedRoles; | |
} | |
function updateMafiaState(gameId, gameState) { | |
const resultDiv = document.getElementById('mafia-result'); | |
const voteInput = document.getElementById('mafia-vote-input'); | |
const voteButton = document.getElementById('mafia-vote-button'); | |
if (!resultDiv || !voteInput || !voteButton) return; | |
resultDiv.innerHTML = ''; // Очищаем | |
// Ночь | |
if (gameState.isRunning && gameState.phase === 'night') { | |
voteInput.disabled = true; // Отключаем голосование ночью | |
voteButton.disabled = true; | |
if (gameState.roles[username] === 'mafia') { | |
resultDiv.innerHTML = '<p>Вы мафия. Обсудите с другими мафиози, кого убить.</p>'; | |
} else { | |
resultDiv.innerHTML = '<p>Ночь. Все спят.</p>'; | |
} | |
} | |
// День | |
else if(gameState.isRunning && gameState.phase === 'day'){ | |
voteInput.disabled = false; | |
voteButton.disabled = false; | |
if (gameState.killed) { | |
resultDiv.innerHTML += `<p>Ночью был убит ${gameState.killed}.</p>`; | |
} | |
resultDiv.innerHTML += '<p>День. Обсуждение и голосование.</p>'; | |
if (gameState.votes && Object.keys(gameState.votes).length > 0) { | |
resultDiv.innerHTML += '<p>Голоса:</p>'; | |
for (const voter in gameState.votes) { | |
resultDiv.innerHTML += `<p>${voter}: ${gameState.votes[voter]}</p>`; | |
} | |
} | |
// Подсчет голосов и определение, кого казнят | |
if(Object.keys(gameState.votes).length === {{ rooms[token]['users']|length }}){ | |
const voteCounts = {}; | |
let maxVotes = 0; | |
let votedOut = null; | |
let tied = false; // Флаг ничьей | |
for(const voter in gameState.votes){ | |
const votedFor = gameState.votes[voter]; | |
voteCounts[votedFor] = (voteCounts[votedFor] || 0) + 1; | |
if(voteCounts[votedFor] > maxVotes){ | |
maxVotes = voteCounts[votedFor]; | |
votedOut = votedFor; | |
tied = false; // Сбрасываем ничью, если новый лидер | |
}else if(voteCounts[votedFor] === maxVotes){ | |
tied = true; // Ничья | |
} | |
} | |
if (tied) { | |
resultDiv.innerHTML += '<p>Голосование завершилось ничьей. Никто не казнен.</p>'; | |
}else{ | |
resultDiv.innerHTML += `<p>По итогам голосования казнен ${votedOut}.</p>`; | |
// Проверяем, была ли это мафия | |
if (gameState.roles[votedOut] === 'mafia') { | |
resultDiv.innerHTML += '<p>Мирные жители победили!</p>'; | |
gameState.isRunning = false; | |
} | |
} | |
// Проверяем, остались ли мафиози | |
const remainingMafia = Object.values(gameState.roles).filter(role => role === 'mafia').length; | |
if (remainingMafia === 0) { | |
resultDiv.innerHTML += '<p>Мирные жители победили!</p>'; | |
gameState.isRunning = false; | |
} | |
} | |
} | |
} | |
//Карточная игра "Дурак" | |
function initDurak(gameId, gameInfo, gameContent){ | |
if (isAdmin) { | |
const startButton = document.createElement('button'); | |
startButton.textContent = 'Раздать карты'; | |
startButton.classList.add('game-button'); | |
startButton.onclick = () => { | |
const { deck, hands } = dealDurakCards({{ rooms[token]['users']|tojson }}); | |
socket.emit('set_game_state', { token, game_id: gameId, state: { deck, hands, table: [], turn: 0, attacker: null, defender: null, trumpSuit: deck.length > 0 ? deck[deck.length-1].suit : null, isGameEnd: false, winner: null} }); | |
}; | |
gameContent.appendChild(startButton); | |
} | |
const playerHandContainer = document.createElement('div'); | |
playerHandContainer.id = 'player-hand'; | |
playerHandContainer.classList.add('card-container'); | |
gameContent.appendChild(playerHandContainer); | |
const tableContainer = document.createElement('div'); | |
tableContainer.id = 'durak-table'; | |
gameContent.appendChild(tableContainer); | |
const buttonsDiv = document.createElement('div'); | |
buttonsDiv.id = 'durak-buttons'; | |
gameContent.appendChild(buttonsDiv); | |
const takeButton = document.createElement('button'); | |
takeButton.textContent = 'Взять'; | |
takeButton.id = 'take-button'; | |
takeButton.classList.add('game-button'); | |
buttonsDiv.appendChild(takeButton); | |
const doneButton = document.createElement('button'); | |
doneButton.textContent = 'Готово/Пас'; | |
doneButton.id = 'done-button'; | |
doneButton.classList.add('game-button'); | |
buttonsDiv.appendChild(doneButton); | |
const infoDiv = document.createElement('div'); | |
infoDiv.id = 'durak-info'; | |
gameContent.appendChild(infoDiv); | |
takeButton.onclick = () => { | |
socket.emit('game_action', { token, game_id: gameId, action: 'take', user: username }); | |
}; | |
doneButton.onclick = () => { | |
socket.emit('game_action', { token, game_id: gameId, action: 'done', user: username }); | |
} | |
} | |
function dealDurakCards(players) { | |
const suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']; | |
const ranks = ['6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; | |
let deck = []; | |
// Создаем колоду | |
for (const suit of suits) { | |
for (const rank of ranks) { | |
deck.push({ suit, rank }); | |
} | |
} | |
// Перемешиваем колоду | |
for (let i = deck.length - 1; i > 0; i--) { | |
const j = Math.floor(Math.random() * (i + 1)); | |
[deck[i], deck[j]] = [deck[j], deck[i]]; | |
} | |
// Раздаем карты | |
const hands = {}; | |
players.forEach(player => hands[player] = []); | |
let cardIndex = 0; | |
while (cardIndex < 6 * players.length && cardIndex < deck.length) { | |
players.forEach(player => { | |
if (cardIndex < deck.length) { | |
hands[player].push(deck[cardIndex]); | |
cardIndex++; | |
} | |
}); | |
} | |
deck = deck.slice(cardIndex); // Оставшиеся карты - колода. | |
return { deck, hands }; | |
} | |
function renderCard(card, isHidden) { | |
const cardElement = document.createElement('div'); | |
cardElement.classList.add('card'); | |
if (isHidden) { | |
cardElement.classList.add('back'); // Рубашка | |
} else { | |
cardElement.textContent = `${card.rank}${getSuitSymbol(card.suit)}`; | |
cardElement.dataset.suit = card.suit; | |
cardElement.dataset.rank = card.rank; | |
} | |
return cardElement; | |
} | |
function getSuitSymbol(suit) { | |
switch (suit) { | |
case 'Hearts': return '♥'; | |
case 'Diamonds': return '♦'; | |
case 'Clubs': return '♣'; | |
case 'Spades': return '♠'; | |
default: return ''; | |
} | |
} | |
function canBeat(attackingCard, defendingCard, trumpSuit){ | |
if(attackingCard.suit === defendingCard.suit){ | |
return compareRanks(defendingCard.rank, attackingCard.rank) > 0; | |
}else{ | |
return defendingCard.suit === trumpSuit; | |
} | |
} | |
function compareRanks(rank1, rank2){ | |
const ranks = ['6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; | |
return ranks.indexOf(rank1) - ranks.indexOf(rank2); | |
} | |
function updateDurakState(gameId, gameState){ | |
const playerHandContainer = document.getElementById('player-hand'); | |
const tableContainer = document.getElementById('durak-table'); | |
const infoDiv = document.getElementById('durak-info'); | |
if (!playerHandContainer || !tableContainer || !infoDiv) return; | |
playerHandContainer.innerHTML = ''; // Очистка | |
tableContainer.innerHTML = ''; | |
infoDiv.innerHTML = ''; | |
const trumpCardEl = document.createElement('div'); | |
if(gameState.trumpSuit){ | |
infoDiv.innerHTML = `Козырь: ${getSuitSymbol(gameState.trumpSuit)}`; | |
trumpCardEl.classList.add('card'); | |
if(gameState.deck.length > 0){ | |
trumpCardEl.textContent = `${gameState.deck[gameState.deck.length -1].rank} ${getSuitSymbol(gameState.deck[gameState.deck.length -1].suit)}`; | |
infoDiv.appendChild(trumpCardEl) | |
} | |
} | |
const myHand = gameState.hands[username] || []; | |
myHand.forEach(card => { | |
const cardElement = renderCard(card, false); | |
cardElement.addEventListener('click', () => { | |
const selectedCards = playerHandContainer.querySelectorAll('.selected'); | |
if (cardElement.classList.contains('selected')) { | |
cardElement.classList.remove('selected'); | |
} else { | |
// Логика выбора карты для хода/защиты | |
if(gameState.attacker === username){ //Если игрок атакующий | |
if(gameState.table.length < 6){ | |
//Выбираем только одну карту | |
selectedCards.forEach(c => c.classList.remove('selected')); | |
cardElement.classList.add('selected'); | |
} | |
} | |
else if(gameState.defender === username){ | |
// Можно выбрать только одну карту для защиты | |
selectedCards.forEach(c => c.classList.remove('selected')); | |
cardElement.classList.add('selected'); | |
} | |
//Выделяем карту | |
//cardElement.classList.add('selected'); | |
} | |
//Определяем, можем ли мы сделать ход выбранной картой | |
const selected = playerHandContainer.querySelector('.selected'); //Одна карта | |
if(selected){ | |
const selectedCard = {suit: selected.dataset.suit, rank: selected.dataset.rank}; | |
socket.emit('game_action', {token, game_id: gameId, action: 'move', card: selectedCard, user: username}); | |
} | |
}); | |
playerHandContainer.appendChild(cardElement); | |
}); | |
const takeButton = document.getElementById('take-button'); | |
const doneButton = document.getElementById('done-button'); | |
// Обновляем кнопки | |
takeButton.style.display = 'none'; | |
doneButton.style.display = 'none'; | |
// Если текущий игрок - защищающийся, и он еще не взял карты, показываем кнопку | |
if (gameState.defender === username && gameState.turn === 1) { //turn === 1 - Защищающийся | |
takeButton.style.display = 'inline-block'; | |
} | |
// Если текущий игрок - атакующий, и он еще не закончил ход, показываем "Готово" | |
if(gameState.attacker === username && gameState.turn === 0){ | |
doneButton.style.display = 'inline-block'; | |
} | |
// Отображаем карты на столе | |
gameState.table.forEach(pair => { | |
const pairDiv = document.createElement('div'); | |
pairDiv.classList.add('card-pair'); | |
const attackingCardEl = renderCard(pair.attackingCard, false); | |
pairDiv.appendChild(attackingCardEl); | |
if (pair.defendingCard) { | |
const defendingCardEl = renderCard(pair.defendingCard, false); | |
pairDiv.appendChild(defendingCardEl); | |
} | |
tableContainer.appendChild(pairDiv); | |
}); | |
if(gameState.isGameEnd){ | |
infoDiv.innerHTML += `<p>Игра окончена, победитель - ${gameState.winner} </p>` | |
} | |
} | |
socket.on('update_game_state', (data) => { | |
const gameId = data.game_id; | |
const gameState = data.state; | |
if (gameId === 'crocodile'){ | |
updateCrocodileState(gameId, gameState); | |
}else if(gameId === 'alias'){ | |
updateAliasState(gameId, gameState); | |
}else if(gameId === 'mafia'){ | |
updateMafiaState(gameId, gameState) | |
}else if(gameId === 'durak'){ | |
updateDurakState(gameId, gameState); | |
} | |
}); | |
socket.on('timer_tick', (data) => { | |
const gameId = data.game_id; | |
if(gameId === 'crocodile' && document.getElementById('crocodile-timer')){ | |
document.getElementById('crocodile-timer').textContent = `Время: ${data.time}`; | |
}else if(gameId === 'alias' && document.getElementById('alias-timer')){ | |
document.getElementById('alias-timer').textContent = `Время: ${data.time}`; | |
} | |
}); | |
socket.on('game_action_result', (data) => { | |
const gameId = data.game_id; | |
const action = data.action; | |
//console.log("LOG:", gameId, action) | |
if(gameId === 'durak' && action === 'move'){ // Для дурака убрал, так как все через update | |
} | |
}); | |
function leaveRoom() { | |
socket.emit('leave', { token, username }); | |
if (localStream) { | |
localStream.getTracks().forEach(track => track.stop()); | |
} | |
for (let user in peers) { | |
peers[user].peerConnection.close(); | |
} | |
window.location.href = '/dashboard'; | |
} | |
</script> | |
</body> | |
</html> | |
''', token=token, session=session, is_admin=is_admin, rooms=rooms, games_data=games_data) | |
def handle_join(data): | |
token = data['token'] | |
username = data['username'] | |
print( f"User {username} joining room {token}") | |
if token in rooms and len(rooms[token]['users']) < rooms[token]['max_users']: | |
join_room(token) | |
if username not in rooms[token]['users']: | |
rooms[token]['users'].append(username) | |
save_json(ROOMS_DB, rooms) | |
emit('user_joined', {'username': username, 'users': rooms[token]['users']}, room=token) | |
emit('init_users', {'users': rooms[token]['users']}, to=request.sid) | |
if rooms[token]['current_game']: # Если игра уже идет, отправляем новому пользователю текущее состояние | |
emit('game_started', {'game_id': rooms[token]['current_game']}, to=request.sid) | |
emit('update_game_state', {'game_id': rooms[token]['current_game'], 'state': games_data[rooms[token]['current_game']]['state'][token]}, to=request.sid); | |
else: | |
emit('error_message', {'message': 'Комната заполнена или не существует'}, to=request.sid) | |
def handle_leave(data): | |
token = data['token'] | |
username = data['username'] | |
print(f"User {username} leaving room {token}") | |
if token in rooms and username in rooms[token]['users']: | |
leave_room(token) | |
rooms[token]['users'].remove(username) | |
if rooms[token]['admin'] == username: | |
# Если админ выходит и игра активна, завершаем игру | |
if rooms[token].get('current_game'): | |
game_id = rooms[token]['current_game'] | |
if games_data[game_id]['state'].get(token): | |
del games_data[game_id]['state'][token] | |
save_json(GAMES_DB, games_data) | |
del rooms[token] | |
save_json(ROOMS_DB, rooms) | |
emit('user_left', {'username': username, 'users': []}, room=token) # Важно уведомить всех, что комната больше не существует | |
return; | |
if rooms[token].get('current_game'): # Если игра в процессе | |
game_id = rooms[token]['current_game'] | |
if games_data[game_id]['state'].get(token): | |
if game_id == 'crocodile': | |
if games_data[game_id]['state'][token].get('presenter') == username: | |
# Сброс игры, если уходит ведущий | |
del games_data[game_id]['state'][token] | |
emit('update_game_state', {'game_id': game_id, 'state': {}}, room=token); # Уведомляем, что состояние сброшено | |
save_json(GAMES_DB, games_data) | |
return | |
#Удаление догадок | |
if games_data[game_id]['state'][token].get('guesses'): | |
games_data[game_id]['state'][token]['guesses'] = [guess for guess in games_data[game_id]['state'][token]['guesses'] if guess['user'] != username] | |
elif game_id == 'alias': | |
if games_data[game_id]['state'][token].get('presenter') == username: | |
del games_data[game_id]['state'][token] | |
emit('update_game_state', {'game_id': game_id, 'state': {}}, room=token) | |
save_json(GAMES_DB, games_data) | |
return | |
if games_data[game_id]['state'][token].get('guesses'): | |
games_data[game_id]['state'][token]['guesses'] = [guess for guess in games_data[game_id]['state'][token]['guesses'] if guess['user'] != username] | |
elif game_id == 'mafia': | |
if games_data[game_id]['state'][token].get('roles') and username in games_data[game_id]['state'][token]['roles']: | |
del games_data[game_id]['state'][token]['roles'][username] | |
if games_data[game_id]['state'][token].get('votes'): | |
#Удаляем голос, если он был | |
if username in games_data[game_id]['state'][token]['votes']: | |
del games_data[game_id]['state'][token]['votes'][username] | |
elif game_id == 'durak': | |
if games_data[game_id]['state'][token].get('hands') and username in games_data[game_id]['state'][token]['hands']: | |
del games_data[game_id]['state'][token]['hands'][username] | |
#Если ушел атакующий или защищающийся | |
if games_data[game_id]['state'][token].get('attacker') == username: | |
games_data[game_id]['state'][token]['attacker'] = None; | |
if games_data[game_id]['state'][token].get('defender') == username: | |
games_data[game_id]['state'][token]['defender'] = None; | |
save_json(GAMES_DB, games_data); # Сохраняем изменения в любом случае | |
emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, room=token); | |
save_json(ROOMS_DB, rooms) | |
emit('user_left', {'username': username, 'users': rooms[token]['users']}, room=token) | |
def handle_signal(data): | |
emit('signal', data, room=data['token'], include_self=False) | |
def handle_admin_mute(data): | |
token = data['token'] | |
target_user = data['targetUser'] | |
by_user = data['byUser'] | |
if token in rooms and rooms[token].get('admin') == by_user: | |
emit('admin_muted', {'targetUser': target_user}, room=token) | |
def handle_start_game(data): | |
token = data['token'] | |
game_id = data['game_id'] | |
if token in rooms and rooms[token]['admin'] == session.get('username'): | |
rooms[token]['current_game'] = game_id | |
games_data[game_id]['state'][token] = {} # Инициализируем состояние игры для этой комнаты | |
save_json(ROOMS_DB, rooms) | |
save_json(GAMES_DB, games_data) # Сохраняем изменения в games_data | |
emit('game_started', {'game_id': game_id}, room=token) | |
def handle_set_game_state(data): | |
token = data['token'] | |
game_id = data['game_id'] | |
state = data['state'] | |
#print("SET STATE", data) | |
if token in rooms: | |
if rooms[token]['admin'] == session.get('username'): | |
# Проверяем, что игра выбрана и текущий игрок - админ | |
if rooms[token]['current_game'] == game_id : | |
games_data[game_id]['state'][token] = state # Сохраняем в games_data | |
save_json(GAMES_DB, games_data) | |
emit('update_game_state', {'game_id': game_id, 'state': state}, room=token) | |
if (game_id == 'crocodile' or game_id == 'alias') and state.get('isRunning'): # Запуск таймера | |
start_timer(token, game_id) | |
def handle_game_action(data): | |
token = data['token'] | |
game_id = data['game_id'] | |
action = data['action'] | |
value = data.get('value') # Могут быть и другие данные (не только value) | |
user = data['user'] | |
card = data.get('card') # Для карт | |
if token in rooms and game_id == rooms[token]['current_game']: | |
current_state = games_data[game_id]['state'].get(token, {}) | |
if game_id == 'crocodile' or game_id == 'alias': | |
if action == 'guess' and current_state.get('isRunning'): | |
result = "Не угадано" | |
if value.lower() == current_state.get('word', '').lower(): | |
result = "Угадано!" | |
if 'guesses' not in current_state: | |
current_state['guesses'] = [] | |
current_state['guesses'].append({'user':user, 'value': value, 'result':result}) | |
games_data[game_id]['state'][token] = current_state | |
save_json(GAMES_DB, games_data) | |
emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token) | |
elif game_id == 'mafia': | |
if action == 'vote' and current_state.get('phase') == 'day' and current_state.get('isRunning'): | |
if 'votes' not in current_state: | |
current_state['votes'] = {} | |
current_state['votes'][user] = value # Сохраняем голос | |
#Если все проголосовали | |
if len(current_state['votes']) == len(rooms[token]['users']): | |
#Меняем фазу на ночь | |
current_state['phase'] = 'night' | |
games_data[game_id]['state'][token] = current_state | |
save_json(GAMES_DB, games_data); | |
emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token) | |
elif game_id == 'durak': | |
if action == 'move': | |
if current_state.get('attacker') == user: | |
# Проверяем, можно ли походить этой картой | |
if len(current_state['table']) == 0 or any(c['rank'] == card['rank'] for pair in current_state['table'] for c in [pair.get('attackingCard'), pair.get('defendingCard')] if c): | |
current_state['table'].append({'attackingCard': card, 'defendingCard': None}) | |
current_state['hands'][user].remove(card) | |
#Передаем ход защищающемуся | |
current_state['turn'] = 1; | |
current_state['defender'] = get_next_player(token, current_state['attacker']); | |
games_data[game_id]['state'][token] = current_state | |
save_json(GAMES_DB, games_data) | |
emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token) | |
elif current_state.get('defender') == user: | |
if len(current_state['table']) > 0 : | |
last_pair = current_state['table'][-1] | |
if last_pair.get('defendingCard') is None: | |
attacking_card = last_pair['attackingCard'] | |
#Можем ли побить | |
if canBeat(attacking_card, card, current_state['trumpSuit']): | |
last_pair['defendingCard'] = card | |
current_state['hands'][user].remove(card) | |
current_state['turn'] = 0 #Ход переходит атакующему | |
#Меняем атакующего и защищающегося, если нужно | |
current_state['attacker'] = get_next_player(token, current_state['defender']); | |
current_state['defender'] = get_next_player(token, current_state['attacker']); | |
games_data[game_id]['state'][token] = current_state | |
save_json(GAMES_DB, games_data) | |
emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token) | |
elif action == 'take': | |
if current_state.get('defender') == user: | |
# Защищающийся берет карты со стола | |
taken_cards = [] | |
for pair in current_state['table']: | |
taken_cards.append(pair['attackingCard']) | |
if pair.get('defendingCard'): | |
taken_cards.append(pair['defendingCard']) | |
current_state['hands'][user].extend(taken_cards) | |
current_state['table'] = [] # Очищаем стол | |
#Добираем карты | |
current_state = refill_hands(current_state, token); | |
# Ход переходит к следующему игроку после защищавшегося. | |
current_state['attacker'] = get_next_player(token, current_state['defender']); | |
current_state['defender'] = get_next_player(token, current_state['attacker']); | |
current_state['turn'] = 0; # Ходит атакующий | |
games_data[game_id]['state'][token] = current_state | |
save_json(GAMES_DB, games_data) | |
emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token) | |
elif action == 'done': | |
if current_state.get('attacker') == user: | |
current_state['table'] = [] | |
current_state = refill_hands(current_state, token); #Раздача | |
#Определение следующего атакующего и защищающегося | |
current_state['attacker'] = get_next_player(token, current_state['attacker']); | |
current_state['defender'] = get_next_player(token, current_state['attacker']); | |
current_state['turn'] = 0; # Ходит атакующий | |
games_data[game_id]['state'][token] = current_state; | |
save_json(GAMES_DB, games_data); | |
emit('update_game_state', {'game_id': game_id, 'state':current_state}, room=token); | |
def get_next_player(token, current_player): | |
"""Определяет следующего игрока в комнате.""" | |
if token not in rooms: | |
return None | |
users = rooms[token]['users'] | |
if not users: | |
return None | |
current_index = users.index(current_player) | |
next_index = (current_index + 1) % len(users) # Циклический переход | |
return users[next_index] | |
def refill_hands(game_state, token): | |
"""Раздает карты игрокам до 6, если в колоде еще есть карты.""" | |
players = rooms[token]['users'] | |
for player in players: | |
while len(game_state['hands'].get(player, [])) < 6 and len(game_state['deck']) > 0: | |
game_state['hands'][player].append(game_state['deck'].pop()) | |
#Проверка, не закончилась ли игра | |
if len(game_state['deck']) == 0: | |
players_without_cards = [player for player in players if len(game_state['hands'].get(player,[])) == 0] | |
if len(players_without_cards) > 0: | |
game_state['isGameEnd'] = True | |
game_state['winner'] = players_without_cards[0] # Первый, кто избавился от карт | |
return game_state; | |
def start_timer(token, game_id): | |
if game_id != 'crocodile' and game_id != 'alias': # Таймер пока только для крокодила | |
return | |
def timer_loop(): | |
with app.app_context(): | |
while True: | |
if token not in rooms or rooms[token]['current_game'] != game_id: | |
break # Выход, если комната удалена или игра сменилась | |
if not games_data[game_id]['state'].get(token) or not games_data[game_id]['state'][token].get('isRunning'): | |
break #Выход , исли игра не идет | |
games_data[game_id]['state'][token]['timer'] -= 1 | |
save_json(GAMES_DB, games_data) | |
socketio.emit('timer_tick', {'game_id': game_id, 'time': games_data[game_id]['state'][token]['timer']}, room=token) | |
if games_data[game_id]['state'][token]['timer'] <= 0: | |
games_data[game_id]['state'][token]['isRunning'] = False | |
save_json(GAMES_DB, games_data) | |
socketio.emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, room=token) | |
break # Конец отсчета | |
socketio.sleep(1) | |
socketio.start_background_task(timer_loop) | |
if __name__ == '__main__': | |
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True) |