Initial commit

This commit is contained in:
goyban
2026-04-26 11:52:07 +00:00
commit 460c9d57ed
14 changed files with 3927 additions and 0 deletions
+1235
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
/root/hokm/public/cards
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="22" fill="#145228"/>
<text x="50" y="68" font-size="58" text-anchor="middle" fill="#e53935"></text>
</svg>

After

Width:  |  Height:  |  Size: 212 B

+415
View File
@@ -0,0 +1,415 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Hearts</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#0d2744">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Hearts">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- ══════════════ LOBBY ══════════════ -->
<div id="screen-lobby" class="screen active">
<div class="lobby-box">
<h1 class="logo">♥ Hearts ♠</h1>
<p class="tagline">The classic trick-taking card game</p>
<button id="btn-install" class="btn-install hidden">📲 Add to Home Screen</button>
<div id="auth-bar" class="auth-bar">
<span id="auth-status" class="auth-status">Playing as guest</span>
<div class="auth-bar-actions">
<button id="btn-show-leaderboard" class="btn-text">🏆 Leaderboard</button>
<button id="btn-show-profile" class="btn-text hidden">👤 Profile</button>
<button id="btn-logout" class="btn-text hidden">Logout</button>
<button id="btn-show-auth" class="btn-text">Login / Register</button>
</div>
</div>
<div class="field">
<label>Your Name</label>
<input id="input-name" type="text" maxlength="16" placeholder="Enter your name…" autocomplete="off">
</div>
<div class="lobby-tabs">
<button class="tab-btn active" data-tab="create">Create Game</button>
<button class="tab-btn" data-tab="join">Join Game</button>
</div>
<div id="tab-create" class="tab-panel active">
<div class="field">
<label>Score limit <small>(game ends when a player reaches this)</small></label>
<div class="score-limit-row">
<button class="score-btn active" data-score="100">100</button>
<button class="score-btn" data-score="50">50</button>
<button class="score-btn" data-score="200">200</button>
</div>
</div>
<button id="btn-create" class="btn-primary">Create Game</button>
</div>
<div id="tab-join" class="tab-panel">
<div class="field">
<label>Room Code</label>
<input id="input-code" type="text" maxlength="8" placeholder="e.g. AB12CD" autocomplete="off" style="text-transform:uppercase">
</div>
<button id="btn-join" class="btn-primary">Join Game</button>
<hr style="border-color:rgba(255,255,255,.1);margin:4px 0">
<div class="field">
<label>Watch as spectator</label>
<input id="input-spectate-code" type="text" maxlength="8" placeholder="Room code" style="text-transform:uppercase" autocomplete="off">
</div>
<button id="btn-spectate" class="btn-secondary">👁 Watch Game</button>
</div>
<p id="lobby-error" class="error-msg"></p>
</div>
</div>
<!-- ══════════════ WAITING ROOM ══════════════ -->
<div id="screen-waiting" class="screen">
<div class="waiting-box">
<button id="btn-leave-waiting" class="btn-leave-screen">← Leave</button>
<h2>Waiting for Players</h2>
<div class="room-code-box">
<span class="label">Room Code</span>
<span id="display-room-code" class="room-code">——</span>
<button id="btn-copy" class="btn-copy" title="Copy code"></button>
</div>
<p class="hint">Share this code — 4 players needed</p>
<div class="waiting-seats" id="waiting-seats">
<div class="seat-slot" data-seat="0"><span class="seat-num">1</span><span class="seat-name"></span></div>
<div class="seat-slot" data-seat="1"><span class="seat-num">2</span><span class="seat-name"></span></div>
<div class="seat-slot" data-seat="2"><span class="seat-num">3</span><span class="seat-name"></span></div>
<div class="seat-slot" data-seat="3"><span class="seat-num">4</span><span class="seat-name"></span></div>
</div>
<p id="waiting-status" class="waiting-status">Waiting for 3 more players…</p>
<button id="btn-fill-bots" class="btn-fill-bots hidden">🤖 Fill empty seats with bots</button>
</div>
</div>
<!-- ══════════════ GAME TABLE ══════════════ -->
<div id="screen-game" class="screen">
<!-- Info bar -->
<div id="info-bar">
<button id="btn-leave-game" class="btn-leave-screen">← Menu</button>
<!-- Individual scores -->
<div id="score-block">
<div id="score-display"></div>
<div id="hand-points-display"></div>
</div>
<!-- Hearts broken indicator -->
<div id="hearts-broken-display" class="hearts-broken hidden">♥ Broken</div>
<!-- Pass direction indicator -->
<div id="pass-dir-display" class="pass-dir-display hidden"></div>
<div class="game-menu-wrap">
<button id="btn-game-menu" class="btn-leave-screen" title="Options"></button>
<div id="game-menu-dropdown" class="game-menu-dropdown hidden">
<button id="btn-info-pos" class="game-menu-item">↕ Move score bar</button>
<button id="btn-refresh-game" class="game-menu-item">↺ Reload</button>
<button id="btn-exit-game" class="game-menu-item game-menu-exit">🚪 Exit</button>
</div>
</div>
</div>
<!-- Spectator banner -->
<div id="spectator-banner" class="spectator-banner hidden">👁 Spectating — watching only</div>
<!-- Table grid -->
<div id="table-grid">
<!-- Top player -->
<div id="area-top" class="player-area area-top">
<div class="player-label">
<span id="top-name"></span>
<span id="top-turn" class="turn-dot hidden"></span>
<span id="top-score" class="player-score-badge"></span>
</div>
<div id="top-cards" class="opp-cards"></div>
</div>
<!-- Left player -->
<div id="area-left" class="player-area area-left">
<div class="player-label vertical">
<span id="left-name"></span>
<span id="left-turn" class="turn-dot hidden"></span>
<span id="left-score" class="player-score-badge"></span>
</div>
<div id="left-cards" class="opp-cards vertical"></div>
</div>
<!-- Center / trick area -->
<div id="trick-area">
<div id="trick-top" class="trick-slot"></div>
<div id="trick-middle" class="trick-middle-row">
<div id="trick-left" class="trick-slot"></div>
<div id="trick-center" class="trick-center-info">
<div id="phase-msg" class="phase-msg"></div>
</div>
<div id="trick-right" class="trick-slot"></div>
</div>
<div id="trick-bottom" class="trick-slot"></div>
</div>
<!-- Right player -->
<div id="area-right" class="player-area area-right">
<div class="player-label vertical">
<span id="right-name"></span>
<span id="right-turn" class="turn-dot hidden"></span>
<span id="right-score" class="player-score-badge"></span>
</div>
<div id="right-cards" class="opp-cards vertical"></div>
</div>
</div><!-- /table-grid -->
<!-- Bottom: my hand -->
<div id="my-area">
<div id="my-label">
<span id="my-name">You</span>
<span id="my-turn" class="turn-dot hidden"></span>
<span id="my-score" class="player-score-badge"></span>
<span id="my-hand-pts" class="hand-pts-badge"></span>
<button id="btn-play-mode" class="btn-play-mode hidden" title="Switch play mode">👆 Tap</button>
<button id="btn-hand-mode" class="btn-hand-mode" title="Switch hand display">📜 Scroll</button>
</div>
<div id="my-hand" class="my-hand"></div>
</div>
</div><!-- /screen-game -->
<!-- ══════════════ PASS OVERLAY ══════════════ -->
<div id="overlay-pass" class="overlay hidden">
<div class="overlay-box pass-box">
<h3 id="pass-title">Pass 3 cards</h3>
<p id="pass-hint" class="pass-hint"></p>
<div id="pass-hand" class="pass-hand"></div>
<div class="pass-selected-row">
<span class="pass-selected-label">Selected:</span>
<div id="pass-selected-preview" class="pass-selected-preview"></div>
</div>
<button id="btn-confirm-pass" class="btn-primary" disabled>Confirm Pass</button>
<p id="pass-waiting" class="hint" style="min-height:18px"></p>
</div>
</div>
<!-- ══════════════ HAND RESULT OVERLAY ══════════════ -->
<div id="overlay-hand" class="overlay hidden">
<div class="overlay-box">
<div id="hand-result-icon" class="result-icon"></div>
<h3 id="hand-result-title"></h3>
<p id="hand-result-detail"></p>
<div id="hand-result-scores" class="result-scores"></div>
<p class="hint">Next hand starting…</p>
</div>
</div>
<!-- ══════════════ GAME OVER OVERLAY ══════════════ -->
<div id="overlay-gameover" class="overlay hidden">
<div class="overlay-box">
<div class="result-icon big">🏆</div>
<h2 id="gameover-title"></h2>
<div id="gameover-scores" class="gameover-scores"></div>
<button id="btn-new-game" class="btn-primary">New Game</button>
</div>
</div>
<!-- ══════════════ AUTH MODAL ══════════════ -->
<div id="overlay-auth" class="overlay hidden">
<div class="overlay-box auth-box">
<button id="btn-auth-close" class="btn-close"></button>
<div class="auth-tabs">
<button class="auth-tab active" data-auth-tab="login">Login</button>
<button class="auth-tab" data-auth-tab="register">Register</button>
</div>
<div id="auth-panel-login" class="auth-panel active">
<div class="field">
<label>Username</label>
<input id="auth-login-user" type="text" maxlength="16" placeholder="Username" autocomplete="username">
</div>
<div class="field">
<label>Password</label>
<input id="auth-login-pass" type="password" placeholder="Password" autocomplete="current-password">
</div>
<p class="hint" style="font-size:.8rem;color:rgba(255,255,255,.5)">Hokm accounts work here too</p>
<button id="btn-do-login" class="btn-primary">Login</button>
<p id="auth-login-error" class="error-msg"></p>
</div>
<div id="auth-panel-register" class="auth-panel">
<div id="reg-step-form">
<div class="field">
<label>Username <small>(216 chars)</small></label>
<input id="auth-reg-user" type="text" maxlength="16" placeholder="Choose a username" autocomplete="username">
</div>
<div class="field">
<label>Email</label>
<input id="auth-reg-email" type="email" placeholder="your@email.com" autocomplete="email">
</div>
<div class="field">
<label>Password <small>(min 4 chars)</small></label>
<input id="auth-reg-pass" type="password" placeholder="Choose a password" autocomplete="new-password">
</div>
<div id="auth-captcha-wrap" class="hidden" style="margin:6px 0"></div>
<button id="btn-do-register" class="btn-primary">Send Verification Code</button>
<p id="auth-reg-error" class="error-msg"></p>
</div>
<div id="reg-step-verify" class="hidden">
<p id="reg-verify-hint" style="margin:0 0 12px;color:var(--muted);font-size:.9em"></p>
<div class="field">
<label>Verification Code</label>
<input id="auth-reg-code" type="text" inputmode="numeric" maxlength="6" placeholder="6-digit code" autocomplete="one-time-code">
</div>
<button id="btn-do-verify" class="btn-primary">Create Account</button>
<button id="btn-reg-back" class="btn-secondary" style="margin-top:6px;width:100%">← Back</button>
<p id="auth-verify-error" class="error-msg"></p>
</div>
</div>
</div>
</div>
<!-- ══════════════ PROFILE SCREEN ══════════════ -->
<div id="screen-profile" class="screen">
<div class="profile-wrap">
<div class="profile-box">
<button id="btn-profile-back" class="btn-back">← Back</button>
<div class="profile-avatar"></div>
<h2 id="profile-username" class="profile-name"></h2>
<div class="stat-grid">
<div class="stat-card">
<span class="stat-num" id="stat-games-played"></span>
<span class="stat-label">Games Played</span>
</div>
<div class="stat-card">
<span class="stat-num" id="stat-games-won"></span>
<span class="stat-label">Games Won</span>
</div>
<div class="stat-card">
<span class="stat-num" id="stat-moon-shots"></span>
<span class="stat-label">Moon Shots 🌙</span>
</div>
<div class="stat-card">
<span class="stat-num" id="stat-total-score"></span>
<span class="stat-label">Total Score</span>
</div>
</div>
<div class="points-legend">
<h4>How to win</h4>
<ul>
<li><strong>1 pt</strong> each ♥ heart taken</li>
<li><strong>13 pts</strong> taking the ♠Q (Queen of Spades)</li>
<li><strong>Shoot the Moon</strong> take all 13 hearts + ♠Q: you score 0, everyone else scores 26</li>
<li>Game ends when any player reaches the score limit. <strong>Lowest score wins.</strong></li>
</ul>
</div>
<div class="profile-section">
<button id="btn-show-change-pass" class="btn-secondary" style="width:100%">🔑 Change Password</button>
<div id="change-pass-form" class="hidden" style="margin-top:10px;display:flex;flex-direction:column;gap:8px">
<div class="field">
<label>Current Password</label>
<input id="change-pass-current" type="password" placeholder="Current password" autocomplete="current-password">
</div>
<div class="field">
<label>New Password <small>(min 4 chars)</small></label>
<input id="change-pass-new" type="password" placeholder="New password" autocomplete="new-password">
</div>
<button id="btn-do-change-pass" class="btn-primary">Update Password</button>
<p id="change-pass-msg" class="error-msg"></p>
</div>
</div>
<div id="admin-panel" class="profile-section admin-section hidden">
<h4 class="admin-title">⚙ Admin</h4>
<div class="admin-row">
<span>New registrations</span>
<button id="btn-toggle-signups" class="btn-secondary btn-admin-toggle"></button>
</div>
</div>
</div>
</div>
</div>
<!-- ══════════════ LEADERBOARD ══════════════ -->
<div id="screen-leaderboard" class="screen">
<div class="profile-wrap">
<div class="profile-box" style="max-width:520px">
<button id="btn-lb-back" class="btn-back">← Back</button>
<div class="profile-avatar">🏆</div>
<h2 class="profile-name">Leaderboard</h2>
<table class="lb-table">
<thead>
<tr>
<th>#</th>
<th>Player</th>
<th>Avg ♥</th>
<th>W</th>
<th>Played</th>
<th>🌙</th>
<th></th>
</tr>
</thead>
<tbody id="lb-body"></tbody>
</table>
</div>
</div>
</div>
<!-- ══════════════ PLAYER DETAILS MODAL ══════════════ -->
<div id="overlay-player-details" class="overlay hidden">
<div class="modal-box">
<button id="btn-details-close" class="btn-back" style="margin-bottom:12px">← Close</button>
<div class="profile-avatar" style="font-size:2rem"></div>
<h3 id="details-username" style="margin:8px 0 16px;color:#fff"></h3>
<table class="lb-table" style="width:100%">
<tbody id="details-body"></tbody>
</table>
</div>
</div>
<!-- ══════════════ EXIT CONFIRM ══════════════ -->
<div id="overlay-exit-confirm" class="overlay hidden">
<div class="overlay-box small" style="text-align:center">
<h3>Leave Game?</h3>
<p style="font-size:.88rem;color:rgba(255,255,255,.65);margin:8px 0 20px">Your session will be discarded.<br>You will not be able to rejoin.</p>
<div style="display:flex;gap:10px;justify-content:center">
<button id="btn-exit-confirm-yes" class="btn-primary" style="background:rgba(200,50,50,.7);border-color:rgba(200,50,50,.9)">Leave & Exit</button>
<button id="btn-exit-confirm-no" class="btn-secondary">Cancel</button>
</div>
</div>
</div>
<!-- ── PWA Install Banner ── -->
<div id="pwa-banner" class="pwa-banner hidden">
<img src="/icons/icon-192.png" class="pwa-banner-icon" alt="">
<div class="pwa-banner-text">
<strong>Install Hearts</strong>
<span>Play offline, launch like an app</span>
</div>
<button id="pwa-banner-install" class="pwa-banner-btn">Install</button>
<button id="pwa-banner-dismiss" class="pwa-banner-close" aria-label="Dismiss"></button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="app.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
{
"name": "Hearts - Card Game",
"short_name": "Hearts",
"description": "The classic trick-taking card game",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#0d2744",
"theme_color": "#0d2744",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
+1021
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
'use strict';
const CACHE_NAME = 'hearts-v1';
const PRECACHE = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/manifest.json',
'/icons/icon-192.png',
'/icons/icon-512.png',
'/icons/icon.svg',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE))
);
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener('fetch', event => {
const url = event.request.url;
if (event.request.method !== 'GET') return;
if (url.includes('/socket.io/')) return;
if (url.includes('/cards/')) {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return fetch(event.request).then(res => {
caches.open(CACHE_NAME).then(c => c.put(event.request, res.clone()));
return res;
});
})
);
return;
}
event.respondWith(
fetch(event.request)
.then(res => {
caches.open(CACHE_NAME).then(c => c.put(event.request, res.clone()));
return res;
})
.catch(() => caches.match(event.request))
);
});