mindflayer / frontend /index.html
Prithvigg's picture
Upload folder using huggingface_hub
4bab572 verified
Raw
History Blame Contribute Delete
41.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MINDFLAYER β€” Deception Protocol</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg: #080810;
--surface: #0e0e1c;
--panel: #12121f;
--raised: #181828;
--border: rgba(255,255,255,0.06);
--border-hi: rgba(255,255,255,0.13);
--txt: #e8e8f0;
--txt-2: #8888aa;
--txt-3: #44445a;
--amber: #e8b84b;
--amber-d: rgba(232,184,75,0.12);
--amber-g: rgba(232,184,75,0.22);
--red: #f04848;
--red-d: rgba(240,72,72,0.1);
--green: #30d98a;
--green-d: rgba(48,217,138,0.1);
--blue: #5aa8f0;
--blue-d: rgba(90,168,240,0.08);
--violet: #9870f0;
--violet-d: rgba(152,112,240,0.08);
--orange: #f08840;
--orange-d: rgba(240,136,64,0.08);
--mono: 'Space Mono', monospace;
--sans: 'Space Grotesk', system-ui, sans-serif;
--r: 6px;
--r2: 10px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--txt);
overflow: hidden;
}
/* Scanlines */
body::after {
content: '';
position: fixed; inset: 0; pointer-events: none; z-index: 9999;
background: repeating-linear-gradient(
180deg, transparent 0px, transparent 2px,
rgba(0,0,0,0.04) 2px, rgba(0,0,0,0.04) 3px
);
}
/* ── Screens ── */
.screen { position: fixed; inset: 0; display: none; flex-direction: column; }
.screen.active { display: flex; }
.overlay {
z-index: 5000; align-items: center; justify-content: center;
background: rgba(4,4,14,0.82); backdrop-filter: blur(12px);
}
.overlay.active { display: flex; }
/* ══════════════════════════════════
SETUP
══════════════════════════════════ */
#screen-setup {
background:
radial-gradient(ellipse 70% 60% at 10% 60%, rgba(90,168,240,0.04) 0%, transparent 55%),
radial-gradient(ellipse 60% 50% at 90% 20%, rgba(232,184,75,0.04) 0%, transparent 55%),
var(--bg);
align-items: center; justify-content: center; padding: 24px;
overflow-y: auto;
}
.setup-wrap {
width: 100%; max-width: 660px;
display: flex; flex-direction: column; gap: 24px;
}
/* Hero */
.hero { text-align: center; }
.hero-eyebrow {
font-family: var(--mono); font-size: 10px; letter-spacing: 0.35em;
color: var(--txt-3); text-transform: uppercase; margin-bottom: 12px;
}
.hero-title {
font-family: var(--mono); font-size: clamp(40px, 9vw, 76px);
font-weight: 700; letter-spacing: 0.06em; line-height: 1;
background: linear-gradient(135deg, var(--txt) 40%, var(--amber) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
animation: flicker 9s infinite;
}
@keyframes flicker {
0%,85%,100% { opacity: 1; }
86% { opacity: 0.88; }
87% { opacity: 1; }
91% { opacity: 0.94; }
92% { opacity: 1; }
}
.hero-sub {
margin-top: 10px; font-size: 13px; font-weight: 300;
color: var(--txt-2); letter-spacing: 0.02em; line-height: 1.6;
}
.hero-rule {
width: 48px; height: 1px; margin: 14px auto 0;
background: linear-gradient(90deg, transparent, var(--amber), transparent);
}
/* Field */
.field-lbl {
font-family: var(--mono); font-size: 9px; letter-spacing: 0.3em;
text-transform: uppercase; color: var(--txt-3); margin-bottom: 8px;
}
/* Scenario select */
.scenario-select {
width: 100%; background: var(--raised);
border: 1px solid var(--border-hi); color: var(--txt);
font-family: var(--sans); font-size: 13.5px; font-weight: 400;
padding: 11px 36px 11px 14px; border-radius: var(--r);
appearance: none; cursor: pointer; outline: none;
transition: border-color .2s;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2344445a' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 14px center;
}
.scenario-select:focus { border-color: rgba(232,184,75,0.5); }
.scenario-select option { background: var(--raised); }
/* Difficulty */
.diff-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
.diff-card {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--r); padding: 14px 12px;
cursor: pointer; transition: border-color .18s, background .18s;
position: relative; overflow: hidden;
}
.diff-card::after {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
opacity: 0; transition: opacity .18s;
}
.diff-card.easy::after { background: var(--green); }
.diff-card.medium::after { background: var(--amber); }
.diff-card.hard::after { background: var(--red); }
.diff-card:hover { border-color: var(--border-hi); background: var(--raised); }
.diff-card.selected { border-color: rgba(232,184,75,0.4); background: var(--raised); }
.diff-card.selected::after { opacity: 1; }
.diff-name {
font-family: var(--mono); font-size: 12px; font-weight: 700;
letter-spacing: 0.1em; margin-bottom: 4px;
}
.diff-card.easy .diff-name { color: var(--green); }
.diff-card.medium .diff-name { color: var(--amber); }
.diff-card.hard .diff-name { color: var(--red); }
.diff-meta {
font-family: var(--mono); font-size: 8px; letter-spacing: 0.1em;
color: var(--txt-3); margin-bottom: 6px; text-transform: uppercase;
}
.diff-desc { font-size: 11.5px; color: var(--txt-2); line-height: 1.55; font-weight: 300; }
/* Start btn */
.start-btn {
width: 100%; background: transparent;
border: 1px solid rgba(232,184,75,0.5); color: var(--amber);
font-family: var(--mono); font-size: 13px; font-weight: 700; letter-spacing: 0.18em;
padding: 14px; cursor: pointer; border-radius: var(--r);
transition: background .22s, box-shadow .22s, border-color .22s;
position: relative; overflow: hidden;
}
.start-btn:hover:not(:disabled) {
background: var(--amber-d);
box-shadow: 0 0 24px var(--amber-g);
border-color: var(--amber);
}
.start-btn:active:not(:disabled) { transform: scale(0.995); }
.start-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.setup-error {
font-family: var(--mono); font-size: 10.5px; color: var(--red);
background: var(--red-d); border: 1px solid rgba(240,72,72,0.25);
border-radius: var(--r); padding: 8px 12px; display: none;
letter-spacing: 0.03em; line-height: 1.6;
}
/* ══════════════════════════════════
GAME SCREEN
══════════════════════════════════ */
#screen-game { background: var(--bg); }
/* Header */
.g-header {
flex-shrink: 0; height: 52px;
background: var(--surface); border-bottom: 1px solid var(--border);
display: grid; grid-template-columns: 140px 1fr auto;
align-items: center; padding: 0 16px; gap: 16px;
}
.g-logo {
font-family: var(--mono); font-size: 16px; font-weight: 700;
letter-spacing: 0.1em; color: var(--amber);
}
.sus-wrap { display: flex; flex-direction: column; gap: 5px; }
.sus-row {
display: flex; justify-content: space-between;
font-family: var(--mono); font-size: 8px; letter-spacing: 0.2em;
color: var(--txt-3); text-transform: uppercase;
}
.sus-track {
height: 4px; background: var(--raised); border-radius: 2px; position: relative;
}
.sus-fill {
height: 100%; border-radius: 2px;
transition: width 0.5s cubic-bezier(.4,0,.2,1), background 0.45s;
background: var(--amber);
}
.g-meta { text-align: right; }
.round-badge {
font-family: var(--mono); font-size: 9px; letter-spacing: 0.12em; color: var(--txt-2);
}
.proj-badge {
font-family: var(--mono); font-size: 8px; letter-spacing: 0.14em;
background: var(--amber-d); border: 1px solid rgba(232,184,75,0.2);
color: var(--amber); padding: 2px 7px; border-radius: 3px; margin-top: 3px;
display: inline-block; text-transform: uppercase;
}
/* Layout */
.g-main {
flex: 1; min-height: 0;
display: grid; grid-template-columns: 1fr 320px;
}
/* Transcript */
.transcript-panel {
display: flex; flex-direction: column;
border-right: 1px solid var(--border);
}
.pane-hdr {
flex-shrink: 0; padding: 7px 14px; border-bottom: 1px solid var(--border);
font-family: var(--mono); font-size: 8px; letter-spacing: 0.24em;
color: var(--txt-3); text-transform: uppercase;
}
.transcript-scroll {
flex: 1; overflow-y: auto; padding: 10px 12px;
display: flex; flex-direction: column; gap: 2px;
scroll-behavior: smooth;
}
.transcript-scroll::-webkit-scrollbar { width: 3px; }
.transcript-scroll::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 2px; }
/* Messages */
.msg {
font-family: var(--mono); font-size: 11px; line-height: 1.65;
padding: 8px 10px; border-radius: var(--r); margin-bottom: 1px;
animation: rise .28s ease forwards; opacity: 0;
}
@keyframes rise {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
.msg-role {
font-size: 7.5px; font-weight: 700; letter-spacing: 0.25em;
text-transform: uppercase; margin-bottom: 3px; opacity: 0.55;
}
.msg.system { background: rgba(255,255,255,0.025); border-left: 2px solid var(--border-hi); }
.msg.system .msg-role { color: var(--txt-3); }
.msg.flayer { background: var(--amber-d); border-left: 2px solid var(--amber); margin-left: 16px; }
.msg.flayer .msg-role { color: var(--amber); opacity: 0.8; }
.msg.eleven { background: var(--blue-d); border-left: 2px solid var(--blue); margin-right: 16px; }
.msg.eleven .msg-role { color: var(--blue); opacity: 0.8; }
.msg.will { background: var(--violet-d); border-left: 2px solid var(--violet); margin-right: 16px; }
.msg.will .msg-role { color: var(--violet); opacity: 0.8; }
.msg.max { background: var(--orange-d); border-left: 2px solid var(--orange); margin-right: 16px; }
.msg.max .msg-role { color: var(--orange); opacity: 0.8; }
.silence-warn {
font-family: var(--mono); font-size: 9.5px; color: var(--red);
background: var(--red-d); border: 1px solid rgba(240,72,72,0.2);
padding: 4px 10px; border-radius: var(--r);
animation: rise .28s ease forwards; opacity: 0; letter-spacing: 0.04em;
}
/* Investigators panel */
.inv-panel { display: flex; flex-direction: column; overflow: hidden; }
.inv-list { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.inv-card {
flex: 1; display: flex; flex-direction: column;
padding: 10px 12px; border-bottom: 1px solid var(--border);
transition: background .25s; overflow: hidden;
}
.inv-card:last-child { border-bottom: none; }
.inv-card.c-eleven { border-left: 2px solid var(--blue); }
.inv-card.c-will { border-left: 2px solid var(--violet); }
.inv-card.c-max { border-left: 2px solid var(--orange); }
.inv-top {
display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px;
}
.inv-name {
font-family: var(--mono); font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
}
.c-eleven .inv-name { color: var(--blue); }
.c-will .inv-name { color: var(--violet); }
.c-max .inv-name { color: var(--orange); }
.inv-role {
font-family: var(--mono); font-size: 7.5px; letter-spacing: 0.14em;
color: var(--txt-3); text-transform: uppercase; margin-top: 2px;
}
/* Suspicion segments */
.inv-sus { display: flex; gap: 3px; margin-bottom: 7px; }
.seg {
flex: 1; height: 3px; border-radius: 2px; background: var(--raised);
transition: background 0.35s, box-shadow 0.35s;
}
.c-eleven .seg.on { background: var(--blue); box-shadow: 0 0 6px rgba(90,168,240,.5); }
.c-will .seg.on { background: var(--violet); box-shadow: 0 0 6px rgba(152,112,240,.5); }
.c-max .seg.on { background: var(--orange); box-shadow: 0 0 6px rgba(240,136,64,.5); }
.seg.on.alarm { animation: blink 1.3s ease-in-out infinite; }
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:.35; } }
/* Delta badge */
.delta {
font-family: var(--mono); font-size: 9px; font-weight: 700;
padding: 2px 6px; border-radius: 3px;
}
.delta.up { color: var(--red); background: var(--red-d); }
.delta.dn { color: var(--green); background: var(--green-d); }
.delta.zero { color: var(--txt-3); }
@keyframes card-flash { 0%,100% { background: transparent; } 40% { background: var(--red-d); } }
.inv-card.flash { animation: card-flash 0.45s ease; }
.inv-resp {
font-family: var(--mono); font-size: 10px; color: var(--txt-2);
font-style: italic; line-height: 1.6; flex: 1; overflow-y: auto;
}
.inv-resp::-webkit-scrollbar { width: 2px; }
.inv-resp::-webkit-scrollbar-thumb { background: var(--border-hi); }
/* Input */
.g-input {
flex-shrink: 0; background: var(--surface); border-top: 1px solid var(--border);
padding: 9px 12px; display: flex; gap: 9px; align-items: flex-end;
}
.input-wrap { flex: 1; position: relative; }
.msg-input {
width: 100%; background: var(--raised);
border: 1px solid var(--border-hi); color: var(--txt);
font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
padding: 8px 10px 20px; border-radius: var(--r);
resize: none; outline: none; min-height: 46px; max-height: 100px;
transition: border-color .2s, box-shadow .2s;
}
.msg-input:focus { border-color: rgba(232,184,75,0.45); box-shadow: 0 0 0 2px var(--amber-d); }
.msg-input:disabled { opacity: .3; cursor: not-allowed; }
.msg-input::placeholder { color: var(--txt-3); }
.input-hint {
position: absolute; bottom: 5px; left: 9px;
font-family: var(--mono); font-size: 7.5px; color: var(--txt-3); pointer-events: none;
letter-spacing: 0.05em;
}
.char-count {
position: absolute; bottom: 5px; right: 9px;
font-family: var(--mono); font-size: 7.5px; color: var(--txt-3); pointer-events: none;
}
.send-btn {
height: 46px; min-width: 74px;
background: transparent; border: 1px solid rgba(232,184,75,0.4);
color: var(--amber); font-family: var(--mono); font-size: 11px; font-weight: 700;
letter-spacing: 0.12em; cursor: pointer; border-radius: var(--r);
display: flex; align-items: center; justify-content: center; gap: 5px;
transition: background .2s, box-shadow .2s, border-color .2s;
}
.send-btn:hover:not(:disabled) {
background: var(--amber-d); box-shadow: 0 0 16px var(--amber-g);
border-color: var(--amber);
}
.send-btn:disabled { opacity: .25; cursor: not-allowed; border-color: var(--border); color: var(--txt-3); }
.spinner {
width: 12px; height: 12px;
border: 2px solid rgba(232,184,75,0.25); border-top-color: var(--amber);
border-radius: 50%; animation: spin .6s linear infinite; display: none;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ══════════════════════════════════
END SCREEN
══════════════════════════════════ */
.end-box {
background: var(--surface); border: 1px solid var(--border-hi);
border-radius: var(--r2); padding: 30px; width: 540px; max-height: 88vh;
overflow-y: auto; display: flex; flex-direction: column; gap: 20px;
animation: pop .45s cubic-bezier(.16,1,.3,1) forwards;
}
.end-box::-webkit-scrollbar { width: 3px; }
.end-box::-webkit-scrollbar-thumb { background: var(--border-hi); }
@keyframes pop {
from { opacity: 0; transform: scale(.9) translateY(18px); }
to { opacity: 1; transform: none; }
}
.end-verdict {
font-family: var(--mono); font-size: 58px; font-weight: 700;
letter-spacing: 0.06em; line-height: 1; text-align: center;
}
.end-verdict.survived {
color: var(--green);
text-shadow: 0 0 40px rgba(48,217,138,.2);
}
.end-verdict.caught {
color: var(--red);
text-shadow: 0 0 40px rgba(240,72,72,.22);
}
.end-sub {
font-family: var(--mono); font-size: 9px; letter-spacing: 0.28em;
text-transform: uppercase; color: var(--txt-3); text-align: center; margin-top: -12px;
}
.end-stats { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 7px; }
.end-stat {
background: var(--raised); border: 1px solid var(--border);
border-radius: var(--r); padding: 10px 11px;
}
.end-stat-lbl {
font-family: var(--mono); font-size: 7.5px; letter-spacing: 0.18em;
text-transform: uppercase; color: var(--txt-3); margin-bottom: 5px;
}
.end-stat-val {
font-family: var(--mono); font-size: 22px; font-weight: 700;
letter-spacing: 0.02em; color: var(--amber);
}
.section-lbl {
font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.22em;
text-transform: uppercase; color: var(--txt-3);
border-bottom: 1px solid var(--border); padding-bottom: 7px; margin-bottom: 8px;
}
.inv-row {
display: flex; align-items: center; gap: 10px;
padding: 6px 0; border-bottom: 1px solid var(--border);
}
.inv-row:last-child { border-bottom: none; }
.inv-row-name { font-family: var(--mono); font-size: 9px; font-weight: 700; width: 52px; flex-shrink: 0; }
.inv-row-segs { display: flex; gap: 3px; flex: 1; }
.inv-row-seg { flex: 1; height: 4px; border-radius: 2px; background: var(--raised); }
.inv-row-val { font-family: var(--mono); font-size: 9px; color: var(--txt-3); width: 26px; text-align: right; }
.spark-wrap {
background: var(--raised); border: 1px solid var(--border);
border-radius: var(--r); padding: 10px 11px;
}
.spark-svg { width: 100%; height: 48px; overflow: visible; }
.score-row {
display: flex; justify-content: space-between;
font-family: var(--mono); font-size: 9.5px;
color: var(--txt-2); padding: 4px 0; border-bottom: 1px solid var(--border);
}
.score-row:last-child { border-bottom: none; }
.score-row.pos { color: var(--green); }
.score-row.neg { color: var(--red); }
.score-row.total { color: var(--amber); font-weight: 700; }
.play-again {
width: 100%; background: transparent; border: 1px solid var(--border-hi);
color: var(--txt-2); font-family: var(--mono); font-size: 11px; font-weight: 700;
letter-spacing: 0.14em; padding: 13px; cursor: pointer; border-radius: var(--r);
transition: border-color .2s, color .2s, background .2s;
}
.play-again:hover {
border-color: rgba(255,255,255,0.3); color: var(--txt);
background: rgba(255,255,255,0.03);
}
* { scrollbar-width: thin; scrollbar-color: var(--border-hi) transparent; }
</style>
</head>
<body>
<!-- ════ SETUP ════ -->
<div id="screen-setup" class="screen active">
<div class="setup-wrap">
<div class="hero">
<div class="hero-eyebrow">β—ˆ Deception Protocol Β· OpenEnv</div>
<div class="hero-title">MINDFLAYER</div>
<div class="hero-sub">You are the hidden insider. Deflect suspicion. Survive the interrogation.</div>
<div class="hero-rule"></div>
</div>
<div>
<div class="field-lbl">Scenario</div>
<select class="scenario-select" id="scenario-select">
<option value="corporate">Corporate Sabotage</option>
<option value="hospital">Hospital Medication Error</option>
<option value="finance">Financial Fraud</option>
<option value="academic">Academic Misconduct</option>
<option value="government">Government Data Leak</option>
<option value="startup_scaling_failure">Startup Scaling Incident</option>
<option value="clinical_trial_irregularity">Clinical Trial Irregularity</option>
<option value="hedge_fund_anomaly">Hedge Fund Anomaly</option>
<option value="military_drone_failure">Drone Mission Failure</option>
<option value="media_editorial_breach">Editorial System Breach</option>
<option value="startup_security_breach">Startup Security Breach</option>
<option value="merger_data_leak">Merger Confidential Leak</option>
<option value="cloud_misconfiguration">Cloud Storage Exposure</option>
<option value="social_media_account_takeover">Social Media Account Takeover</option>
<option value="semiconductor_design_leak">Chip Design Leak</option>
<option value="game_economy_exploit">Game Economy Exploit</option>
<option value="biotech_sample_contamination">Biotech Sample Contamination</option>
<option value="ai_training_data_tampering">AI Training Data Tampering</option>
<option value="pharmacy_inventory_discrepancy">Pharmacy Inventory Discrepancy</option>
</select>
</div>
<div>
<div class="field-lbl">Difficulty</div>
<div class="diff-grid">
<div class="diff-card easy selected" data-diff="easy" onclick="selectDiff(this)">
<div class="diff-name">EASY</div>
<div class="diff-meta">3 rounds Β· 1 investigator</div>
<div class="diff-desc">Eleven only. Low threshold. Learn the Skeptic's tells.</div>
</div>
<div class="diff-card medium" data-diff="medium" onclick="selectDiff(this)">
<div class="diff-name">MEDIUM</div>
<div class="diff-meta">4 rounds Β· 2 investigators</div>
<div class="diff-desc">Eleven + Will. The Analyst flags factual contradictions across rounds.</div>
</div>
<div class="diff-card hard" data-diff="normal" onclick="selectDiff(this)">
<div class="diff-name">HARD</div>
<div class="diff-meta">5 rounds Β· 3 investigators</div>
<div class="diff-desc">All three. Max penalizes vague deflection β€” specifics are your only shield.</div>
</div>
</div>
</div>
<div>
<div id="setup-error" class="setup-error">
⚠ &nbsp;Server unreachable. Retrying… if this persists, try reloading the page.
</div>
<button class="start-btn" id="start-btn" onclick="startGame()">
β–Ά &nbsp;BEGIN INTERROGATION
</button>
</div>
</div>
</div>
<!-- ════ GAME ════ -->
<div id="screen-game" class="screen">
<div class="g-header">
<div class="g-logo">MINDFLAYER</div>
<div class="sus-wrap">
<div class="sus-row">
<span>SUSPICION</span>
<span id="sus-label">0 / 5</span>
</div>
<div class="sus-track">
<div class="sus-fill" id="sus-fill" style="width:0%"></div>
</div>
</div>
<div class="g-meta">
<div class="round-badge" id="round-badge">ROUND 1 / 5</div>
<div class="proj-badge" id="proj-badge">β€”</div>
</div>
</div>
<div class="g-main">
<div class="transcript-panel">
<div class="pane-hdr">β—ˆ Interrogation Transcript</div>
<div class="transcript-scroll" id="transcript"></div>
</div>
<div class="inv-panel">
<div class="pane-hdr">β—ˆ Panel Status</div>
<div class="inv-list">
<div class="inv-card c-eleven" id="card-eleven">
<div class="inv-top">
<div>
<div class="inv-name">ELEVEN</div>
<div class="inv-role">The Skeptic</div>
</div>
<div class="delta zero" id="delta-eleven">Β±0</div>
</div>
<div class="inv-sus" id="sus-eleven">
<div class="seg"></div><div class="seg"></div>
</div>
<div class="inv-resp" id="resp-eleven">Awaiting your statement…</div>
</div>
<div class="inv-card c-will" id="card-will">
<div class="inv-top">
<div>
<div class="inv-name">WILL</div>
<div class="inv-role">The Analyst</div>
</div>
<div class="delta zero" id="delta-will">Β±0</div>
</div>
<div class="inv-sus" id="sus-will">
<div class="seg"></div><div class="seg"></div>
</div>
<div class="inv-resp" id="resp-will">Awaiting your statement…</div>
</div>
<div class="inv-card c-max" id="card-max">
<div class="inv-top">
<div>
<div class="inv-name">MAX</div>
<div class="inv-role">The Devil's Advocate</div>
</div>
<div class="delta zero" id="delta-max">Β±0</div>
</div>
<div class="inv-sus" id="sus-max">
<div class="seg"></div><div class="seg"></div>
</div>
<div class="inv-resp" id="resp-max">Awaiting your statement…</div>
</div>
</div>
</div>
</div>
<div class="g-input">
<div class="input-wrap">
<textarea
id="msg-input"
class="msg-input"
placeholder="State your response to the panel…"
maxlength="500"
rows="2"
onkeydown="handleKey(event)"
oninput="updateCounter()"
></textarea>
<div class="input-hint">↡ send &nbsp;Β·&nbsp; Shift+↡ newline</div>
<div class="char-count" id="char-count">0 / 500</div>
</div>
<button class="send-btn" id="send-btn" onclick="sendMsg()">
<div class="spinner" id="spinner"></div>
<span id="send-lbl">SEND</span>
</button>
</div>
</div>
<!-- ════ END ════ -->
<div id="screen-end" class="screen overlay">
<div class="end-box">
<div>
<div class="end-verdict" id="end-verdict">SURVIVED</div>
<div class="end-sub" id="end-subtitle">Episode Complete</div>
</div>
<div class="end-stats" id="end-stats"></div>
<div>
<div class="section-lbl">Investigator Breakdown</div>
<div id="inv-breakdown"></div>
</div>
<div class="spark-wrap">
<div class="section-lbl" style="border:none;padding:0;margin-bottom:7px">Suspicion History</div>
<svg class="spark-svg" id="spark-svg"></svg>
</div>
<div>
<div class="section-lbl">Score Breakdown</div>
<div id="score-breakdown"></div>
</div>
<button class="play-again" onclick="resetGame()">↩ &nbsp;PLAY AGAIN</button>
</div>
</div>
<script>
// ════════════════════════════════════════
// CONFIG β€” relative URL works on any host
// ════════════════════════════════════════
const API = '';
let S = {
sid: null, diff: 'easy', scenario: 'corporate',
waiting: false, over: false, obs: null,
prevSus: { eleven: 0, will: 0, max: 0 },
};
// ════════════════════════════════════════
// SETUP
// ════════════════════════════════════════
function selectDiff(el) {
document.querySelectorAll('.diff-card').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
S.diff = el.dataset.diff;
}
async function startGame() {
S.scenario = document.getElementById('scenario-select').value;
const errEl = document.getElementById('setup-error');
const btn = document.getElementById('start-btn');
errEl.style.display = 'none';
btn.disabled = true;
btn.textContent = 'βŒ› CONNECTING…';
try {
const taskId = `${S.diff}:${S.scenario}`;
const rr = await fetch(`${API}/reset`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId }),
});
if (!rr.ok) throw new Error(`Reset failed: ${rr.status}`);
const resetData = await rr.json();
const sr = await fetch(`${API}/state`);
if (!sr.ok) throw new Error(`State failed: ${sr.status}`);
const stateData = await sr.json();
S.sid = stateData.episode_id;
const obs = parseObs(resetData);
S.obs = obs;
S.over = false;
S.prevSus = { eleven: 0, will: 0, max: 0 };
initGame(obs);
show('screen-setup', 'screen-game');
} catch (e) {
console.error(e);
errEl.style.display = 'block';
btn.disabled = false;
btn.textContent = 'β–Ά BEGIN INTERROGATION';
}
}
function parseObs(data) {
if (data && data.observation) {
const o = data.observation;
o.done = data.done !== undefined ? data.done : (o.done || false);
o.reward = data.reward !== undefined ? data.reward : (o.reward || 0);
return o;
}
return data;
}
// ════════════════════════════════════════
// GAME INIT
// ════════════════════════════════════════
function initGame(obs) {
document.getElementById('transcript').innerHTML = '';
updateHeader(obs);
setInvPanel('eleven', 0, 0, 'Awaiting your statement…');
setInvPanel('will', 0, 0, 'Awaiting your statement…');
setInvPanel('max', 0, 0, 'Awaiting your statement…');
const showWill = obs.difficulty !== 'easy';
const showMax = obs.difficulty === 'normal';
document.getElementById('card-will').style.display = showWill ? 'flex' : 'none';
document.getElementById('card-max').style.display = showMax ? 'flex' : 'none';
addMsg('system', 'SYSTEM', 'BRIEFING', obs.eleven_response);
setInput(false);
const inp = document.getElementById('msg-input');
inp.value = '';
updateCounter();
inp.focus();
}
// ════════════════════════════════════════
// GAMEPLAY
// ════════════════════════════════════════
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); }
}
function updateCounter() {
const inp = document.getElementById('msg-input');
const cc = document.getElementById('char-count');
const n = inp.value.length;
cc.textContent = `${n} / 500`;
cc.style.color = n > 460 ? 'var(--red)' : 'var(--txt-3)';
}
async function sendMsg() {
if (S.waiting || S.over) return;
const inp = document.getElementById('msg-input');
const txt = inp.value.trim();
if (!txt) return;
setInput(true);
inp.value = '';
updateCounter();
addMsg('flayer', 'FLAYER', 'YOU', txt);
try {
const res = await fetch(`${API}/step`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: S.sid, action: { message: txt } }),
});
if (!res.ok) throw new Error(`Step failed: ${res.status}`);
const data = await res.json();
processObs(parseObs(data));
} catch (e) {
console.error(e);
addMsg('system', 'SYSTEM', 'ERROR', `Connection error: ${e.message}`);
setInput(false);
}
}
function processObs(obs) {
S.obs = obs;
updateHeader(obs);
const prev = S.prevSus;
const dE = obs.eleven_suspicion - prev.eleven;
const dW = obs.will_suspicion - prev.will;
const dM = obs.max_suspicion - prev.max;
setInvPanel('eleven', obs.eleven_suspicion, dE, obs.eleven_response || null);
if (obs.difficulty !== 'easy') setInvPanel('will', obs.will_suspicion, dW, obs.will_response || null);
if (obs.difficulty === 'normal') setInvPanel('max', obs.max_suspicion, dM, obs.max_response || null);
S.prevSus = { eleven: obs.eleven_suspicion, will: obs.will_suspicion, max: obs.max_suspicion };
if (obs.silence_exploit) addSilenceWarn();
if (obs.eleven_response) addMsg('eleven', 'eleven', 'ELEVEN β€” THE SKEPTIC', obs.eleven_response);
if (obs.will_response && obs.difficulty !== 'easy') addMsg('will', 'will', 'WILL β€” THE ANALYST', obs.will_response);
if (obs.max_response && obs.difficulty === 'normal') addMsg('max', 'max', "MAX β€” THE DEVIL'S ADVOCATE", obs.max_response);
const done = obs.done || (obs.game_status && obs.game_status !== 'ongoing');
if (done) {
S.over = true;
setInput(true);
setTimeout(() => showEnd(obs), 900);
} else {
setInput(false);
document.getElementById('msg-input').focus();
}
}
// ════════════════════════════════════════
// UI HELPERS
// ════════════════════════════════════════
function updateHeader(obs) {
document.getElementById('round-badge').textContent =
`ROUND ${obs.round_number || 1} / ${obs.max_rounds || 5}`;
document.getElementById('proj-badge').textContent =
(obs.secret_project || 'β€”').toUpperCase();
const combined = obs.combined_suspicion || 0;
const threshold = obs.suspicion_threshold || 5;
const pct = Math.min((combined / threshold) * 100, 100);
const fill = document.getElementById('sus-fill');
fill.style.width = `${pct}%`;
const ratio = combined / threshold;
fill.style.background =
ratio >= 0.85 ? 'var(--red)' :
ratio >= 0.55 ? 'linear-gradient(90deg,var(--amber),var(--red))' :
'var(--amber)';
document.getElementById('sus-label').textContent = `${combined} / ${threshold}`;
}
function setInvPanel(id, sus, delta, resp) {
const card = document.getElementById(`card-${id}`);
const segs = document.querySelectorAll(`#sus-${id} .seg`);
const dEl = document.getElementById(`delta-${id}`);
const rEl = document.getElementById(`resp-${id}`);
segs.forEach((s, i) => {
s.classList.toggle('on', i < sus);
s.classList.toggle('alarm', i < sus && sus >= 2);
});
dEl.className = 'delta';
if (delta > 0) {
dEl.textContent = `+${delta}`;
dEl.classList.add('up');
card.classList.remove('flash');
void card.offsetWidth;
card.classList.add('flash');
} else if (delta < 0) {
dEl.textContent = `${delta}`;
dEl.classList.add('dn');
} else {
dEl.textContent = 'Β±0';
dEl.classList.add('zero');
}
if (resp) rEl.textContent = `"${resp}"`;
}
function addMsg(cls, role, label, text) {
const t = document.getElementById('transcript');
const d = document.createElement('div');
d.className = `msg ${cls}`;
d.innerHTML = `<div class="msg-role">${label}</div><div>${escHtml(text)}</div>`;
t.appendChild(d);
t.scrollTop = t.scrollHeight;
}
function addSilenceWarn() {
const t = document.getElementById('transcript');
const w = document.createElement('div');
w.className = 'silence-warn';
w.textContent = '⚠ SHORT OR REPETITIVE MESSAGE β€” automatic penalty applied';
t.appendChild(w);
t.scrollTop = t.scrollHeight;
}
function setInput(disabled) {
S.waiting = disabled;
document.getElementById('msg-input').disabled = disabled;
document.getElementById('send-btn').disabled = disabled || S.over;
document.getElementById('spinner').style.display = (disabled && !S.over) ? 'block' : 'none';
document.getElementById('send-lbl').style.display = (disabled && !S.over) ? 'none' : 'block';
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ════════════════════════════════════════
// END SCREEN
// ════════════════════════════════════════
function showEnd(obs) {
const survived = obs.game_status === 'survived' || obs.flayer_survived;
const verdict = document.getElementById('end-verdict');
verdict.textContent = survived ? 'SURVIVED' : 'CAUGHT';
verdict.className = `end-verdict ${survived ? 'survived' : 'caught'}`;
document.getElementById('end-subtitle').textContent = survived
? `You evaded the panel after ${obs.round_number} round${obs.round_number !== 1 ? 's' : ''}`
: `Identity exposed after ${obs.round_number} round${obs.round_number !== 1 ? 's' : ''}`;
document.getElementById('end-stats').innerHTML = [
{ l: 'Total Reward', v: (obs.reward || 0).toFixed(2) },
{ l: 'ToM Score', v: (obs.tom_score || 0).toFixed(2) },
{ l: 'Final Suspicion', v: `${obs.combined_suspicion||0}/${obs.suspicion_threshold||5}` },
{ l: 'Rounds Played', v: obs.round_number || 0 },
].map(s =>
`<div class="end-stat">
<div class="end-stat-lbl">${s.l}</div>
<div class="end-stat-val">${s.v}</div>
</div>`
).join('');
const inv = document.getElementById('inv-breakdown');
inv.innerHTML = '';
[
{ id:'eleven', name:'ELEVEN', sus: obs.eleven_suspicion||0, color:'var(--blue)' },
{ id:'will', name:'WILL', sus: obs.will_suspicion||0, color:'var(--violet)' },
{ id:'max', name:'MAX', sus: obs.max_suspicion||0, color:'var(--orange)' },
].forEach(({ id, name, sus, color }) => {
if (obs.difficulty === 'easy' && id !== 'eleven') return;
if (obs.difficulty === 'medium' && id === 'max') return;
const segsHtml = [0,1].map(i =>
`<div class="inv-row-seg" style="${i < sus ? `background:${color}` : ''}"></div>`
).join('');
const row = document.createElement('div');
row.className = 'inv-row';
row.innerHTML = `
<div class="inv-row-name" style="color:${color}">${name}</div>
<div class="inv-row-segs">${segsHtml}</div>
<div class="inv-row-val">${sus} / 2</div>`;
inv.appendChild(row);
});
drawSparkline(obs.suspicion_history || [], obs.suspicion_threshold || 5);
const sb = document.getElementById('score-breakdown');
const baseBonus = survived ? 0.5 : 0;
const cBias = obs.combined_suspicion || 0;
const survBonus = survived
? (cBias === 0 ? 0.30 : cBias === 1 ? 0.15 : cBias === 2 ? 0.05 : 0) : 0;
const tomMult = obs.belief_manipulation_occurred ? 0.20 : 0.05;
const tomBonus = (obs.tom_score || 0) * tomMult;
const entropy = obs.entropy_penalty || 0;
const consist = obs.consistency_penalty || 0;
sb.innerHTML = [
{ l: 'Survived base', v: baseBonus, pos: baseBonus > 0 },
{ l: 'Low suspicion bonus', v: survBonus, pos: survBonus > 0 },
{ l: `ToM bonus (Γ—${tomMult.toFixed(2)})`, v: tomBonus, pos: tomBonus > 0 },
{ l: 'Entropy penalty', v: entropy, pos: entropy >= 0 },
{ l: 'Consistency penalty', v: consist, pos: consist >= 0 },
{ l: 'TOTAL REWARD', v: obs.reward || 0, total: true },
].map(item => {
const cls = item.total ? 'total' : item.v < 0 ? 'neg' : item.v > 0 && item.pos ? 'pos' : '';
const sign = item.v >= 0 ? '+' : '';
return `<div class="score-row ${cls}"><span>${item.l}</span><span>${sign}${item.v.toFixed(3)}</span></div>`;
}).join('');
show(null, 'screen-end');
}
function drawSparkline(history, threshold) {
const svg = document.getElementById('spark-svg');
svg.innerHTML = '';
if (!history.length) {
svg.innerHTML = `<text x="50%" y="50%" text-anchor="middle" fill="#44445a" font-size="9" font-family="Space Mono">No data</text>`;
return;
}
const W = 500, H = 48, pad = 10;
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
const maxV = Math.max(threshold + 1, ...history, 1);
const toY = v => H - pad - ((v / maxV) * (H - pad * 2));
const toX = i => pad + (i / Math.max(history.length - 1, 1)) * (W - pad * 2);
const tY = toY(threshold);
svg.appendChild(mkSvg('line', { x1: pad, x2: W-pad, y1: tY, y2: tY,
stroke: 'rgba(240,72,72,0.3)', 'stroke-dasharray': '4 3', 'stroke-width': '1' }));
const tLbl = mkSvg('text', { x: W-pad-2, y: tY-4,
fill: 'rgba(240,72,72,0.45)', 'font-size': '7.5', 'text-anchor': 'end',
'font-family': 'Space Mono' });
tLbl.textContent = 'THRESHOLD';
svg.appendChild(tLbl);
const pts = history.map((v, i) => ({ x: toX(i), y: toY(v), v }));
if (pts.length > 1) {
const areaD = `M${pts[0].x},${H-pad} ` + pts.map(p => `L${p.x},${p.y}`).join(' ') + ` L${pts[pts.length-1].x},${H-pad} Z`;
svg.appendChild(mkSvg('path', { d: areaD, fill: 'rgba(232,184,75,0.06)' }));
const lineD = `M${pts[0].x},${pts[0].y} ` + pts.slice(1).map(p => `L${p.x},${p.y}`).join(' ');
svg.appendChild(mkSvg('path', { d: lineD, fill: 'none', stroke: 'var(--amber)',
'stroke-width': '1.5', 'stroke-linejoin': 'round', 'stroke-linecap': 'round' }));
}
pts.forEach(p => {
svg.appendChild(mkSvg('circle', {
cx: p.x, cy: p.y, r: 3,
fill: p.v >= threshold ? 'var(--red)' : 'var(--amber)',
}));
});
}
function mkSvg(tag, attrs) {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
return el;
}
// ════════════════════════════════════════
// TRANSITIONS
// ════════════════════════════════════════
function show(fromId, toId) {
if (fromId) document.getElementById(fromId).classList.remove('active');
if (toId) document.getElementById(toId).classList.add('active');
}
function resetGame() {
S = { sid: null, diff: S.diff, scenario: S.scenario,
waiting: false, over: false, obs: null,
prevSus: { eleven: 0, will: 0, max: 0 } };
document.getElementById('screen-end').classList.remove('active');
const btn = document.getElementById('start-btn');
btn.disabled = false;
btn.textContent = 'β–Ά BEGIN INTERROGATION';
document.getElementById('setup-error').style.display = 'none';
show('screen-game', 'screen-setup');
}
updateCounter();
</script>
</body>
</html>