Videospace4 / app.py
Aleksmorshen's picture
Update app.py
60ef9b3 verified
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()
@app.route('/', methods=['GET', 'POST'])
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>''')
@app.route('/dashboard', methods=['GET', 'POST'])
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)
@app.route('/logout', methods=['POST'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))
@app.route('/room/<token>')
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)
@socketio.on('join')
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)
@socketio.on('leave')
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)
@socketio.on('signal')
def handle_signal(data):
emit('signal', data, room=data['token'], include_self=False)
@socketio.on('admin_mute')
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)
@socketio.on('start_game')
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)
@socketio.on('set_game_state')
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)
@socketio.on('game_action')
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)