Below is the code for Social Matrix Games.
<!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>Social Matrix Game</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0a0a;
--surface: #111111;
--surface2: #1a1a1a;
--border: #2a2a2a;
--border-bright: #444;
--gold: #c9a84c;
--gold-dim: #7a6030;
--gold-bright: #e8c96a;
--crimson: #8b1a1a;
--crimson-bright: #c0392b;
--sage: #2d5a3d;
--sage-bright: #3d7a52;
--ice: #1a3a5c;
--ice-bright: #2a5a8c;
--text: #e8e0d0;
--text-dim: #8a8070;
--text-muted: #504840;
--radius: 4px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Crimson Pro', Georgia, serif;
background: var(--bg);
color: var(--text);
min-height: 100dvh;
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
overflow-x: hidden;
background-image:
radial-gradient(ellipse at 20% 0%, rgba(201,168,76,0.04) 0%, transparent 60%),
radial-gradient(ellipse at 80% 100%, rgba(139,26,26,0.04) 0%, transparent 60%);
}
/* ── Header ── */
header {
text-align: center;
padding: 12px 0 6px;
border-bottom: 1px solid var(--gold-dim);
position: relative;
}
header::before {
content: '✦ ✦ ✦';
display: block;
font-size: 0.5rem;
color: var(--gold-dim);
letter-spacing: 0.5em;
margin-bottom: 6px;
}
header h1 {
font-family: 'Cinzel', serif;
font-size: 1.3rem;
font-weight: 900;
color: var(--gold);
letter-spacing: 0.15em;
text-transform: uppercase;
text-shadow: 0 0 30px rgba(201,168,76,0.3);
}
.status-row {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 6px;
font-size: 0.72rem;
color: var(--text-dim);
font-family: 'Cinzel', serif;
letter-spacing: 0.08em;
}
.status-pip {
display: flex;
align-items: center;
gap: 5px;
}
.pip-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--gold-dim);
}
.pip-dot.live { background: var(--sage-bright); animation: blink 2s infinite; }
.pip-dot.dead { background: var(--crimson-bright); }
@keyframes blink {
0%,100% { opacity:1; } 50% { opacity:0.3; }
}
/* ── Panels ── */
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
position: relative;
}
.panel::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
}
.panel-title {
font-family: 'Cinzel', serif;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--gold);
padding: 7px 12px 6px;
background: rgba(201,168,76,0.04);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 8px;
}
.panel-title .title-icon { font-size: 0.8rem; }
.scroll-box {
height: 180px;
overflow-y: auto;
padding: 10px 12px;
font-size: 0.92rem;
line-height: 1.65;
word-break: break-word;
scrollbar-width: thin;
scrollbar-color: var(--border-bright) transparent;
}
.scroll-box::-webkit-scrollbar { width: 3px; }
.scroll-box::-webkit-scrollbar-track { background: transparent; }
.scroll-box::-webkit-scrollbar-thumb { background: var(--border-bright); }
/* Old Games panel gets a taller scroll box */
.scroll-box-tall {
height: 260px;
overflow-y: auto;
padding: 10px 12px;
font-size: 0.92rem;
line-height: 1.65;
word-break: break-word;
scrollbar-width: thin;
scrollbar-color: var(--border-bright) transparent;
}
.scroll-box-tall::-webkit-scrollbar { width: 3px; }
.scroll-box-tall::-webkit-scrollbar-track { background: transparent; }
.scroll-box-tall::-webkit-scrollbar-thumb { background: var(--border-bright); }
.empty-state {
color: var(--text-muted);
font-style: italic;
font-size: 0.82rem;
text-align: center;
padding: 18px 0;
font-family: 'Crimson Pro', serif;
}
/* Log entry styles */
.log-entry {
padding: 6px 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.log-entry:last-child { border-bottom: none; }
.log-scenario {
font-family: 'Cinzel', serif;
font-size: 0.82rem;
font-weight: 700;
color: var(--gold-bright);
letter-spacing: 0.05em;
}
.log-tally {
font-size: 0.78rem;
color: var(--text-muted);
font-style: italic;
margin-top: 2px;
}
.log-timeout {
color: var(--crimson-bright);
font-style: italic;
font-size: 0.82rem;
}
/* Old game divider */
.old-game-divider {
border: none;
border-top: 1px solid var(--gold-dim);
margin: 10px 0;
opacity: 0.5;
}
.text-struck { text-decoration: line-through; opacity: 0.55; }
.text-blue { color: #6ab0e8; }
.text-green { color: #6ac98a; }
.text-challenged { color: var(--crimson-bright); font-style: italic; font-size: 0.8rem; }
.text-replaced { color: var(--gold); font-style: italic; font-size: 0.8rem; }
.text-replace-fail { color: #e8c46a; font-style: italic; font-size: 0.8rem; }
/* ── Button grid ── */
.btn-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 7px;
}
.btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
padding: 11px 6px;
border-radius: var(--radius);
border: 1px solid transparent;
font-family: 'Cinzel', serif;
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
transition: all 0.12s;
-webkit-tap-highlight-color: transparent;
position: relative;
overflow: hidden;
}
.btn:active { transform: scale(0.95); }
.btn-sub {
font-family: 'Crimson Pro', serif;
font-size: 0.72rem;
font-weight: 400;
letter-spacing: 0;
text-transform: none;
font-style: italic;
opacity: 0.8;
}
.btn-gold {
background: linear-gradient(160deg, #2a2010, #1a1508);
border-color: var(--gold-dim);
color: var(--gold);
}
.btn-gold:active { background: linear-gradient(160deg, #3a3020, #2a2010); }
.btn-crimson {
background: linear-gradient(160deg, #2a0808, #1a0505);
border-color: #5a1515;
color: #e88080;
}
.btn-crimson:active { background: linear-gradient(160deg, #3a1010, #2a0808); }
.btn-sage {
background: linear-gradient(160deg, #0a1f12, #061208);
border-color: #1a4a28;
color: #6ac98a;
}
.btn-sage:active { background: linear-gradient(160deg, #102a18, #0a1f12); }
.btn-ice {
background: linear-gradient(160deg, #0a1520, #060e15);
border-color: #1a3a5c;
color: #6ab0e8;
}
.btn-ice:active { background: linear-gradient(160deg, #102030, #0a1520); }
.btn-dim {
background: var(--surface2);
border-color: var(--border);
color: var(--text-dim);
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
background: rgba(201,168,76,0.15);
border: 1px solid var(--gold-dim);
border-radius: 10px;
font-size: 0.68rem;
font-weight: 900;
color: var(--gold);
padding: 0 4px;
font-family: 'Cinzel', serif;
}
.timer-tag {
font-size: 0.62rem;
background: rgba(106,176,232,0.1);
border: 1px solid #1a3a5c;
border-radius: 3px;
padding: 1px 5px;
color: #6ab0e8;
font-family: 'Cinzel', serif;
font-variant-numeric: tabular-nums;
}
/* ── Modals ── */
.overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
backdrop-filter: blur(6px);
z-index: 100;
align-items: center;
justify-content: center;
padding: 14px;
}
.overlay.open { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border-bright);
border-radius: 6px;
padding: 18px 16px;
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 13px;
animation: riseIn 0.18s ease;
position: relative;
}
.modal::before {
content: '';
position: absolute;
top: 0; left: 10%; right: 10%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
@keyframes riseIn {
from { transform: translateY(16px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-heading {
font-family: 'Cinzel', serif;
font-size: 0.9rem;
font-weight: 700;
color: var(--gold);
letter-spacing: 0.08em;
}
.modal-sub {
font-size: 0.82rem;
color: var(--text-dim);
margin-top: 2px;
font-style: italic;
}
.field-label {
font-family: 'Cinzel', serif;
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 4px;
}
textarea {
width: 100%;
background: #0d0d0d;
border: 1px solid var(--border-bright);
border-radius: var(--radius);
color: var(--text);
font-family: 'Crimson Pro', serif;
font-size: 0.92rem;
line-height: 1.5;
padding: 9px 11px;
resize: none;
outline: none;
transition: border-color 0.15s;
}
textarea:focus { border-color: var(--gold-dim); }
textarea::placeholder { color: var(--text-muted); font-style: italic; }
.char-count {
font-size: 0.68rem;
color: var(--text-muted);
text-align: right;
margin-top: 3px;
font-family: 'Cinzel', serif;
}
.char-count.near { color: var(--gold-dim); }
.char-count.full { color: var(--crimson-bright); }
.modal-body-text {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text);
}
.big-timer {
font-family: 'Cinzel', serif;
font-size: 2rem;
font-weight: 700;
text-align: center;
color: var(--gold);
letter-spacing: 0.1em;
font-variant-numeric: tabular-nums;
}
.btn-row {
display: flex;
gap: 8px;
}
.mbtn {
flex: 1;
padding: 9px 12px;
border-radius: var(--radius);
border: 1px solid transparent;
font-family: 'Cinzel', serif;
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
transition: all 0.12s;
-webkit-tap-highlight-color: transparent;
}
.mbtn:active { transform: scale(0.96); }
.mbtn-gold {
background: linear-gradient(135deg, #2a2010, #1a1508);
border-color: var(--gold-dim);
color: var(--gold);
}
.mbtn-crimson {
background: linear-gradient(135deg, #2a0808, #1a0505);
border-color: #5a1515;
color: #e88080;
}
.mbtn-sage {
background: linear-gradient(135deg, #0a1f12, #061208);
border-color: #1a4a28;
color: #6ac98a;
}
.mbtn-ice {
background: linear-gradient(135deg, #0a1520, #060e15);
border-color: #1a3a5c;
color: #6ab0e8;
}
.mbtn-dim {
background: var(--surface2);
border-color: var(--border);
color: var(--text-dim);
}
/* Replace timer bar */
.rbar-wrap {
height: 3px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.rbar-fill {
height: 100%;
background: linear-gradient(90deg, var(--sage-bright), var(--gold), var(--crimson-bright));
transition: width 1s linear;
}
.rbar-label {
font-family: 'Cinzel', serif;
font-size: 0.62rem;
color: var(--text-muted);
text-align: center;
letter-spacing: 0.08em;
margin-top: 3px;
}
/* Toast */
.toast {
position: fixed;
bottom: 18px;
left: 50%;
transform: translateX(-50%) translateY(16px);
background: var(--surface2);
border: 1px solid var(--gold-dim);
border-radius: 20px;
padding: 7px 18px;
font-family: 'Cinzel', serif;
font-size: 0.68rem;
letter-spacing: 0.08em;
color: var(--gold);
z-index: 999;
opacity: 0;
transition: all 0.22s;
pointer-events: none;
white-space: nowrap;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<header>
<h1>Social Matrix</h1>
<div class="status-row">
<div class="status-pip">
<div class="pip-dot" id="connDot"></div>
<span id="connLabel">Connecting</span>
</div>
<div class="status-pip" id="gameTimerPip" style="display:none">
<span>⏳</span>
<span id="gameTimerVal">4:00:00</span>
</div>
</div>
</header>
<!-- Game Log -->
<div class="panel">
<div class="panel-title"><span class="title-icon">📜</span> Game Log</div>
<div class="scroll-box" id="gameLog">
<div class="empty-state">No game in progress. Start a new game to begin.</div>
</div>
</div>
<!-- Judge Move -->
<div class="panel">
<div class="panel-title"><span class="title-icon">⚖️</span> Judge Move</div>
<div class="scroll-box" id="judgeMove">
<div class="empty-state">No move submitted yet.</div>
</div>
</div>
<!-- Buttons -->
<div class="btn-grid">
<button class="btn btn-crimson" onclick="doReplace()">
<span>Replace</span>
</button>
<button class="btn btn-gold" onclick="doChallenge()">
<span>Challenge</span>
<span class="badge" id="challengeBadge">0</span>
</button>
<button class="btn btn-ice" onclick="doComment()">
<span>Comment</span>
</button>
<button class="btn btn-sage" onclick="doSummarize()">
<span>Summarize</span>
</button>
<button class="btn btn-gold" onclick="doMakeMove()">
<span>Make a Move</span>
<span class="timer-tag" id="turnTimerTag">—</span>
</button>
<button class="btn btn-dim" onclick="openOverlay('newGameOverlay')">
<span>New Game</span>
</button>
</div>
<!-- Old Games -->
<div class="panel">
<div class="panel-title"><span class="title-icon">🗃️</span> Old Games</div>
<div class="scroll-box-tall" id="oldGamesLog">
<div class="empty-state">No previous games yet.</div>
</div>
</div>
<!-- ══════════════ OVERLAYS ══════════════ -->
<!-- New Game -->
<div class="overlay" id="newGameOverlay">
<div class="modal">
<div>
<div class="modal-heading">Start a New Game</div>
<div class="modal-sub">Describe the scenario for players to navigate</div>
</div>
<div>
<div class="field-label">Title / Scenario Problem</div>
<textarea id="scenarioTA" rows="5" maxlength="500"
placeholder="Describe the opening scenario… (up to 500 characters)"
oninput="countChars(this,'scenarioCount',500)"></textarea>
<div class="char-count" id="scenarioCount">0 / 500</div>
</div>
<div class="btn-row">
<button class="mbtn mbtn-dim" onclick="closeOverlay('newGameOverlay')">Cancel</button>
<button class="mbtn mbtn-gold" onclick="submitNewGame()">Submit New Game</button>
</div>
</div>
</div>
<!-- Make a Move -->
<div class="overlay" id="makeMoveOverlay">
<div class="modal">
<div>
<div class="modal-heading">Make a Move</div>
<div class="modal-sub">What does your character do?</div>
</div>
<div>
<div class="field-label">Write a Move</div>
<textarea id="moveTA" rows="4" maxlength="200"
placeholder="Describe your action… (up to 200 characters)"
oninput="countChars(this,'moveCount',200)"></textarea>
<div class="char-count" id="moveCount">0 / 200</div>
</div>
<div class="btn-row">
<button class="mbtn mbtn-dim" onclick="closeOverlay('makeMoveOverlay')">Cancel</button>
<button class="mbtn mbtn-sage" onclick="submitMove()">Submit Move</button>
</div>
</div>
</div>
<!-- Timer Block -->
<div class="overlay" id="timerBlockOverlay">
<div class="modal">
<div class="modal-heading">⏳ Hold Your Move</div>
<div class="modal-body-text">You can't make a new move until the timer runs out.</div>
<div class="big-timer" id="blockTimerVal">3:00</div>
<button class="mbtn mbtn-dim" onclick="closeOverlay('timerBlockOverlay')">Return to the Game</button>
</div>
</div>
<!-- Summarize -->
<div class="overlay" id="summarizeOverlay">
<div class="modal">
<div>
<div class="modal-heading">Summarize</div>
<div class="modal-sub">Condense the current judge move</div>
</div>
<div>
<div class="field-label">Summarize the move in fewer words</div>
<textarea id="summaryTA" rows="3" maxlength="50"
placeholder="Short summary… (up to 50 characters)"
oninput="countChars(this,'summaryCount',50)"></textarea>
<div class="char-count" id="summaryCount">0 / 50</div>
</div>
<div class="btn-row">
<button class="mbtn mbtn-dim" onclick="closeOverlay('summarizeOverlay')">Cancel</button>
<button class="mbtn mbtn-ice" onclick="submitSummary()">Submit Summary</button>
</div>
</div>
</div>
<!-- Comment -->
<div class="overlay" id="commentOverlay">
<div class="modal">
<div>
<div class="modal-heading">Add a Comment</div>
<div class="modal-sub">Your comment appears in green</div>
</div>
<div>
<div class="field-label">Write a Comment</div>
<textarea id="commentTA" rows="3" maxlength="100"
placeholder="Your comment… (up to 100 characters)"
oninput="countChars(this,'commentCount',100)"></textarea>
<div class="char-count" id="commentCount">0 / 100</div>
</div>
<div class="btn-row">
<button class="mbtn mbtn-dim" onclick="closeOverlay('commentOverlay')">Cancel</button>
<button class="mbtn mbtn-sage" onclick="submitComment()">Submit Comment</button>
</div>
</div>
</div>
<!-- Challenge Result -->
<div class="overlay" id="challengeResultOverlay">
<div class="modal">
<div class="modal-heading" id="chalResultTitle">Challenge Result</div>
<div class="modal-body-text" id="chalResultBody"></div>
<button class="mbtn mbtn-dim" onclick="closeOverlay('challengeResultOverlay')">Return to the Game</button>
</div>
</div>
<!-- Already Challenged -->
<div class="overlay" id="alreadyChallengedOverlay">
<div class="modal">
<div class="modal-heading">⛔ Cannot Replace</div>
<div class="modal-body-text">This move has already been challenged so it cannot be replaced.</div>
<button class="mbtn mbtn-dim" onclick="closeOverlay('alreadyChallengedOverlay')">Return to the Game</button>
</div>
</div>
<!-- Replace -->
<div class="overlay" id="replaceOverlay">
<div class="modal">
<div>
<div class="modal-heading">Replace the Move</div>
<div class="modal-sub">You have 1 minute to propose a replacement</div>
</div>
<div class="rbar-wrap">
<div class="rbar-fill" id="rbarFill" style="width:100%"></div>
</div>
<div class="rbar-label" id="rbarLabel">1:00 remaining</div>
<div>
<div class="field-label">Write what happens instead</div>
<textarea id="replaceTA" rows="3" maxlength="100"
placeholder="What happens instead? (up to 100 characters)"
oninput="countChars(this,'replaceCount',100)"></textarea>
<div class="char-count" id="replaceCount">0 / 100</div>
</div>
<div class="btn-row">
<button class="mbtn mbtn-dim" onclick="cancelReplace()">Cancel</button>
<button class="mbtn mbtn-crimson" onclick="submitReplacement()">Submit Replacement</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- ══════════════ FIREBASE + LOGIC ══════════════ -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
import { getDatabase, ref, onValue, set, update } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-database.js";
const firebaseConfig = {
apiKey: "AIzaSyAw97JWLLBMpznRcYnrYpFOXNyVDQgXyAc",
authDomain: "social-matrix-game.firebaseapp.com",
databaseURL: "https://social-matrix-game-default-rtdb.firebaseio.com",
projectId: "social-matrix-game",
storageBucket: "social-matrix-game.firebasestorage.app",
messagingSenderId: "324499539002",
appId: "1:324499539002:web:4b519896394e5cd617a103"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
const gRef = ref(db, 'game_state');
// ── State ──────────────────────────────────────────
let S = {
game_log: '', judge_move: '',
challenge_counter: 0,
game_timer: null, turn_timer: null, replace_timer: null,
old_games: ''
};
// Interval handles
let gameTimerIv = null, turnTimerIv = null, replaceTimerIv = null;
let replaceSecsLeft = 0;
// ── DOM ────────────────────────────────────────────
const $gameLog = document.getElementById('gameLog');
const $judgeMove = document.getElementById('judgeMove');
const $oldGames = document.getElementById('oldGamesLog');
const $chalBadge = document.getElementById('challengeBadge');
const $turnTag = document.getElementById('turnTimerTag');
const $blockTimer = document.getElementById('blockTimerVal');
const $gameTimerV = document.getElementById('gameTimerVal');
const $gameTimerP = document.getElementById('gameTimerPip');
const $connDot = document.getElementById('connDot');
const $connLabel = document.getElementById('connLabel');
// ── Helpers ────────────────────────────────────────
function esc(s){
return String(s)
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
.replace(/"/g,'"').replace(/'/g,''');
}
function fmt(sec){
const h=Math.floor(sec/3600), m=Math.floor((sec%3600)/60), s=sec%60;
if(h>0) return `${h}:${pad(m)}:${pad(s)}`;
return `${m}:${pad(s)}`;
}
function pad(n){ return String(n).padStart(2,'0'); }
function toast(msg, dur=2400){
const t=document.getElementById('toast');
t.textContent=msg; t.classList.add('show');
setTimeout(()=>t.classList.remove('show'), dur);
}
window.openOverlay = id => document.getElementById(id).classList.add('open');
window.closeOverlay = id => document.getElementById(id).classList.remove('open');
window.countChars = (el, countId, max) => {
const n = el.value.length;
const c = document.getElementById(countId);
c.textContent = `${n} / ${max}`;
c.className = 'char-count' + (n>=max?' full': n>=max*0.85?' near':'');
};
function renderBox(el, html, emptyMsg){
el.innerHTML = (html && html.trim())
? html
: `<div class="empty-state">${emptyMsg}</div>`;
}
// ── Firebase listener ──────────────────────────────
onValue(gRef, snap => {
$connDot.classList.remove('dead'); $connDot.classList.add('live');
$connLabel.textContent = 'Live';
const d = snap.val() || {};
S = {
game_log: d.game_log || '',
judge_move: d.judge_move || '',
challenge_counter: d.challenge_counter || 0,
game_timer: d.game_timer || null,
turn_timer: d.turn_timer || null,
replace_timer: d.replace_timer || null,
old_games: d.old_games || ''
};
renderBox($gameLog, S.game_log, 'No game in progress. Start a new game to begin.');
renderBox($judgeMove, S.judge_move, 'No move submitted yet.');
renderBox($oldGames, S.old_games, 'No previous games yet.');
$chalBadge.textContent = S.challenge_counter;
$gameLog.scrollTop = $gameLog.scrollHeight;
$judgeMove.scrollTop = $judgeMove.scrollHeight;
// Old games: scroll to top so most recent is visible
$oldGames.scrollTop = 0;
syncGameTimer(S.game_timer);
syncTurnTimer(S.turn_timer);
}, err => {
$connDot.classList.remove('live'); $connDot.classList.add('dead');
$connLabel.textContent = 'Disconnected';
console.error(err);
});
// ── Game timer ─────────────────────────────────────
function syncGameTimer(start){
clearInterval(gameTimerIv);
if(!start){ $gameTimerP.style.display='none'; return; }
$gameTimerP.style.display='flex';
const total = 4*60*60;
function tick(){
const elapsed = Math.floor((Date.now()-start)/1000);
const left = Math.max(0, total-elapsed);
$gameTimerV.textContent = fmt(left);
if(left<=0){ clearInterval(gameTimerIv); fireGameTimeout(); }
}
tick(); gameTimerIv=setInterval(tick,1000);
}
function fireGameTimeout(){
if(S.game_log && S.game_log.includes('game has timed out')) return;
const msg = `<div class="log-entry log-timeout">⌛ The game has timed out. Please start a new game.</div>`;
update(gRef, { game_log: S.game_log + msg, game_timer: null });
}
// ── Turn timer ─────────────────────────────────────
function syncTurnTimer(start){
clearInterval(turnTimerIv);
if(!start){ $turnTag.textContent='—'; $blockTimer.textContent='—'; return; }
const total = 3*60;
function tick(){
const elapsed = Math.floor((Date.now()-start)/1000);
const left = Math.max(0, total-elapsed);
const d = fmt(left);
$turnTag.textContent = d;
$blockTimer.textContent = d;
if(left<=0){ clearInterval(turnTimerIv); $turnTag.textContent='—'; update(gRef,{turn_timer:null}); }
}
tick(); turnTimerIv=setInterval(tick,1000);
}
// ── NEW GAME ───────────────────────────────────────
window.submitNewGame = async () => {
const text = document.getElementById('scenarioTA').value.trim();
if(!text){ toast('Please write a scenario first.'); return; }
// Archive current game_log to old_games before starting a new game
let newOldGames = S.old_games;
if(S.game_log && S.game_log.trim()){
const timestamp = new Date().toLocaleString(undefined, {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
const divider = `<hr class="old-game-divider"><div class="log-entry log-tally">📅 Game ended ${timestamp}</div>`;
// Prepend so newest archived game appears at top
newOldGames = divider + S.game_log + newOldGames;
}
const scenarioHtml =
`<div class="log-entry log-scenario">📌 ${esc(text)}</div>`;
await set(gRef, {
game_log: scenarioHtml,
judge_move: '',
challenge_counter: 0,
game_timer: Date.now(),
turn_timer: null,
replace_timer: null,
old_games: newOldGames
});
document.getElementById('scenarioTA').value = '';
countChars(document.getElementById('scenarioTA'),'scenarioCount',500);
closeOverlay('newGameOverlay');
toast('New game started!');
};
// ── MAKE A MOVE ────────────────────────────────────
window.doMakeMove = () => {
if(S.turn_timer){
const elapsed = Math.floor((Date.now()-S.turn_timer)/1000);
if((3*60)-elapsed > 0){ openOverlay('timerBlockOverlay'); return; }
}
document.getElementById('moveTA').value='';
countChars(document.getElementById('moveTA'),'moveCount',200);
openOverlay('makeMoveOverlay');
};
window.submitMove = async () => {
const text = document.getElementById('moveTA').value.trim();
if(!text){ toast('Please write a move first.'); return; }
let newLog = S.game_log;
// 1. Copy current judge_move into game_log (preserving all HTML/formatting)
if(S.judge_move && S.judge_move.trim()){
newLog += S.judge_move;
// 2. Print challenge_counter tally below it
const plural = S.challenge_counter !== 1 ? 's' : '';
newLog += `<div class="log-entry log-tally">🗳 ${S.challenge_counter} Challenge${plural}</div>`;
}
// 3. New move goes into judge_move; reset counter; start turn timer
const moveHtml = `<div class="log-entry">${esc(text)}</div>`;
await update(gRef, {
game_log: newLog,
judge_move: moveHtml,
turn_timer: Date.now(),
challenge_counter: 0
});
document.getElementById('moveTA').value='';
countChars(document.getElementById('moveTA'),'moveCount',200);
closeOverlay('makeMoveOverlay');
toast('Move submitted!');
};
// ── SUMMARIZE ──────────────────────────────────────
window.doSummarize = () => {
document.getElementById('summaryTA').value='';
countChars(document.getElementById('summaryTA'),'summaryCount',50);
openOverlay('summarizeOverlay');
};
window.submitSummary = async () => {
const text = document.getElementById('summaryTA').value.trim();
if(!text){ toast('Please write a summary first.'); return; }
const html = `<div class="log-entry text-blue">📝 ${esc(text)}</div>`;
await update(gRef, { judge_move: S.judge_move + html });
document.getElementById('summaryTA').value='';
countChars(document.getElementById('summaryTA'),'summaryCount',50);
closeOverlay('summarizeOverlay');
toast('Summary added!');
};
// ── COMMENT ────────────────────────────────────────
window.doComment = () => {
document.getElementById('commentTA').value='';
countChars(document.getElementById('commentTA'),'commentCount',100);
openOverlay('commentOverlay');
};
window.submitComment = async () => {
const text = document.getElementById('commentTA').value.trim();
if(!text){ toast('Please write a comment first.'); return; }
const html = `<div class="log-entry text-green">💬 ${esc(text)}</div>`;
await update(gRef, { judge_move: S.judge_move + html });
document.getElementById('commentTA').value='';
countChars(document.getElementById('commentTA'),'commentCount',100);
closeOverlay('commentOverlay');
toast('Comment added!');
};
// ── CHALLENGE ──────────────────────────────────────
window.doChallenge = async () => {
if(!S.judge_move || !S.judge_move.trim()){ toast('No move to challenge yet.'); return; }
const newCount = S.challenge_counter + 1;
const roll = Math.floor(Math.random()*100)+1;
if(roll<=10){
// Success — strike through, add note, archive to game_log, clear judge_move
const struck = `<div style="text-decoration:line-through;opacity:0.55">${S.judge_move}</div>`;
const note = `<div class="log-entry text-challenged">⚔️ This move was successfully challenged. (roll: ${roll})</div>`;
const newLog = S.game_log + struck + note;
await update(gRef, {
challenge_counter: newCount,
judge_move: '',
game_log: newLog
});
document.getElementById('chalResultTitle').textContent = '⚔️ Challenge Succeeded!';
document.getElementById('chalResultBody').innerHTML =
`<span style="color:#6ac98a">Roll: ${roll} / 100 — needed 1–10.</span><br><br>The move was struck through and archived to the Game Log.`;
} else {
// Fail — just increment
await update(gRef, { challenge_counter: newCount });
document.getElementById('chalResultTitle').textContent = '🛡 Challenge Failed';
document.getElementById('chalResultBody').innerHTML =
`<span style="color:var(--text-dim)">Roll: ${roll} / 100 — needed 1–10.</span><br><br>The move stands.`;
}
openOverlay('challengeResultOverlay');
};
// ── REPLACE ────────────────────────────────────────
window.doReplace = () => {
if(!S.judge_move || !S.judge_move.trim()){ toast('No move to replace yet.'); return; }
if(S.challenge_counter > 0){ openOverlay('alreadyChallengedOverlay'); return; }
document.getElementById('replaceTA').value='';
document.getElementById('replaceTA').disabled=false;
countChars(document.getElementById('replaceTA'),'replaceCount',100);
openOverlay('replaceOverlay');
startReplaceTimer();
};
window.cancelReplace = () => {
clearInterval(replaceTimerIv);
document.getElementById('replaceTA').disabled=false;
closeOverlay('replaceOverlay');
};
function startReplaceTimer(){
clearInterval(replaceTimerIv);
replaceSecsLeft = 60;
document.getElementById('rbarFill').style.width='100%';
document.getElementById('rbarLabel').textContent='1:00 remaining';
replaceTimerIv = setInterval(()=>{
replaceSecsLeft--;
const pct = Math.max(0,(replaceSecsLeft/60)*100);
document.getElementById('rbarFill').style.width = pct+'%';
document.getElementById('rbarLabel').textContent = fmt(replaceSecsLeft)+' remaining';
if(replaceSecsLeft<=0){
clearInterval(replaceTimerIv);
const ta = document.getElementById('replaceTA');
ta.value="You've run out of time.";
ta.disabled=true;
countChars(ta,'replaceCount',100);
}
},1000);
}
window.submitReplacement = async () => {
const ta = document.getElementById('replaceTA');
const text = ta.value.trim();
if(!text || text==="You've run out of time."){ toast("Can't submit — no text or time expired."); return; }
clearInterval(replaceTimerIv);
ta.disabled=false;
const roll = Math.floor(Math.random()*2)+1;
if(roll===1){
// Success — strike old move, add note, archive; put replacement in judge_move
const struck = `<div style="text-decoration:line-through;opacity:0.55">${S.judge_move}</div>`;
const note = `<div class="log-entry text-replaced">🔄 This move was successfully replaced.</div>`;
const newLog = S.game_log + struck + note;
const newMove = `<div class="log-entry">${esc(text)}</div>`;
await update(gRef, { game_log: newLog, judge_move: newMove });
toast('Replacement succeeded!');
} else {
// Fail — strike proposed text, add note, append both to judge_move
const struckProp = `<div class="log-entry text-struck">${esc(text)}</div>`;
const failNote = `<div class="log-entry text-replace-fail">🚫 Someone tried unsuccessfully to replace this move.</div>`;
const newMove = S.judge_move + struckProp + failNote;
await update(gRef, { judge_move: newMove });
toast('Replacement failed — original stands.');
}
ta.value='';
countChars(ta,'replaceCount',100);
closeOverlay('replaceOverlay');
};
// ── Connection timeout fallback ────────────────────
setTimeout(()=>{
if($connLabel.textContent==='Connecting'){
$connDot.classList.add('dead');
$connLabel.textContent='Check connection';
}
},5000);
</script>
</body>
</html>