Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfe4821b57 | |||
| 9c0e4c6e5d | |||
| c848173353 | |||
| 0f0c30caf5 | |||
| f49c89c23f | |||
| f97f57dc81 | |||
| ca18f30315 | |||
| 445498a927 | |||
| aed73aa0a3 | |||
| 390af31d4f | |||
| 8d43654e3c | |||
| 5431eebb01 | |||
| a90c1eacbb | |||
| b372674f32 | |||
| e2561a9af1 | |||
| c169cef984 | |||
| 024f20f651 | |||
| e4f45663ac | |||
| b86d506b2d | |||
| 479e51f67a | |||
| f64a0f16d9 | |||
| a03b267fb3 | |||
| 010f25f882 | |||
| 0488e10217 | |||
| a043f25f59 | |||
| 75b4a8f84b | |||
| f35c234c65 | |||
| ad610c5f9c | |||
| a26c3f79fe | |||
| 77668ba463 | |||
| e5406ec333 | |||
| c2354d31fd | |||
| b75ab0a61b | |||
| cb07a64dbc | |||
| 85f9f58680 | |||
| 5884ea55f9 | |||
| 43f3836b5f | |||
| cdb2968180 | |||
| b0f9461d9d | |||
| 5dce10149c | |||
| e35fb67cf0 | |||
| 3545c91bfd | |||
| a793aa57a3 | |||
| d9284f90f6 | |||
| 81feb139b9 | |||
| a8e3779e07 | |||
| 70d8fb8d3f | |||
| 01487026f5 | |||
| 635d0aa64c | |||
| 9e1b1642d7 | |||
| 05a8e60b26 | |||
| 342d25a93d | |||
| 6cd7efbd99 | |||
| 2b7c6e4284 | |||
| 4adbe2cf66 | |||
| b924f6a6ef | |||
| 93ac3d1c5e | |||
| e7c9d38df8 | |||
| b749d0194a | |||
| db975b964e | |||
| 7e2e9dcfb8 | |||
| 21296ab59d | |||
| ee15d3c1fd | |||
| 3b04d54bcc | |||
| 60127088c5 | |||
| 11d7e5be96 | |||
| 62c2f86678 | |||
| e69bdabc15 | |||
| 330b025107 | |||
| 43017b00bb | |||
| d0f02ed85e | |||
| dec8122c2f | |||
| 93ceffd1f9 |
+851
-28
File diff suppressed because it is too large
Load Diff
+189
-19
@@ -22,14 +22,21 @@
|
||||
<!-- iOS Icons -->
|
||||
<link rel="apple-touch-icon" href="/icon-192.png">
|
||||
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<link rel="stylesheet" href="/style.css?v=3">
|
||||
<link rel="stylesheet" href="/night-mode.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Экран лобби -->
|
||||
<div id="lobby-screen" class="screen active">
|
||||
<div class="container">
|
||||
<h1>🎴 Чешский</h1>
|
||||
<div class="lobby-header">
|
||||
<h1>🎴 Чешский</h1>
|
||||
<button id="night-mode-toggle-btn" class="night-mode-btn" title="Переключить ночной режим">
|
||||
<span class="sun-icon" style="display: none;">☀️</span>
|
||||
<span class="moon-icon">🌙</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="create-room-section">
|
||||
<h2>Создать комнату</h2>
|
||||
@@ -63,16 +70,25 @@
|
||||
<div class="container">
|
||||
<div class="room-header">
|
||||
<h2>Комната</h2>
|
||||
<!-- Таймер в заголовке -->
|
||||
<div id="countdown-display" style="display: none;">
|
||||
<span class="countdown-timer-small" id="countdown-number">6</span>
|
||||
</div>
|
||||
<button id="leave-room-btn" class="btn btn-secondary">Покинуть</button>
|
||||
</div>
|
||||
|
||||
<div id="players-waiting">
|
||||
<h3>Игроки в комнате:</h3>
|
||||
<p class="hint">Игра начнётся когда все игроки будут готовы (минимум 2 игрока)</p>
|
||||
<div id="players-list"></div>
|
||||
|
||||
<!-- Кнопка готов после списка игроков -->
|
||||
<button id="ready-toggle-btn" class="btn btn-primary">Готов</button>
|
||||
|
||||
<!-- Настройки комнаты (только для создателя) -->
|
||||
<div id="room-settings" style="display: none;">
|
||||
<h3>Настройки игры</h3>
|
||||
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">36 карт</span>
|
||||
@@ -83,15 +99,34 @@
|
||||
<p class="hint-small">В режиме 36 карт восьмёрка не действует</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="countdown-display" style="display: none;">
|
||||
<div class="countdown-timer">
|
||||
<span id="countdown-number">6</span>
|
||||
|
||||
<!-- Переключатель приватной/публичной комнаты -->
|
||||
<div class="room-settings" id="private-room-settings">
|
||||
<!-- Переключатель приватной комнаты -->
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Публичная</span>
|
||||
<input type="checkbox" id="private-room-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Приватная</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Блок с ссылкой для приглашения (скрыт по умолчанию) -->
|
||||
<div id="invite-link-block" style="display: none;">
|
||||
<p class="invite-hint">Поделитесь ссылкой с друзьями:</p>
|
||||
<div class="invite-link-container">
|
||||
<code id="invite-link" class="invite-link"></code>
|
||||
<button id="copy-link-btn" class="btn btn-copy" title="Копировать ссылку"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1.25H10.9436C9.10583 1.24998 7.65019 1.24997 6.51098 1.40314C5.33856 1.56076 4.38961 1.89288 3.64124 2.64124C2.89288 3.38961 2.56076 4.33856 2.40314 5.51098C2.24997 6.65019 2.24998 8.10582 2.25 9.94357V16C2.25 17.8722 3.62205 19.424 5.41551 19.7047C5.55348 20.4687 5.81753 21.1208 6.34835 21.6517C6.95027 22.2536 7.70814 22.5125 8.60825 22.6335C9.47522 22.75 10.5775 22.75 11.9451 22.75H15.0549C16.4225 22.75 17.5248 22.75 18.3918 22.6335C19.2919 22.5125 20.0497 22.2536 20.6517 21.6517C21.2536 21.0497 21.5125 20.2919 21.6335 19.3918C21.75 18.5248 21.75 17.4225 21.75 16.0549V10.9451C21.75 9.57754 21.75 8.47522 21.6335 7.60825C21.5125 6.70814 21.2536 5.95027 20.6517 5.34835C20.1208 4.81753 19.4687 4.55348 18.7047 4.41551C18.424 2.62205 16.8722 1.25 15 1.25ZM17.1293 4.27117C16.8265 3.38623 15.9876 2.75 15 2.75H11C9.09318 2.75 7.73851 2.75159 6.71085 2.88976C5.70476 3.02502 5.12511 3.27869 4.7019 3.7019C4.27869 4.12511 4.02502 4.70476 3.88976 5.71085C3.75159 6.73851 3.75 8.09318 3.75 10V16C3.75 16.9876 4.38624 17.8265 5.27117 18.1293C5.24998 17.5194 5.24999 16.8297 5.25 16.0549V10.9451C5.24998 9.57754 5.24996 8.47522 5.36652 7.60825C5.48754 6.70814 5.74643 5.95027 6.34835 5.34835C6.95027 4.74643 7.70814 4.48754 8.60825 4.36652C9.47522 4.24996 10.5775 4.24998 11.9451 4.25H15.0549C15.8297 4.24999 16.5194 4.24998 17.1293 4.27117ZM7.40901 6.40901C7.68577 6.13225 8.07435 5.9518 8.80812 5.85315C9.56347 5.75159 10.5646 5.75 12 5.75H15C16.4354 5.75 17.4365 5.75159 18.1919 5.85315C18.9257 5.9518 19.3142 6.13225 19.591 6.40901C19.8678 6.68577 20.0482 7.07435 20.1469 7.80812C20.2484 8.56347 20.25 9.56458 20.25 11V16C20.25 17.4354 20.2484 18.4365 20.1469 19.1919C20.0482 19.9257 19.8678 20.3142 19.591 20.591C19.3142 20.8678 18.9257 21.0482 18.1919 21.1469C17.4365 21.2484 16.4354 21.25 15 21.25H12C10.5646 21.25 9.56347 21.2484 8.80812 21.1469C8.07435 21.0482 7.68577 20.8678 7.40901 20.591C7.13225 20.3142 6.9518 19.9257 6.85315 19.1919C6.75159 18.4365 6.75 17.4354 6.75 16V11C6.75 9.56458 6.75159 8.56347 6.85315 7.80812C6.9518 7.07435 7.13225 6.68577 7.40901 6.40901Z" fill="#FFFFFF"/>
|
||||
</svg></button>
|
||||
<button id="share-link-btn" class="btn btn-share" title="Поделиться" style="display: none;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 1.25H11.9426C9.63423 1.24999 7.82519 1.24998 6.41371 1.43975C4.96897 1.63399 3.82895 2.03933 2.93414 2.93414C2.03933 3.82895 1.63399 4.96897 1.43975 6.41371C1.24998 7.82519 1.24999 9.63423 1.25 11.9426V12.0574C1.24999 14.3658 1.24998 16.1748 1.43975 17.5863C1.63399 19.031 2.03933 20.1711 2.93414 21.0659C3.82895 21.9607 4.96897 22.366 6.41371 22.5603C7.82519 22.75 9.63423 22.75 11.9426 22.75H12.0574C14.3658 22.75 16.1748 22.75 17.5863 22.5603C19.031 22.366 20.1711 21.9607 21.0659 21.0659C21.9607 20.1711 22.366 19.031 22.5603 17.5863C22.75 16.1748 22.75 14.3658 22.75 12.0574V12C22.75 11.5858 22.4142 11.25 22 11.25C21.5858 11.25 21.25 11.5858 21.25 12C21.25 14.3782 21.2484 16.0864 21.0736 17.3864C20.9018 18.6648 20.5749 19.4355 20.0052 20.0052C19.4355 20.5749 18.6648 20.9018 17.3864 21.0736C16.0864 21.2484 14.3782 21.25 12 21.25C9.62178 21.25 7.91356 21.2484 6.61358 21.0736C5.33517 20.9018 4.56445 20.5749 3.9948 20.0052C3.42514 19.4355 3.09825 18.6648 2.92637 17.3864C2.75159 16.0864 2.75 14.3782 2.75 12C2.75 9.62178 2.75159 7.91356 2.92637 6.61358C3.09825 5.33517 3.42514 4.56445 3.9948 3.9948C4.56445 3.42514 5.33517 3.09825 6.61358 2.92637C7.91356 2.75159 9.62178 2.75 12 2.75C12.4142 2.75 12.75 2.41421 12.75 2C12.75 1.58579 12.4142 1.25 12 1.25Z" fill="#FFFFFF"/>
|
||||
<path d="M12.4697 10.4697C12.1768 10.7626 12.1768 11.2374 12.4697 11.5303C12.7626 11.8232 13.2374 11.8232 13.5303 11.5303L21.25 3.81066V7.34375C21.25 7.75796 21.5858 8.09375 22 8.09375C22.4142 8.09375 22.75 7.75796 22.75 7.34375V2C22.75 1.58579 22.4142 1.25 22 1.25H16.6562C16.242 1.25 15.9062 1.58579 15.9062 2C15.9062 2.41421 16.242 2.75 16.6562 2.75H20.1893L12.4697 10.4697Z" fill="#FFFFFF"/>
|
||||
</svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="countdown-text">Игра начинается...</p>
|
||||
</div>
|
||||
<button id="ready-toggle-btn" class="btn btn-primary">Готов</button>
|
||||
<p class="hint">Игра начнётся когда все игроки будут готовы (минимум 2 игрока)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,15 +245,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно быстрых реакций -->
|
||||
<div id="reaction-picker" class="reaction-picker">
|
||||
<div class="reaction-picker-scroll">
|
||||
<button class="reaction-btn" data-reaction="🙃">🙃</button>
|
||||
<button class="reaction-btn" data-reaction="🙁">🙁</button>
|
||||
<button class="reaction-btn" data-reaction="😡">😡</button>
|
||||
<button class="reaction-btn" data-reaction="😈">😈</button>
|
||||
<button class="reaction-btn" data-reaction="😇">😇</button>
|
||||
<button class="reaction-btn" data-reaction="😠">😠</button>
|
||||
<button class="reaction-btn" data-reaction="😄">😄</button>
|
||||
<button class="reaction-btn" data-reaction="😎">😎</button>
|
||||
<button class="reaction-btn" data-reaction="🤔">🤔</button>
|
||||
<button class="reaction-btn" data-reaction="😤">😤</button>
|
||||
<button class="reaction-btn" data-reaction="😐">😐</button>
|
||||
<button class="reaction-btn" data-reaction="👍">👍</button>
|
||||
<button class="reaction-btn" data-reaction="👎">👎</button>
|
||||
<button class="reaction-btn" data-reaction="🤝">🤝</button>
|
||||
<button class="reaction-btn" data-reaction="🫰">🫰</button>
|
||||
<button class="reaction-btn" data-reaction="🪬">🪬</button>
|
||||
<button class="reaction-btn" data-reaction="🤯">🤯</button>
|
||||
<button class="reaction-btn" data-reaction="🤡">🤡</button>
|
||||
<button class="reaction-btn" data-reaction="🤨">🤨</button>
|
||||
<button class="reaction-btn" data-reaction="😑">😑</button>
|
||||
<button class="reaction-btn" data-reaction="😌">😌</button>
|
||||
<button class="reaction-btn" data-reaction="😴">😴</button>
|
||||
<button class="reaction-btn" data-reaction="🌚">🌚</button>
|
||||
<button class="reaction-btn" data-reaction="🐱">🐱</button>
|
||||
<button class="reaction-btn" data-reaction="🐸">🐸</button>
|
||||
<button class="reaction-btn" data-reaction="🐺">🐺</button>
|
||||
<button class="reaction-btn" data-reaction="🐊">🐊</button>
|
||||
<button class="reaction-btn" data-reaction="🐠">🐠</button>
|
||||
<button class="reaction-btn" data-reaction="🦆">🦆</button>
|
||||
<button class="reaction-btn" data-reaction="🌹">🌹</button>
|
||||
<button class="reaction-btn" data-reaction="🗿">🗿</button>
|
||||
<button class="reaction-btn" data-reaction="👁️">👁️</button>
|
||||
<button class="reaction-btn" data-reaction="💩">💩</button>
|
||||
<button class="reaction-btn" data-reaction="🔩">🔩</button>
|
||||
<button class="reaction-btn" data-reaction="🔪">🔪</button>
|
||||
<button class="reaction-btn" data-reaction="⚔️">⚔️</button>
|
||||
<button class="reaction-btn" data-reaction="🧀">🧀</button>
|
||||
<button class="reaction-btn" data-reaction="🎺">🎺</button>
|
||||
<button class="reaction-btn" data-reaction="🎁">🎁</button>
|
||||
<button class="reaction-btn" data-reaction="🔮">🔮</button>
|
||||
<button class="reaction-btn" data-reaction="🎲">🎲</button>
|
||||
<button class="reaction-btn" data-reaction="🎯">🎯</button>
|
||||
<button class="reaction-btn" data-reaction="♥️">♥️</button>
|
||||
<button class="reaction-btn" data-reaction="♦️">♦️</button>
|
||||
<button class="reaction-btn" data-reaction="♣️">♣️</button>
|
||||
<button class="reaction-btn" data-reaction="♠️">♠️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно выбора масти -->
|
||||
<div id="suit-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Выберите масть</h3>
|
||||
<div class="suit-selector">
|
||||
<button class="suit-btn" data-suit="hearts">♥️ Черви</button>
|
||||
<button class="suit-btn" data-suit="diamonds">♦️ Бубны</button>
|
||||
<button class="suit-btn" data-suit="clubs">♣️ Трефы</button>
|
||||
<button class="suit-btn" data-suit="spades">♠️ Пики</button>
|
||||
<button class="suit-btn" data-suit="hearts"><span class="suit-emoji">♥️</span> Черви</button>
|
||||
<button class="suit-btn" data-suit="diamonds"><span class="suit-emoji">♦️</span> Бубны</button>
|
||||
<button class="suit-btn" data-suit="clubs"><span class="suit-emoji">♣️</span> Трефы</button>
|
||||
<button class="suit-btn" data-suit="spades"><span class="suit-emoji">♠️</span> Пики</button>
|
||||
</div>
|
||||
<div class="modal-buttons" style="margin-top: 15px;">
|
||||
<button id="cancel-suit-btn" class="btn btn-secondary">Отмена</button>
|
||||
@@ -287,7 +374,7 @@
|
||||
<div class="settings-content">
|
||||
<!-- Полноэкранный режим -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">Полноэкранный режим</span>
|
||||
<span class="setting-label">⛶ Полноэкранный режим</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
@@ -300,7 +387,7 @@
|
||||
|
||||
<!-- Звук -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">Звук</span>
|
||||
<span class="setting-label">🔊 Звук</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
@@ -313,7 +400,7 @@
|
||||
|
||||
<!-- Анимации -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">Анимации карт</span>
|
||||
<span class="setting-label">💫 Анимации карт</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
@@ -324,9 +411,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Режим Про -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🎓 Режим Про</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="pro-mode-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ночной режим -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🌙 Ночной режим</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="night-mode-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Белые масти (только в ночном режиме) -->
|
||||
<div class="setting-item" id="white-suits-setting" style="display: none;">
|
||||
<span class="setting-label">🤍 Белые масти</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="white-suits-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Лог (только на мобильных) -->
|
||||
<div class="setting-item mobile-only">
|
||||
<span class="setting-label">Показывать лог</span>
|
||||
<span class="setting-label">📃 Показывать лог</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
@@ -336,13 +462,57 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор цвета рубашки карт -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🎴 Цвет рубашки карт</span>
|
||||
<div class="card-back-selector">
|
||||
<div class="card-back-option" data-back="gray">
|
||||
<div class="card-back-preview card-back-gray"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="green">
|
||||
<div class="card-back-preview card-back-green"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="blue">
|
||||
<div class="card-back-preview card-back-blue"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="red">
|
||||
<div class="card-back-preview card-back-red"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="purple">
|
||||
<div class="card-back-preview card-back-purple"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор цвета фона -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🎨 Цвет фона</span>
|
||||
<div class="background-selector">
|
||||
<div class="background-option" data-bg="default">
|
||||
<div class="background-preview bg-default"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="green">
|
||||
<div class="background-preview bg-green"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="blue">
|
||||
<div class="background-preview bg-blue"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="purple">
|
||||
<div class="background-preview bg-purple"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="orange">
|
||||
<div class="background-preview bg-orange"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/marked.min.js"></script>
|
||||
<script src="/game.js"></script>
|
||||
<script src="/marked.min.js?v=3"></script>
|
||||
<script src="/game.js?v=3"></script>
|
||||
|
||||
<!-- Service Worker Registration -->
|
||||
<script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+790
-76
File diff suppressed because it is too large
Load Diff
+138
-49
@@ -1,26 +1,84 @@
|
||||
const CACHE_NAME = 'czech-fool-v1';
|
||||
const CACHE_TIMESTAMP_KEY = 'cache-timestamps';
|
||||
const CACHE_MAX_AGE = 60 * 60 * 1000; // 1 час в миллисекундах
|
||||
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/style.css',
|
||||
'/game.js',
|
||||
'/sounds/playcard.ogg',
|
||||
'/sounds/drawcard.ogg',
|
||||
'/sounds/eight.ogg',
|
||||
'/sounds/change.ogg',
|
||||
'/sounds/skip.ogg',
|
||||
'/sounds/alert.ogg',
|
||||
'/sounds/chat.ogg',
|
||||
'/sounds/win.ogg',
|
||||
'/sounds/lose.ogg',
|
||||
'/sounds/two.ogg',
|
||||
'/sounds/six.ogg',
|
||||
'/sounds/seven.ogg',
|
||||
'/sounds/shuffle.ogg',
|
||||
'/sounds/ace.ogg',
|
||||
'/sounds/eightplace.ogg'
|
||||
'/sounds/playcard.aac',
|
||||
'/sounds/drawcard.aac',
|
||||
'/sounds/eight.aac',
|
||||
'/sounds/change.aac',
|
||||
'/sounds/skip.aac',
|
||||
'/sounds/alert.aac',
|
||||
'/sounds/chat.aac',
|
||||
'/sounds/win.aac',
|
||||
'/sounds/lose.aac',
|
||||
'/sounds/two.aac',
|
||||
'/sounds/six.aac',
|
||||
'/sounds/seven.aac',
|
||||
'/sounds/shuffle.aac',
|
||||
'/sounds/ace.aac',
|
||||
'/sounds/eightplace.aac'
|
||||
];
|
||||
|
||||
// Функция для получения временных меток из IndexedDB
|
||||
async function getTimestamps() {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_TIMESTAMP_KEY);
|
||||
const response = await cache.match('timestamps');
|
||||
if (response) {
|
||||
return await response.json();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error reading timestamps:', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Функция для сохранения временных меток
|
||||
async function saveTimestamps(timestamps) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_TIMESTAMP_KEY);
|
||||
const response = new Response(JSON.stringify(timestamps));
|
||||
await cache.put('timestamps', response);
|
||||
} catch (e) {
|
||||
console.log('Error saving timestamps:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для очистки старых записей кеша
|
||||
async function cleanOldCache() {
|
||||
try {
|
||||
const timestamps = await getTimestamps();
|
||||
const now = Date.now();
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const requests = await cache.keys();
|
||||
|
||||
let cleaned = 0;
|
||||
for (const request of requests) {
|
||||
const url = request.url;
|
||||
const timestamp = timestamps[url];
|
||||
|
||||
// Если запись старше часа - удаляем
|
||||
if (timestamp && (now - timestamp) > CACHE_MAX_AGE) {
|
||||
await cache.delete(request);
|
||||
delete timestamps[url];
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
console.log(`Cleaned ${cleaned} old cache entries`);
|
||||
await saveTimestamps(timestamps);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error cleaning cache:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Установка service worker
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
@@ -35,16 +93,21 @@ self.addEventListener('install', event => {
|
||||
// Активация service worker
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
Promise.all([
|
||||
// Удаляем старые версии кеша
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== CACHE_NAME && cacheName !== CACHE_TIMESTAMP_KEY) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
// Очищаем записи старше часа
|
||||
cleanOldCache()
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -56,33 +119,59 @@ self.addEventListener('fetch', event => {
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
// Возвращаем из кеша если есть
|
||||
if (response) {
|
||||
(async () => {
|
||||
// Проверяем кеш
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
|
||||
if (cachedResponse) {
|
||||
// Проверяем возраст записи
|
||||
const timestamps = await getTimestamps();
|
||||
const url = event.request.url;
|
||||
const timestamp = timestamps[url];
|
||||
const now = Date.now();
|
||||
|
||||
// Если запись свежая (младше часа) - возвращаем из кеша
|
||||
if (timestamp && (now - timestamp) < CACHE_MAX_AGE) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Если запись старая - удаляем и загружаем заново
|
||||
console.log('Cache expired for:', url);
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.delete(event.request);
|
||||
delete timestamps[url];
|
||||
await saveTimestamps(timestamps);
|
||||
}
|
||||
|
||||
// Загружаем с сервера
|
||||
try {
|
||||
const fetchRequest = event.request.clone();
|
||||
const response = await fetch(fetchRequest);
|
||||
|
||||
// Проверяем валидность ответа
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Клонируем запрос
|
||||
const fetchRequest = event.request.clone();
|
||||
// Кешируем новый ресурс с временной меткой
|
||||
const responseToCache = response.clone();
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.put(event.request, responseToCache);
|
||||
|
||||
return fetch(fetchRequest).then(response => {
|
||||
// Проверяем валидность ответа
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Клонируем ответ
|
||||
const responseToCache = response.clone();
|
||||
|
||||
// Кешируем новый ресурс
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
})
|
||||
// Сохраняем временную метку
|
||||
const timestamps = await getTimestamps();
|
||||
timestamps[event.request.url] = Date.now();
|
||||
await saveTimestamps(timestamps);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log('Fetch failed:', error);
|
||||
// Если есть старый кеш - возвращаем его как fallback
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ class Room:
|
||||
last_loser_id: Optional[str] = None # ID проигравшего в прошлой игре (станет дилером)
|
||||
deck_size: int = 52 # Размер колоды: 52 или 36 карт
|
||||
creator_id: Optional[str] = None # ID создателя комнаты
|
||||
is_private: bool = False # Приватная комната (не отображается в списке)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@@ -72,7 +73,8 @@ class Room:
|
||||
'player_count': len(self.players),
|
||||
'game_started': self.game_started,
|
||||
'deck_size': self.deck_size,
|
||||
'creator_id': self.creator_id
|
||||
'creator_id': self.creator_id,
|
||||
'is_private': self.is_private
|
||||
}
|
||||
|
||||
class GameServer:
|
||||
@@ -80,6 +82,7 @@ class GameServer:
|
||||
self.rooms: Dict[str, Room] = {}
|
||||
self.clients: Dict[WebSocketServerProtocol, str] = {} # ws -> player_id
|
||||
self.player_rooms: Dict[str, str] = {} # player_id -> room_id
|
||||
self.shake_cooldowns: Dict[str, float] = {} # player_id -> timestamp последней тряски
|
||||
self.db = GameDatabase(
|
||||
host="localhost",
|
||||
port=5432,
|
||||
@@ -168,7 +171,8 @@ class GameServer:
|
||||
waiting_for_eight=room_data['waiting_for_eight'],
|
||||
card_drawn_this_turn=room_data['card_drawn_this_turn'],
|
||||
deck_size=room_data.get('deck_size', 52),
|
||||
creator_id=room_data.get('creator_id')
|
||||
creator_id=room_data.get('creator_id'),
|
||||
is_private=room_data.get('is_private', False)
|
||||
)
|
||||
|
||||
# Инициализируем дополнительные атрибуты
|
||||
@@ -203,7 +207,8 @@ class GameServer:
|
||||
'eight_draw_used': room.eight_draw_used,
|
||||
'card_drawn_this_turn': room.card_drawn_this_turn,
|
||||
'deck_size': room.deck_size,
|
||||
'creator_id': room.creator_id
|
||||
'creator_id': room.creator_id,
|
||||
'is_private': room.is_private
|
||||
})
|
||||
|
||||
# Сохраняем игроков
|
||||
@@ -309,6 +314,8 @@ class GameServer:
|
||||
|
||||
async def handle_create_room(self, ws: WebSocketServerProtocol, data: dict):
|
||||
nickname = data.get('nickname')
|
||||
is_private = data.get('is_private', False)
|
||||
|
||||
if not nickname:
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Nickname required'}))
|
||||
return
|
||||
@@ -331,7 +338,8 @@ class GameServer:
|
||||
current_player_index=0,
|
||||
dealer_index=0,
|
||||
game_started=False,
|
||||
creator_id=player_id
|
||||
creator_id=player_id,
|
||||
is_private=is_private
|
||||
)
|
||||
|
||||
self.rooms[room_id] = room
|
||||
@@ -449,11 +457,19 @@ class GameServer:
|
||||
|
||||
room = self.rooms.get(room_id)
|
||||
if not room:
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Room not found'}))
|
||||
await ws.send(json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Комната не найдена или больше не существует',
|
||||
'error_code': 'room_not_found'
|
||||
}))
|
||||
return
|
||||
|
||||
if room.game_started:
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Game already started'}))
|
||||
await ws.send(json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Игра уже началась. Присоединиться невозможно.',
|
||||
'error_code': 'game_started'
|
||||
}))
|
||||
return
|
||||
|
||||
# Проверяем максимальное количество игроков
|
||||
@@ -521,6 +537,9 @@ class GameServer:
|
||||
ready_count = sum(1 for p in room.players.values() if p.ready)
|
||||
total_count = len(room.players)
|
||||
|
||||
# Проверяем это первая игра (у всех 0 очков) или между раундами
|
||||
is_first_game = all(p.score == 0 for p in room.players.values())
|
||||
|
||||
# Если все готовы - начинаем сразу
|
||||
if total_count >= 2 and ready_count == total_count:
|
||||
# Отменяем таймер если был
|
||||
@@ -529,8 +548,8 @@ class GameServer:
|
||||
room.countdown_task = None
|
||||
room.countdown_active = False
|
||||
await self.start_game(room_id)
|
||||
# Если >= 2 готовы но не все - запускаем таймер
|
||||
elif ready_count >= 2 and not room.countdown_active:
|
||||
# Если >= 2 готовы но не все - запускаем таймер ТОЛЬКО при первой игре
|
||||
elif ready_count >= 2 and not room.countdown_active and is_first_game:
|
||||
room.countdown_active = True
|
||||
room.countdown_task = asyncio.create_task(self.start_countdown(room_id))
|
||||
# Если готовых стало меньше 2 - отменяем таймер
|
||||
@@ -577,7 +596,7 @@ class GameServer:
|
||||
|
||||
async def make_bot_move(self, room_id: str, bot_id: str):
|
||||
"""Бот делает ход"""
|
||||
await asyncio.sleep(1.5) # Задержка для реалистичности
|
||||
await asyncio.sleep(random.uniform(0.88, 1.82)) # Задержка для реалистичности
|
||||
|
||||
room = self.rooms.get(room_id)
|
||||
if not room or not room.game_started:
|
||||
@@ -607,12 +626,18 @@ class GameServer:
|
||||
# Находим подходящие карты
|
||||
playable_cards = []
|
||||
for card in bot.hand:
|
||||
# Для восьмёрки: можно играть двойку из руки ИЛИ любую карту из eight_drawn_cards
|
||||
# Для восьмёрки: можно играть двойку из руки ИЛИ подходящую карту из eight_drawn_cards
|
||||
if room.waiting_for_eight:
|
||||
if card.rank == '2':
|
||||
playable_cards.append(card)
|
||||
elif hasattr(room, 'eight_drawn_cards') and card.id in room.eight_drawn_cards:
|
||||
playable_cards.append(card)
|
||||
# Проверяем что карта действительно подходит:
|
||||
# двойка, дама, восьмёрка или та же масть что и восьмёрка на столе
|
||||
if (card.rank == '2' or
|
||||
card.rank == 'Q' or
|
||||
card.rank == '8' or
|
||||
card.suit == top_card.suit):
|
||||
playable_cards.append(card)
|
||||
elif self.can_play_card(card, top_card, room.chosen_suit, room.waiting_for_eight):
|
||||
playable_cards.append(card)
|
||||
|
||||
@@ -1230,14 +1255,21 @@ class GameServer:
|
||||
|
||||
# Обрабатываем специальные случаи с очками
|
||||
players_to_kick = [] # Игроки с >101 очком
|
||||
players_reset = [] # Игроки с ровно 101 (обнуляются)
|
||||
for player_id, player in room.players.items():
|
||||
if player.score == 101:
|
||||
# Ровно 101 - обнуляем очки
|
||||
players_reset.append(player_id)
|
||||
player.score = 0
|
||||
elif player.score > 101:
|
||||
# Больше 101 - игрок вылетает
|
||||
players_to_kick.append(player_id)
|
||||
|
||||
# Добавляем флаг reset_to_zero в результаты для обнулившихся игроков
|
||||
for result in results:
|
||||
if result['player_id'] in players_reset:
|
||||
result['reset_to_zero'] = True
|
||||
|
||||
# Сохраняем ID проигравшего в раздаче для следующей игры (если он не вылетает)
|
||||
if round_loser_id and round_loser_id not in players_to_kick:
|
||||
room.last_loser_id = round_loser_id
|
||||
@@ -1423,7 +1455,11 @@ class GameServer:
|
||||
room = self.rooms.get(room_id)
|
||||
if not room:
|
||||
# print(f"Room {room_id} not found in DB")
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Room not found'}))
|
||||
await ws.send(json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Комната не найдена или больше не существует',
|
||||
'error_code': 'room_not_found'
|
||||
}))
|
||||
return
|
||||
|
||||
# print(f"Room {room_id} loaded, players: {list(room.players.keys())}")
|
||||
@@ -1431,7 +1467,11 @@ class GameServer:
|
||||
# Проверяем существование игрока
|
||||
if player_id not in room.players:
|
||||
# print(f"Player {player_id} not found in room {room_id}")
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Player not found in room'}))
|
||||
await ws.send(json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Игрок не найден в этой комнате',
|
||||
'error_code': 'player_not_found'
|
||||
}))
|
||||
return
|
||||
|
||||
# Переподключаем игрока
|
||||
@@ -1497,7 +1537,8 @@ class GameServer:
|
||||
if not room.game_started and all(p.score == 0 for p in room.players.values()):
|
||||
# Проверяем что есть хотя бы один живой игрок (не бот)
|
||||
has_human = any(not p.is_bot for p in room.players.values())
|
||||
if has_human:
|
||||
# Не показываем приватные комнаты в общем списке
|
||||
if has_human and not room.is_private:
|
||||
available_rooms.append(room.to_dict())
|
||||
|
||||
message = json.dumps({
|
||||
@@ -1551,6 +1592,7 @@ class GameServer:
|
||||
await self.send_to_player(pid, {
|
||||
'type': 'chat_message',
|
||||
'message': chat_message,
|
||||
'message_text': message_text,
|
||||
'player_id': player_id,
|
||||
'player_nickname': player.nickname
|
||||
})
|
||||
@@ -1604,6 +1646,176 @@ class GameServer:
|
||||
'deck_size': deck_size
|
||||
})
|
||||
|
||||
async def handle_toggle_private(self, ws: WebSocketServerProtocol, data: dict):
|
||||
"""Переключение приватности комнаты (только создатель, до начала игры)"""
|
||||
player_id = self.clients.get(ws)
|
||||
if not player_id:
|
||||
return
|
||||
|
||||
room_id = self.player_rooms.get(player_id)
|
||||
if not room_id:
|
||||
return
|
||||
|
||||
room = self.rooms.get(room_id)
|
||||
if not room:
|
||||
return
|
||||
|
||||
# Только создатель может менять приватность
|
||||
if room.creator_id != player_id:
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Только создатель может изменить приватность комнаты'}))
|
||||
return
|
||||
|
||||
# Только до начала игры
|
||||
if room.game_started or any(p.score > 0 for p in room.players.values()):
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Нельзя изменить приватность после начала игры'}))
|
||||
return
|
||||
|
||||
is_private = data.get('is_private', False)
|
||||
room.is_private = is_private
|
||||
|
||||
# Сохраняем в БД
|
||||
await self.save_room_to_db(room_id)
|
||||
|
||||
# Уведомляем всех в комнате
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'room_privacy_changed',
|
||||
'is_private': is_private
|
||||
})
|
||||
|
||||
# Обновляем список комнат (приватные не показываются)
|
||||
await self.broadcast_rooms()
|
||||
|
||||
async def handle_shake_discard(self, ws: WebSocketServerProtocol, data: dict):
|
||||
"""Обработка тряски карты сброса - рассылаем всем игрокам в комнате"""
|
||||
player_id = self.clients.get(ws)
|
||||
if not player_id:
|
||||
return
|
||||
|
||||
# Проверяем кулдаун (5 секунд)
|
||||
import time
|
||||
now = time.time()
|
||||
cooldown = 5.0 # 5 секунд
|
||||
|
||||
last_shake = self.shake_cooldowns.get(player_id, 0)
|
||||
if now - last_shake < cooldown:
|
||||
# Просто игнорируем запрос если кулдаун активен
|
||||
return
|
||||
|
||||
# Обновляем время последней тряски
|
||||
self.shake_cooldowns[player_id] = now
|
||||
|
||||
# Находим комнату игрока
|
||||
room_id = None
|
||||
for rid, room in self.rooms.items():
|
||||
if player_id in room.players:
|
||||
room_id = rid
|
||||
break
|
||||
|
||||
if not room_id:
|
||||
return
|
||||
|
||||
# Отправляем событие тряски всем игрокам в комнате
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'shake_discard'
|
||||
})
|
||||
|
||||
async def handle_reaction(self, ws: WebSocketServerProtocol, data: dict):
|
||||
"""Обработка быстрой реакции - рассылаем всем игрокам в комнате"""
|
||||
player_id = self.clients.get(ws)
|
||||
if not player_id:
|
||||
return
|
||||
|
||||
emoji = data.get('emoji')
|
||||
if not emoji:
|
||||
return
|
||||
|
||||
# Находим комнату игрока
|
||||
room_id = None
|
||||
for rid, room in self.rooms.items():
|
||||
if player_id in room.players:
|
||||
room_id = rid
|
||||
break
|
||||
|
||||
if not room_id:
|
||||
return
|
||||
|
||||
room = self.rooms[room_id]
|
||||
|
||||
# Отправляем реакцию всем игрокам в комнате
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'reaction',
|
||||
'player_id': player_id,
|
||||
'emoji': emoji
|
||||
})
|
||||
|
||||
# Если реакция отправлена боту, один случайный бот отвечает
|
||||
if player_id.startswith('bot_'):
|
||||
return # Боты не отвечают на реакции других ботов
|
||||
|
||||
# Находим всех ботов в комнате
|
||||
bots = [pid for pid in room.players if pid.startswith('bot_')]
|
||||
|
||||
if bots:
|
||||
# Запускаем ответ бота в отдельной задаче чтобы не блокировать игру
|
||||
asyncio.create_task(self.bot_reaction_response(room_id, bots))
|
||||
|
||||
async def bot_reaction_response(self, room_id: str, bots: list):
|
||||
"""Бот отвечает на реакцию с задержкой"""
|
||||
# Выбираем одного случайного бота для ответа
|
||||
responding_bot = random.choice(bots)
|
||||
|
||||
# Список всех эмодзи для реакций
|
||||
reaction_emojis = ['🙃', '🙁', '😡', '😈', '😇', '😠', '😄', '😎', '🤔', '😤',
|
||||
'😐', '👍', '👎', '🤝', '🫰', '🪬', '🤯', '🤡', '🤨', '😑',
|
||||
'😌', '😴', '🌚', '🐱', '🐸', '🐺', '🐊', '🐠', '🌹', '🗿',
|
||||
'👁️', '💩', '🔩', '🔪', '⚔️', '🎺', '🔮', '🎲', '🎯',
|
||||
'♥️', '♦️', '♣️', '♠️']
|
||||
|
||||
# Выбираем случайный эмодзи
|
||||
bot_emoji = random.choice(reaction_emojis)
|
||||
|
||||
# Отправляем реакцию от бота с небольшой задержкой
|
||||
await asyncio.sleep(random.uniform(1.7, 3.9))
|
||||
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'reaction',
|
||||
'player_id': responding_bot,
|
||||
'emoji': bot_emoji
|
||||
})
|
||||
|
||||
async def handle_leave_room(self, ws: WebSocketServerProtocol, data: dict):
|
||||
"""Явный выход из комнаты"""
|
||||
player_id = self.clients.get(ws)
|
||||
if not player_id:
|
||||
return
|
||||
|
||||
room_id = self.player_rooms.get(player_id)
|
||||
if not room_id:
|
||||
return
|
||||
|
||||
room = self.rooms.get(room_id)
|
||||
if not room or player_id not in room.players:
|
||||
return
|
||||
|
||||
# Удаляем игрока из комнаты
|
||||
del room.players[player_id]
|
||||
del self.player_rooms[player_id]
|
||||
|
||||
# Если это была приватная комната создателя или комната пустая - удаляем её
|
||||
if (room.is_private and room.creator_id == player_id) or len(room.players) == 0:
|
||||
del self.rooms[room_id]
|
||||
# Удаляем из БД
|
||||
await self.delete_room_from_db(room_id)
|
||||
else:
|
||||
# Уведомляем остальных игроков
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'player_left',
|
||||
'player_id': player_id
|
||||
})
|
||||
|
||||
# Обновляем список комнат
|
||||
await self.broadcast_rooms()
|
||||
|
||||
async def handle_message(self, ws: WebSocketServerProtocol, message: str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
@@ -1631,6 +1843,14 @@ class GameServer:
|
||||
await self.handle_chat_message(ws, data)
|
||||
elif msg_type == 'change_deck_size':
|
||||
await self.handle_change_deck_size(ws, data)
|
||||
elif msg_type == 'toggle_private':
|
||||
await self.handle_toggle_private(ws, data)
|
||||
elif msg_type == 'shake_discard':
|
||||
await self.handle_shake_discard(ws, data)
|
||||
elif msg_type == 'reaction':
|
||||
await self.handle_reaction(ws, data)
|
||||
elif msg_type == 'leave_room':
|
||||
await self.handle_leave_room(ws, data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await ws.send(json.dumps({'type': 'error', 'message': 'Invalid JSON'}))
|
||||
@@ -1686,17 +1906,38 @@ class GameServer:
|
||||
# Игра не началась и ни у кого нет очков - удаляем игрока полностью
|
||||
del room.players[player_id]
|
||||
|
||||
# Проверяем остались ли живые игроки (не боты)
|
||||
human_players = [p for p in room.players.values() if not p.is_bot]
|
||||
|
||||
if len(human_players) == 0:
|
||||
# Если остались только боты или никого - удаляем комнату
|
||||
del self.rooms[room_id]
|
||||
else:
|
||||
# Если это создатель приватной комнаты - удаляем комнату полностью
|
||||
if room.is_private and room.creator_id == player_id:
|
||||
# Уведомляем всех игроков что комната закрывается
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'player_left',
|
||||
'player_id': player_id
|
||||
'type': 'room_closed',
|
||||
'message': 'Создатель комнаты покинул игру. Комната закрыта.'
|
||||
})
|
||||
|
||||
# Удаляем всех игроков из player_rooms
|
||||
for pid in list(room.players.keys()):
|
||||
if pid in self.player_rooms:
|
||||
del self.player_rooms[pid]
|
||||
|
||||
# Удаляем комнату из памяти
|
||||
del self.rooms[room_id]
|
||||
|
||||
# Удаляем комнату из БД
|
||||
await self.db.delete_room(room_id)
|
||||
else:
|
||||
# Проверяем остались ли живые игроки (не боты)
|
||||
human_players = [p for p in room.players.values() if not p.is_bot]
|
||||
|
||||
if len(human_players) == 0:
|
||||
# Если остались только боты или никого - удаляем комнату
|
||||
del self.rooms[room_id]
|
||||
# Удаляем из БД
|
||||
await self.db.delete_room(room_id)
|
||||
else:
|
||||
await self.broadcast_to_room(room_id, {
|
||||
'type': 'player_left',
|
||||
'player_id': player_id
|
||||
})
|
||||
|
||||
await self.broadcast_rooms()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user