Initial commit: Shelem card game

Full-stack multiplayer Shelem (Iranian trick-taking card game) with
Socket.IO, JWT auth, bot players, joker mode, and mobile-friendly UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-05-08 16:17:37 +00:00
commit 8e8478e45b
12 changed files with 3869 additions and 0 deletions
+425
View File
@@ -0,0 +1,425 @@
<!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>Shelem</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a3a1a">
<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="Shelem">
<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">🃏 Shelem</h1>
<p class="tagline">The Persian trick-taking partnership game</p>
<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>Game Mode</label>
<div class="mode-row">
<button class="mode-btn active" data-joker="true">🃏 With Jokers <small>(200 pts)</small></button>
<button class="mode-btn" data-joker="false">♠ No Jokers <small>(165 pts)</small></button>
</div>
</div>
<div class="field">
<label>Win Score <small>(first team to reach this wins)</small></label>
<div class="score-row">
<button class="score-btn" data-score="205">205</button>
<button class="score-btn active" data-score="505">505</button>
<button class="score-btn" data-score="1005">1005</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 class="divider">
<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>
<!-- Partnership preview -->
<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 partner-a" data-seat="2"><span class="seat-num">2</span><span class="seat-name"></span></div>
<div class="seat-slot partner-b" data-seat="1"><span class="seat-num">3</span><span class="seat-name"></span></div>
<div class="seat-slot partner-b" data-seat="3"><span class="seat-num">4</span><span class="seat-name"></span></div>
</div>
<p class="hint" style="font-size:.78rem;color:rgba(255,255,255,.4)">Seats 1&amp;2 vs Seats 3&amp;4</p>
<p id="waiting-status" class="waiting-status">Waiting for 3 more players…</p>
<div id="waiting-options" class="waiting-options hidden">
<span class="waiting-opt-label" id="waiting-mode-label"></span>
<span class="waiting-opt-label" id="waiting-score-label"></span>
</div>
<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>
<!-- Team score block -->
<div id="score-block">
<div id="team-score-0" class="team-score"></div>
<div id="team-score-1" class="team-score"></div>
</div>
<!-- Trump + bid indicator -->
<div id="game-meta">
<span id="trump-display" class="trump-display hidden"></span>
<span id="bid-display" class="bid-display hidden"></span>
</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-refresh-game" class="game-menu-item">↺ Reload</button>
<button id="btn-toggle-bar" class="game-menu-item">⬇ Bar to bottom</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 (seat 2, partner of seat 0) -->
<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-partner" class="partner-badge">partner</span>
<span id="top-score" class="player-score-badge"></span>
</div>
<div id="top-cards" class="opp-cards"></div>
</div>
<!-- Left player (seat 1) -->
<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 (seat 3) -->
<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-partner-label" class="partner-badge">partner</span>
<span id="my-score" class="player-score-badge"></span>
<span id="my-tricks" class="tricks-badge hidden"></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 -->
<!-- ══════════════ BIDDING OVERLAY ══════════════ -->
<div id="overlay-bid" class="overlay hidden">
<div class="overlay-box bid-box">
<h3>Bidding</h3>
<div id="bid-history" class="bid-history"></div>
<div id="bid-controls" class="hidden">
<p class="bid-hint">Your turn — enter your bid or pass</p>
<div class="bid-input-row">
<button id="btn-bid-minus" class="bid-adj">5</button>
<span id="bid-amount-display" class="bid-amount-display">85</span>
<button id="btn-bid-plus" class="bid-adj">+5</button>
</div>
<div class="bid-action-row">
<button id="btn-do-bid" class="btn-primary">Bid</button>
<button id="btn-do-pass" class="btn-secondary">Pass</button>
</div>
</div>
<p id="bid-waiting-msg" class="hint" style="min-height:20px"></p>
</div>
</div>
<!-- ══════════════ WIDOW / DISCARD OVERLAY ══════════════ -->
<div id="overlay-widow" class="overlay hidden">
<div class="overlay-box widow-box">
<h3 id="widow-title">Pick up widow — select cards to discard</h3>
<p id="widow-hint" class="pass-hint"></p>
<div id="widow-hand" class="pass-hand"></div>
<div class="pass-selected-row">
<span class="pass-selected-label">Discarding:</span>
<div id="widow-selected-preview" class="pass-selected-preview"></div>
</div>
<button id="btn-confirm-discard" class="btn-primary" disabled>Confirm Discard</button>
<p id="widow-waiting" class="hint" style="min-height:18px"></p>
</div>
</div>
<!-- ══════════════ TRUMP DECLARE OVERLAY ══════════════ -->
<div id="overlay-trump" class="overlay hidden">
<div class="overlay-box trump-box">
<div id="trump-declare-inner">
<h3>Choose Trump Suit</h3>
<p class="hint">You are the declarer — pick your trump</p>
<div class="trump-suit-row">
<button class="trump-suit-btn" data-suit="C">♣ Clubs</button>
<button class="trump-suit-btn" data-suit="D">♦ Diamonds</button>
<button class="trump-suit-btn" data-suit="H">♥ Hearts</button>
<button class="trump-suit-btn" data-suit="S">♠ Spades</button>
</div>
</div>
<div id="trump-waiting-inner" class="hidden">
<div class="result-icon" style="font-size:2rem"></div>
<p id="trump-waiting-msg" class="hint"></p>
</div>
</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,.45)">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 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>Password <small>(min 4 chars)</small></label>
<input id="auth-reg-pass" type="password" placeholder="Choose a password" autocomplete="new-password">
</div>
<button id="btn-do-register" class="btn-primary">Create Account</button>
<p id="auth-reg-error" class="error-msg"></p>
</div>
</div>
</div>
<!-- ══════════════ PROFILE ══════════════ -->
<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-shelem"></span>
<span class="stat-label">Shelems 🃏</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>Scoring at a glance</h4>
<ul>
<li><strong>Ace / 10</strong> — 10 pts each</li>
<li><strong>Five</strong> — 5 pts each</li>
<li><strong>Color Joker</strong> — 20 pts (trump)</li>
<li><strong>Black Joker</strong> — 15 pts (trump)</li>
<li><strong>Each trick won</strong> — 5 pts</li>
<li><strong>Shelem</strong> — win every trick for 250 pts</li>
<li>Default win score: 505 pts</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:540px">
<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 Score</th><th>W</th><th>Played</th><th>🃏</th>
</tr>
</thead>
<tbody id="lb-body"></tbody>
</table>
</div>
</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 &amp; Exit</button>
<button id="btn-exit-confirm-no" class="btn-secondary">Cancel</button>
</div>
</div>
</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>