Files
shelem/public/index.html
T
goyban a4fefd92f1 Feature: public rooms, mobile UX, reconnection, and gameplay fixes
Rooms & lobby
- Rename docker-compose.yml → compose.yml
- Public/Private toggle on room creation; public rooms assign random seats
  to prevent team collusion
- GET /api/rooms API — lists open public rooms; Join tab shows live list
  with one-tap join
- Room creator: swap any two seats by tapping (select-to-swap UI); ▶ Start
  Game button force-starts with bots filling empty seats

Reconnection
- Session moved from sessionStorage → localStorage (survives browser close)
- Socket handlers split: socket.once for one-shot callbacks, persistent
  socket.on('connect') for auto-rejoin on network drops
- Server rejoin accepts userId match as fallback (cross-device rejoin for
  authenticated users); re-issues token on success
- Server emits hasActiveGame on connect so auth'd users on a new device are
  pulled back into their game automatically
- Explicit leave nulls seat/token/userIds so hasActiveGame never re-drags a
  player back in after they chose to leave

Mobile UX
- Remove all opponent/partner card backs; replace with compact card-count
  badge — frees ~120px of vertical space on small phones
- Screen height: 100dvh (dynamic viewport) instead of 100vh — fixes the
  "only top 1/5 visible" issue on phones with browser chrome
- Table grid side columns shrunk to 36px on touch devices; player names
  rotated vertically
- Bidding overlay: transparent non-blocking top panel on touch; hand stays
  visible and interactive; auto fan-mode during bidding
- touch-action: pan-x on hand scroll, none in fan/drag mode — suppresses
  Android back-gesture and Google Gemini conflicts
- user-select: none on game screen prevents long-press selection menus

Gameplay & notifications
- Center trick area now shows whose turn it is instead of trump (trump is
  already in the info bar); flashes gold when it's the player's turn
- Turn reminder after 5 s of inaction: gold glow pulse on hand area
  + Android vibration OR two-note Web Audio chime on iOS (vibrate API not
  supported by Apple)
- Fix: turn reminder was never triggered after winning a trick — justWon
  branch blocked myTurnNow from being set even when currentTurn === mySeat
- Waiting room ☰ menu: Reload and Exit accessible without entering the game
- Prevent duplicate room joins (same socket, same userId, or same name)

Service worker
- Bump to shelem-v2; pre-cache all 55 card SVGs at install time so cards
  are available instantly from the very first hand, including offline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:42:00 +00:00

453 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<div class="field">
<label>Visibility</label>
<div class="visibility-row">
<button class="visibility-btn active" data-public="false">🔒 Private</button>
<button class="visibility-btn" data-public="true">🌐 Public</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="public-rooms-section">
<div class="rooms-header">
<span class="rooms-label">Public Rooms</span>
<button id="btn-refresh-rooms" class="btn-text"></button>
</div>
<div id="public-rooms-list"><p class="hint">Loading…</p></div>
</div>
<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">
<div class="waiting-top-row">
<button id="btn-leave-waiting" class="btn-leave-screen">← Leave</button>
<div class="game-menu-wrap" style="margin-left:auto">
<button id="btn-waiting-menu" class="btn-leave-screen" title="Options"></button>
<div id="waiting-menu-dropdown" class="game-menu-dropdown hidden">
<button id="btn-waiting-reload" class="game-menu-item">↺ Reload</button>
<button id="btn-waiting-exit" class="game-menu-item game-menu-exit">🚪 Exit</button>
</div>
</div>
</div>
<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>
<div id="waiting-creator-controls" class="waiting-creator-controls hidden">
<button id="btn-fill-bots" class="btn-fill-bots">🤖 Fill with bots</button>
<button id="btn-start-game" class="btn-start-game">▶ Start Game</button>
</div>
</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>
<span id="top-count" class="card-count-badge"></span>
</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>
<span id="left-count" class="card-count-badge"></span>
</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>
<span id="right-count" class="card-count-badge"></span>
</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>