73 Commits

Author SHA1 Message Date
jahlib dfe4821b57 some fixes 2025-11-12 02:21:43 +03:00
jahlib 9c0e4c6e5d Add files via upload 2025-11-12 02:20:45 +03:00
jahlib c848173353 Add files via upload 2025-11-12 01:57:59 +03:00
jahlib 0f0c30caf5 Add files via upload 2025-11-12 01:45:36 +03:00
jahlib f49c89c23f Add files via upload 2025-11-12 01:39:37 +03:00
jahlib f97f57dc81 Add files via upload 2025-11-12 01:38:01 +03:00
jahlib ca18f30315 Add files via upload 2025-11-12 01:34:12 +03:00
jahlib 445498a927 Add files via upload 2025-11-12 01:30:45 +03:00
jahlib aed73aa0a3 Add files via upload 2025-11-12 01:25:50 +03:00
jahlib 390af31d4f Add files via upload 2025-11-12 01:18:50 +03:00
jahlib 8d43654e3c latest 2025-11-11 23:28:16 +03:00
jahlib 5431eebb01 big design update 2025-11-11 23:24:00 +03:00
jahlib a90c1eacbb some updates 2025-11-11 23:23:32 +03:00
jahlib b372674f32 Add files via upload 2025-11-11 23:19:11 +03:00
jahlib e2561a9af1 Add files via upload 2025-11-11 23:16:56 +03:00
jahlib c169cef984 Add files via upload 2025-11-11 23:13:15 +03:00
jahlib 024f20f651 Add files via upload 2025-11-11 23:04:22 +03:00
jahlib e4f45663ac Add files via upload 2025-11-11 23:00:54 +03:00
jahlib b86d506b2d Add files via upload 2025-11-11 22:55:32 +03:00
jahlib 479e51f67a Add files via upload 2025-11-11 22:51:02 +03:00
jahlib f64a0f16d9 Add files via upload 2025-11-11 22:46:16 +03:00
jahlib a03b267fb3 Add files via upload 2025-11-11 22:43:29 +03:00
jahlib 010f25f882 some changes 2025-11-11 18:48:54 +03:00
jahlib 0488e10217 some changes 2025-11-11 18:48:35 +03:00
jahlib a043f25f59 add sounds 2025-11-11 03:05:06 +03:00
jahlib 75b4a8f84b huh and pipe 2025-11-11 02:54:15 +03:00
jahlib f35c234c65 add sounds 2025-11-11 02:53:21 +03:00
jahlib ad610c5f9c add sounds 2025-11-11 02:13:46 +03:00
jahlib a26c3f79fe add sounds 2025-11-11 02:11:44 +03:00
jahlib 77668ba463 bot reaction fix 2025-11-10 17:28:08 +03:00
jahlib e5406ec333 bot fix 2025-11-10 16:56:27 +03:00
jahlib c2354d31fd bot fix 2025-11-10 16:53:14 +03:00
jahlib b75ab0a61b bot reactions 2025-11-10 16:50:44 +03:00
jahlib cb07a64dbc MORE QUICK REACTIONS mousewheel fix 2025-11-10 16:34:57 +03:00
jahlib 85f9f58680 MORE QUICK REACTIONS mousewheel fix 2025-11-10 16:32:12 +03:00
jahlib 5884ea55f9 MORE QUICK REACTIONS 2025-11-10 15:53:34 +03:00
jahlib 43f3836b5f lobby moving 2025-11-10 14:47:18 +03:00
jahlib cdb2968180 flying cards fix 2025-11-10 13:46:16 +03:00
jahlib b0f9461d9d why not frog ? 2025-11-10 13:27:57 +03:00
jahlib 5dce10149c why not frog ? 2025-11-10 13:27:05 +03:00
jahlib e35fb67cf0 fix some fun 2025-11-10 13:11:07 +03:00
jahlib 3545c91bfd some fun 2025-11-10 12:45:02 +03:00
jahlib a793aa57a3 add some fun 2025-11-10 12:44:44 +03:00
jahlib d9284f90f6 just one more night mode css fix... 2025-11-10 02:12:55 +03:00
jahlib 81feb139b9 suit change background color 2025-11-10 01:13:28 +03:00
jahlib a8e3779e07 just some night mode bug fix 2025-11-10 01:11:49 +03:00
jahlib 70d8fb8d3f just one more night mode css fix... 2025-11-09 22:24:28 +03:00
jahlib 01487026f5 just one more night mode css fix... 2025-11-09 17:51:26 +03:00
jahlib 635d0aa64c just one more night mode css fix... 2025-11-09 17:44:34 +03:00
jahlib 9e1b1642d7 modal close behavior 2025-11-09 17:37:59 +03:00
jahlib 05a8e60b26 just one more night mode css fix... 2025-11-09 17:36:28 +03:00
jahlib 342d25a93d just one more night mode css fix... 2025-11-09 17:27:41 +03:00
jahlib 6cd7efbd99 just one more night mode css fix... 2025-11-09 15:52:28 +03:00
jahlib 2b7c6e4284 just one more night mode css fix... 2025-11-09 04:36:48 +03:00
jahlib 4adbe2cf66 just one more night mode css fix... 2025-11-09 04:27:58 +03:00
jahlib b924f6a6ef just one more night mode css fix... 2025-11-09 04:22:33 +03:00
jahlib 93ac3d1c5e night mode rules css fix 2025-11-09 04:04:17 +03:00
jahlib e7c9d38df8 night mode rules fix 2025-11-09 04:03:10 +03:00
jahlib b749d0194a some other stuff 2025-11-09 03:56:08 +03:00
jahlib db975b964e fix night mode and some other stuff 2025-11-09 03:55:19 +03:00
jahlib 7e2e9dcfb8 add night mode toggle 2025-11-09 03:20:40 +03:00
jahlib 21296ab59d add Pro mode on client settings
In Pro mode cards don’t have a disabled state in CSS
2025-11-09 01:14:13 +03:00
jahlib ee15d3c1fd fix cooldown and =101 win 2025-11-07 23:33:50 +03:00
jahlib 3b04d54bcc fix cooldown and =101 win 2025-11-07 23:33:37 +03:00
jahlib 60127088c5 fix private room settings after one game 2025-11-07 11:35:35 +03:00
jahlib 11d7e5be96 bot playable cards bug fix 2025-11-07 02:43:51 +03:00
jahlib 62c2f86678 fix showing roomtoggle with bots 2025-11-06 11:30:34 +03:00
jahlib e69bdabc15 some fixes 2025-11-06 11:26:03 +03:00
jahlib 330b025107 some fixes 2025-11-06 02:20:25 +03:00
jahlib 43017b00bb move private room settings checkbox 2025-11-06 02:19:33 +03:00
jahlib d0f02ed85e add private room support with uniq url 2025-11-06 01:26:59 +03:00
jahlib dec8122c2f add private room support with uniq url 2025-11-06 01:26:43 +03:00
jahlib 93ceffd1f9 disable winqueen choose suit modal 2025-11-05 23:34:58 +03:00
30 changed files with 5326 additions and 195 deletions
+851 -28
View File
File diff suppressed because it is too large Load Diff
+189 -19
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+138 -49
View File
@@ -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;
}
})()
);
});
+1811
View File
File diff suppressed because it is too large Load Diff
+264 -23
View File
@@ -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()