Spaces:
Running
Running
| <html lang="en"> | |
| <head><script>window.huggingface={variables:{"SPACE_CREATOR_USER_ID":"698653c38d72821326e9e4fe"}};</script> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Nomos42 Pixel World β PokΓ©mon Trainer Network</title> | |
| <style> | |
| :root { | |
| /* v2.16 β unified PokΓ©mon B/W (Gen5 DS) palette. */ | |
| --bg:#0b0d12; --bg-2:#14171f; --bg-3:#1a1f2a; --line:#1e2330; | |
| --ink:#e6e8ee; --ink-2:#9aa3b5; --ink-3:#5b6478; | |
| /* Semantic colors β remapped to PokΓ©mon set so all chrome reads as one game. */ | |
| --ok:#207838; --warn:#d8a020; --err:#c03030; | |
| --pink:#c03068; --blue:#4060b0; --green:#207838; --orange:#a05820; --purple:#5040a0; | |
| /* PokΓ©mon box primitives (reused by every chrome surface). */ | |
| --pk-box:#f8f8f8; --pk-ink:#101820; --pk-navy:#4060b0; --pk-red:#c03030; | |
| --pk-yellow:#ffd840; --pk-yellow-2:#fff8c0; --pk-green:#207838; | |
| --pk-steel:#283040; --pk-mute:#808898; | |
| } | |
| * { box-sizing:border-box; } | |
| html,body { margin:0; padding:0; height:100%; background:var(--bg); color:var(--ink); | |
| font-family:"JetBrains Mono","Courier New",monospace; font-size:12px; overflow:hidden; } | |
| /* βββ v2.22 Wall Street marquee ticker β replaces segmented row. βββ | |
| User ask 2026-04-20: "comme Γ wall street avec all important data qui dΓ©filent". | |
| Full-width horizontal scroll; content duplicated for seamless loop. Pointer-events | |
| disabled on the track so clicks pass through to the world. */ | |
| #bloomberg-ticker { position:fixed; top:0; left:0; right:0; height:34px; z-index:100; | |
| background:#f8f8f8; color:#101820; | |
| font-family:"Press Start 2P","VT323","IBM Plex Mono",monospace; font-size:11px; | |
| letter-spacing:0.08em; text-transform:uppercase; | |
| border-bottom:3px solid var(--pk-ink); | |
| box-shadow: inset 0 0 0 2px #f8f8f8, inset 0 0 0 4px var(--pk-navy), | |
| 0 3px 10px rgba(0,0,0,0.55); | |
| display:flex; align-items:center; overflow:hidden; padding:0; } | |
| #bloomberg-ticker .brand { flex:0 0 auto; padding:0 14px; height:100%; | |
| background:var(--pk-navy); color:var(--pk-yellow); | |
| display:flex; align-items:center; gap:8px; | |
| border-right:3px solid var(--pk-ink); z-index:2; | |
| font-family:"Press Start 2P",monospace; font-size:10px; letter-spacing:0.18em; | |
| text-shadow:1px 1px 0 #000; white-space:nowrap; } | |
| #bloomberg-ticker .brand .dot { width:8px; height:8px; background:var(--pk-red); | |
| border:1px solid #000; animation:blk 0.9s steps(2) infinite; } | |
| #ticker-viewport { flex:1; height:100%; overflow:hidden; position:relative; | |
| pointer-events:none; } | |
| #ticker-track { position:absolute; top:0; left:0; height:100%; | |
| display:flex; align-items:center; gap:0; white-space:nowrap; | |
| animation: tk-scroll 62s linear infinite; | |
| will-change: transform; } | |
| #ticker-track:hover { animation-play-state: paused; } | |
| @keyframes tk-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } | |
| #bloomberg-ticker .seg { padding:0 18px; display:inline-flex; gap:7px; | |
| align-items:center; white-space:nowrap; border-right:2px dotted #b0b8c4; height:100%; } | |
| #bloomberg-ticker .lbl { color:var(--pk-navy); font-weight:700; letter-spacing:0.16em; | |
| font-family:"Press Start 2P",monospace; font-size:9px; } | |
| #bloomberg-ticker .val { color:var(--pk-ink); font-weight:700; font-family:"VT323","JetBrains Mono",monospace; font-size:14px; letter-spacing:0.02em; } | |
| #bloomberg-ticker .val.up { color:var(--pk-green); } | |
| #bloomberg-ticker .val.down { color:var(--pk-red); } | |
| #bloomberg-ticker .val.star { color:#a05820; background:var(--pk-yellow); padding:1px 6px; border:1px solid var(--pk-ink); border-radius:3px; } | |
| #bloomberg-ticker .val.flash { animation: tk-flash 0.6s ease-out; } | |
| @keyframes tk-flash { 0% { background:var(--pk-yellow); } 100% { background:transparent; } } | |
| #bloomberg-ticker .arrow { display:inline-block; color:var(--pk-navy); font-weight:900; } | |
| #bloomberg-ticker .arrow.up { color:var(--pk-green); } | |
| #bloomberg-ticker .arrow.down { color:var(--pk-red); } | |
| @keyframes blk { 50% { opacity:0; } } | |
| @keyframes panel-pulse { 0%,100% { box-shadow: 0 0 0 0 currentColor; } 50% { box-shadow: 0 0 0 3px currentColor; opacity:0.7; } } | |
| /* v2.8 2026-04-17: sidebar removed β all panels are now diegetic (in-world PIXI). | |
| Minimal floating HUDs live on top of #stage as absolute overlays. */ | |
| #root { display:grid; grid-template-columns:1fr; height:calc(100vh - 34px); margin-top:34px; } | |
| /* v2.16 β PokΓ©mon summary box (party-menu / PokΓ©dex entry). Off-white double-border + navy rules. */ | |
| /* v2.20 2026-04-20: user asked to remove the STATUS panel from the world β | |
| "not useful for pixel world on pixel world". Hidden via display:none so the | |
| in-world diegetic panels become the primary data surface. Re-enable by | |
| removing display:none if a future redesign needs the HUD back. */ | |
| #hud-kpi { display:none ; position:absolute; top:48px; left:14px; z-index:7; | |
| background:var(--pk-box); color:var(--pk-ink); | |
| border:3px solid var(--pk-ink); border-radius:8px; | |
| box-shadow: inset 0 0 0 2px var(--pk-box), inset 0 0 0 4px var(--pk-navy), | |
| 0 4px 14px rgba(0,0,0,0.55); | |
| padding:10px 14px; font-family:"JetBrains Mono",monospace; font-size:10.5px; | |
| display:flex; flex-direction:column; gap:3px; min-width:180px; } | |
| #hud-kpi::before { content:"β STATUS"; display:block; font-family:"Press Start 2P",monospace; | |
| font-size:8px; color:var(--pk-navy); letter-spacing:0.18em; margin-bottom:4px; | |
| padding-bottom:4px; border-bottom:2px dotted #b0b8c4; } | |
| #hud-kpi .r { display:flex; justify-content:space-between; gap:12px; padding:1px 0; } | |
| #hud-kpi .r .l { color:var(--pk-navy); text-transform:uppercase; letter-spacing:0.14em; font-size:9px; font-weight:700; font-family:"Press Start 2P",monospace; } | |
| #hud-kpi .r .v { color:var(--pk-ink); font-weight:700; font-family:"VT323","JetBrains Mono",monospace; font-size:13px; } | |
| #hud-kpi .r .v.warn { color:#a05820; } | |
| /* βββ v2.23 BLOOMBERG-DARK INSPECT CARD (unified with bubble + hover) βββ | |
| User 2026-04-20: "some yellow, some blanc" β unify to ONE palette. | |
| Fixed top-right on desktop, bottom-sheet on narrow viewports. No PIXI anchor. */ | |
| #inspect-card { position:fixed; display:none; z-index:12; | |
| top:70px; right:16px; width:320px; max-height:calc(100vh - 110px); overflow-y:auto; | |
| background:#0a1520; color:#e6e8ee; | |
| border:2px solid #5eead4; | |
| border-radius:6px; | |
| padding:0; | |
| font-size:11px; | |
| box-shadow: 0 10px 28px rgba(0,0,0,0.75), inset 0 0 0 1px rgba(94,234,212,0.15); | |
| font-family:"JetBrains Mono", monospace; } | |
| @media (max-width: 899px) { | |
| #inspect-card { top:auto; right:12px; left:12px; bottom:12px; width:auto; | |
| max-height:45vh; border-radius:8px 8px 0 0; } | |
| } | |
| #inspect-card .h { display:flex; justify-content:space-between; align-items:center; | |
| background:#0e1c2a; | |
| color:#5eead4; padding:8px 12px; | |
| font-size:10.5px; letter-spacing:0.14em; text-transform:uppercase; font-weight:700; | |
| border-bottom:1px solid #1e3040; | |
| font-family:"JetBrains Mono", monospace; } | |
| #inspect-card .h .t { color:#5eead4; } | |
| #inspect-card .h .x { color:#9aa3b5; cursor:pointer; font-weight:700; padding:0 6px; | |
| font-size:14px; line-height:1; } | |
| #inspect-card .h .x:hover { color:#ff5c5c; } | |
| #inspect-card .r, #inspect-card #inspect-extra { | |
| background:#0a1520; | |
| padding:5px 12px; margin:0; | |
| color:#e6e8ee; font-size:11px; | |
| font-family:"JetBrains Mono", monospace; } | |
| #inspect-card .r { display:flex; justify-content:space-between; gap:12px; | |
| border-bottom:1px solid rgba(94,234,212,0.08); } | |
| #inspect-card .r:last-of-type { border-bottom:none; } | |
| #inspect-card .r .l { color:#9aa3b5; text-transform:uppercase; letter-spacing:0.14em; | |
| font-size:9.5px; font-weight:700; padding-top:1px; } | |
| #inspect-card .r .v { color:#e6e8ee; font-weight:700; text-align:right; } | |
| #inspect-card .r .v.ok { color:#34d399; } | |
| #inspect-card .r .v.warn { color:#fbbf24; } | |
| #inspect-card #inspect-extra { color:#9aa3b5; font-size:10px; | |
| border-top:1px solid rgba(94,234,212,0.12); padding:7px 12px 10px 12px; line-height:1.5; } | |
| #stage { position:relative; background:var(--bg-2); overflow:hidden; } | |
| #stage canvas { image-rendering:pixelated; display:block; } | |
| #scanlines { position:absolute; inset:0; pointer-events:none; z-index:5; | |
| background:repeating-linear-gradient(0deg, rgba(0,0,0,0) 0, rgba(0,0,0,0) 2px, rgba(0,0,0,0.08) 3px, rgba(0,0,0,0) 4px); | |
| mix-blend-mode:multiply; } | |
| #vignette { position:absolute; inset:0; pointer-events:none; z-index:6; | |
| background:radial-gradient(ellipse at center, rgba(0,0,0,0) 55%, rgba(0,0,0,0.45) 100%); } | |
| #hud-top { position:absolute; top:12px; left:12px; right:12px; display:flex; align-items:center; justify-content:space-between; gap:12px; | |
| font-size:11px; color:var(--pk-ink); letter-spacing:0.1em; text-transform:uppercase; z-index:7; } | |
| #hud-top .brand { color:var(--pk-box); font-weight:700; pointer-events:none; | |
| font-family:"Press Start 2P",monospace; font-size:11px; letter-spacing:0.16em; | |
| text-shadow: 2px 2px 0 var(--pk-ink), -1px -1px 0 var(--pk-ink), 1px -1px 0 var(--pk-ink), -1px 1px 0 var(--pk-ink); } | |
| #hud-top .tick { color:var(--pk-green); font-family:"VT323",monospace; font-size:13px; | |
| text-shadow: 1px 1px 0 var(--pk-ink); } | |
| /* v2.22: #hud-top shrinks (marquee replaces the old Bloomberg strip). */ | |
| #hud-top { top: 6px ; font-size: 9px; } | |
| #hud-top .brand { font-size: 9px; } | |
| /* v2.18 β room-tabs + ext-btns removed (single world view). */ | |
| #hud-bot { position:absolute; bottom:120px; left:12px; right:12px; display:flex; justify-content:space-between; | |
| font-size:10px; color:var(--pk-box); pointer-events:none; z-index:7; | |
| font-family:"Press Start 2P",monospace; letter-spacing:0.14em; | |
| text-shadow: 1px 1px 0 var(--pk-ink); } | |
| /* v2.23 β unified Bloomberg-dark tooltip (same palette as inspect-card + bubble). */ | |
| #hover { position:absolute; max-width:300px; padding:10px 14px; | |
| background:#0a1520; color:#e6e8ee; | |
| border:2px solid #5eead4; border-radius:6px; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(94,234,212,0.15); | |
| z-index:8; pointer-events:none; display:none; | |
| font-family:"JetBrains Mono",monospace; font-size:11px; } | |
| #hover .h-head { display:flex; align-items:center; gap:8px; margin-bottom:6px; padding-bottom:6px; | |
| border-bottom:1px solid rgba(94,234,212,0.25); } | |
| #hover .h-dot { width:9px; height:9px; border:1px solid #5eead4; border-radius:50%; } | |
| #hover .h-title { font-weight:700; color:#5eead4; letter-spacing:0.06em; | |
| font-family:"JetBrains Mono",monospace; font-size:11px; text-transform:uppercase; } | |
| #hover .h-sub { color:#9aa3b5; font-size:9px; margin-left:auto; text-transform:uppercase; | |
| letter-spacing:0.14em; font-family:"JetBrains Mono",monospace; font-weight:700; } | |
| #hover .h-row { display:flex; justify-content:space-between; gap:12px; padding:2px 0; font-size:11px; } | |
| #hover .h-row .l { color:#9aa3b5; font-weight:700; font-family:"JetBrains Mono",monospace; | |
| font-size:9.5px; letter-spacing:0.12em; padding-top:2px; text-transform:uppercase; } | |
| #hover .h-row .v { color:#e6e8ee; font-weight:700; font-family:"JetBrains Mono",monospace; font-size:11px; } | |
| #hover .h-row .v.up { color:#34d399; } | |
| #hover .h-row .v.down { color:#ff5c5c; } | |
| #hover .h-tag { display:inline-block; padding:2px 7px; margin:3px 3px 0 0; | |
| font-size:9px; font-family:"JetBrains Mono",monospace; letter-spacing:0.1em; | |
| background:rgba(94,234,212,0.1); color:#5eead4; border:1px solid rgba(94,234,212,0.4); | |
| border-radius:3px; text-transform:uppercase; } | |
| #hover .h-tag.sac { color:#ff5c5c; background:rgba(255,92,92,0.1); border-color:rgba(255,92,92,0.4); } | |
| #hover .h-tag.pact { color:#34d399; background:rgba(52,211,153,0.1); border-color:rgba(52,211,153,0.4); } | |
| #hover .h-note { margin-top:6px; padding-top:6px; border-top:1px solid rgba(94,234,212,0.15); | |
| color:#b8c0d0; font-size:10px; line-height:1.5; font-family:"JetBrains Mono",monospace; } | |
| /* v2.16 β battle toast: PokΓ©mon DS notification card (red ribbon + white face). */ | |
| #event-toast { position:absolute; bottom:120px; right:14px; z-index:9; | |
| display:flex; flex-direction:column; gap:6px; max-width:290px; } | |
| #event-toast .t { padding:7px 11px 7px 14px; background:var(--pk-box); color:var(--pk-ink); | |
| border:3px solid var(--pk-ink); border-radius:6px; | |
| box-shadow: inset 0 0 0 2px var(--pk-box), inset 0 0 0 4px var(--pk-navy), | |
| 0 4px 10px rgba(0,0,0,0.5); | |
| font-family:"JetBrains Mono",monospace; font-size:10.5px; | |
| animation:toast-in 0.3s ease-out, toast-out 0.4s ease-in 4.5s forwards; | |
| position:relative; } | |
| #event-toast .t::before { content:""; position:absolute; left:-3px; top:-3px; bottom:-3px; width:6px; | |
| background:var(--pk-red); border:1px solid var(--pk-ink); border-right:none; | |
| border-radius:6px 0 0 6px; } | |
| #event-toast .t .k { font-weight:700; margin-right:6px; font-family:"Press Start 2P",monospace; font-size:8.5px; letter-spacing:0.12em; } | |
| #event-toast .t .k.nba { color:var(--pk-red); } | |
| #event-toast .t .k.island { color:var(--pk-green); } | |
| #event-toast .t .k.political { color:var(--pk-navy); } | |
| #event-toast .t .k.council { color:#a05820; } | |
| @keyframes toast-in { from { opacity:0; transform:translateX(20px); } to { opacity:1; transform:translateX(0); } } | |
| @keyframes toast-out { to { opacity:0; transform:translateX(20px); } } | |
| /* v2.8 2026-04-17: .panel .kpi .row .pill classes retained β used by paper modal only */ | |
| .kpi { display:flex; justify-content:space-between; font-size:11px; padding:2px 0; } | |
| .kpi .l { color:var(--ink-2); } | |
| .kpi .v-ok { color:var(--ok); } .kpi .v-warn { color:var(--warn); } .kpi .v-err { color:var(--err); } .kpi .v-muted { color:var(--ink-3); } | |
| .row { display:flex; justify-content:space-between; padding:3px 0; font-size:10.5px; border-bottom:1px dotted rgba(255,255,255,0.05); } | |
| .row:last-child { border-bottom:0; } | |
| .row .l { color:var(--ink-2); } | |
| .row .v { color:var(--ink); } | |
| .row .v.up { color:var(--ok); } .row .v.down { color:var(--err); } | |
| .loader { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:var(--ink-3); | |
| font-size:11px; letter-spacing:0.1em; text-transform:uppercase; z-index:4; } | |
| /* v2.18 β ext-btns (PAPERS/LIVE STATUS) removed. */ | |
| /* βββ v2.18 THREE-FLOOR LEADERBOARD βββ Fixed right-side panel. | |
| Three vertical sections (NBA / POL / PQTF) β every trader ranked by bankroll, | |
| persona visible, click a row β inspect card with model + provider + stats. */ | |
| #three-floor-lb { position:fixed; top:40px; right:8px; bottom:14px; width:278px; z-index:9; | |
| background:var(--pk-box); border:4px solid var(--pk-ink); border-radius:8px; | |
| box-shadow: inset 0 0 0 2px var(--pk-box), inset 0 0 0 4px var(--pk-navy), | |
| 0 6px 20px rgba(0,0,0,0.6); | |
| font-family:"JetBrains Mono",monospace; color:var(--pk-ink); | |
| display:flex; flex-direction:column; overflow:hidden; } | |
| #three-floor-lb .hdr { padding:7px 10px; background:var(--pk-navy); color:#fff; | |
| font-family:"Press Start 2P",monospace; font-size:9px; letter-spacing:0.15em; | |
| border-bottom:2px solid var(--pk-ink); display:flex; justify-content:space-between; align-items:center; } | |
| #three-floor-lb .hdr .ttl { color:var(--pk-yellow); text-shadow:1px 1px 0 #000; } | |
| #three-floor-lb .hdr .tm { color:#c0d8ff; font-size:8px; letter-spacing:0.1em; font-family:"JetBrains Mono",monospace; font-weight:700; } | |
| #three-floor-lb .body { flex:1; overflow-y:auto; padding:0; scrollbar-width:thin; | |
| scrollbar-color: var(--pk-navy) var(--pk-box); } | |
| #three-floor-lb .body::-webkit-scrollbar { width:8px; } | |
| #three-floor-lb .body::-webkit-scrollbar-track { background: var(--pk-box); } | |
| #three-floor-lb .body::-webkit-scrollbar-thumb { background: var(--pk-navy); border-radius:3px; } | |
| #three-floor-lb .sec { padding:6px 8px 10px; border-bottom:2px solid var(--pk-ink); } | |
| #three-floor-lb .sec:last-child { border-bottom:0; } | |
| #three-floor-lb .sec .sh { display:flex; justify-content:space-between; align-items:baseline; | |
| padding:4px 4px 6px; border-bottom:1px dotted rgba(16,24,32,0.4); margin-bottom:4px; } | |
| #three-floor-lb .sec .sh .kind { font-family:"Press Start 2P",monospace; font-size:9px; | |
| letter-spacing:0.12em; text-shadow:1px 1px 0 rgba(255,255,255,0.6); } | |
| #three-floor-lb .sec .sh .kind.nba { color:#c03030; } | |
| #three-floor-lb .sec .sh .kind.pol { color:#3068b8; } | |
| #three-floor-lb .sec .sh .kind.pqtf { color:#7038a0; } | |
| #three-floor-lb .sec .sh .day { font-size:9px; color:var(--pk-navy); font-weight:700; letter-spacing:0.04em; } | |
| #three-floor-lb .sec .sh .flt { font-size:9px; color:var(--pk-ink); font-weight:700; margin-left:6px; } | |
| #three-floor-lb .sec .sh .flt.up { color:var(--pk-green); } | |
| #three-floor-lb .sec .sh .flt.down { color:var(--pk-red); } | |
| #three-floor-lb .r { | |
| display:grid; grid-template-columns: 18px 1fr 62px 48px; gap:4px; | |
| padding:3px 4px; font-size:10px; cursor:pointer; border-radius:3px; | |
| transition: background 0.12s; align-items:center; } | |
| #three-floor-lb .r:hover { background: rgba(255, 203, 5, 0.28); } | |
| #three-floor-lb .r .rk { font-family:"Press Start 2P",monospace; font-size:7.5px; | |
| color:var(--pk-navy); text-align:right; padding-right:2px; } | |
| #three-floor-lb .r .rk.top { color:#a05820; } | |
| #three-floor-lb .r .nm { color:var(--pk-ink); font-weight:700; white-space:nowrap; | |
| overflow:hidden; text-overflow:ellipsis; letter-spacing:0.02em; } | |
| #three-floor-lb .r .br { color:var(--pk-ink); text-align:right; font-family:"VT323",monospace; font-size:12px; } | |
| #three-floor-lb .r .dl { text-align:right; font-family:"VT323",monospace; font-size:11px; font-weight:700; } | |
| #three-floor-lb .r .dl.up { color:var(--pk-green); } | |
| #three-floor-lb .r .dl.down { color:var(--pk-red); } | |
| #three-floor-lb .r .dl.flat { color:var(--pk-navy); opacity:0.6; } | |
| #three-floor-lb .r.hot { background: linear-gradient(90deg, rgba(255,203,5,0.14) 0%, transparent 100%); } | |
| #three-floor-lb .empty { padding:10px 6px; font-size:9.5px; color:var(--pk-navy); opacity:0.7; | |
| font-style:italic; text-align:center; } | |
| /* v2.22 2026-04-20: leaderboard + dialog hidden to give agents the full canvas. | |
| User ask: "delete the panel with live bets and keep only the tardeboard (ticker) at top". | |
| All data is on the top marquee now. */ | |
| #three-floor-lb { display:none ; } | |
| #poke-dialog { display:none ; } | |
| #boot-log { display:none ; } | |
| #hud-bot { display:none ; } | |
| #minimap { display:none ; } | |
| #root { margin-right: 0 ; } | |
| /* βββ v2.15 POKΓMON B/W DIALOGUE BOX βββ Replaces marquee bet-ticker. | |
| Double-border (outer black + inner white) + inset shadow, typewriter feed. */ | |
| #poke-dialog { position:absolute; bottom:14px; left:12px; right:12px; height:92px; z-index:7; | |
| background:#f8f8f8; color:#181818; | |
| border:4px solid #101820; | |
| border-radius:8px; | |
| box-shadow: inset 0 0 0 2px #f8f8f8, inset 0 0 0 4px #4060b0, 0 4px 14px rgba(0,0,0,0.55); | |
| font-family:"Press Start 2P","VT323","IBM Plex Mono",monospace; font-size:13px; | |
| padding:12px 22px 10px 22px; display:flex; flex-direction:column; gap:4px; | |
| line-height:1.4; letter-spacing:0.02em; } | |
| #poke-dialog .pd-head { display:flex; justify-content:space-between; font-size:9.5px; | |
| color:#4060b0; text-transform:uppercase; letter-spacing:0.2em; font-weight:700; | |
| border-bottom:2px dotted #b0b8c4; padding-bottom:3px; margin-bottom:2px; | |
| font-family:"JetBrains Mono",monospace; } | |
| #poke-dialog .pd-body { flex:1; overflow:hidden; color:#181820; | |
| font-family:"Press Start 2P","VT323",monospace; font-size:12px; line-height:1.5; } | |
| #poke-dialog .pd-body .trn { color:#c03030; font-weight:700; } | |
| #poke-dialog .pd-body .trn.pol { color:#3068b8; } | |
| #poke-dialog .pd-body .amt { color:#1a1a1a; font-weight:700; } | |
| #poke-dialog .pd-body .cat { color:#a05820; font-weight:700; } | |
| #poke-dialog .pd-body .win { color:#207838; font-weight:700; } | |
| #poke-dialog .pd-body .lose { color:#b82030; font-weight:700; } | |
| #poke-dialog .pd-body .cursor { display:inline-block; color:#4060b0; animation:pdblk 0.6s steps(2) infinite; font-weight:700; } | |
| @keyframes pdblk { 50% { opacity:0; } } | |
| /* v2.16 β PokΓ©dex map frame (red plastic shell + yellow inner face). */ | |
| #minimap { position:absolute; top:48px; right:14px; z-index:7; | |
| width:132px; height:92px; padding:4px; | |
| background:var(--pk-red); border:3px solid var(--pk-ink); border-radius:8px; | |
| box-shadow: inset 0 2px 0 rgba(255,255,255,0.22), inset 0 -3px 0 rgba(0,0,0,0.28), | |
| 0 6px 14px rgba(0,0,0,0.5); | |
| overflow:hidden; } | |
| #minimap canvas { display:block; width:124px; height:84px; image-rendering:pixelated; | |
| border:2px solid var(--pk-ink); background:var(--pk-yellow-2); } | |
| #minimap .mm-lbl { position:absolute; top:-1px; left:6px; font-size:7.5px; | |
| letter-spacing:0.16em; color:var(--pk-yellow); text-transform:uppercase; | |
| font-family:"Press Start 2P",monospace; text-shadow:1px 1px 0 var(--pk-ink); pointer-events:none; } | |
| /* v2.16 β Boot log β PokΓ©mon menu text box (white face + navy inner border). */ | |
| #boot-log { position:absolute; bottom:128px; left:14px; z-index:8; | |
| background:var(--pk-box); color:var(--pk-ink); | |
| border:3px solid var(--pk-ink); border-radius:8px; | |
| box-shadow: inset 0 0 0 2px var(--pk-box), inset 0 0 0 4px var(--pk-navy), | |
| 0 6px 14px rgba(0,0,0,0.5); | |
| padding:10px 14px; | |
| font-family:"JetBrains Mono",monospace; font-size:10.5px; | |
| max-width:380px; transition:opacity 0.5s; pointer-events:none; } | |
| #boot-log .bl-line { color:var(--pk-ink); margin:2px 0; letter-spacing:0.02em; } | |
| #boot-log .bl-line::before { content:"βΈ "; color:var(--pk-green); font-weight:700; } | |
| #boot-log .bl-head { color:var(--pk-navy); font-weight:700; letter-spacing:0.14em; | |
| text-transform:uppercase; margin-bottom:5px; padding-bottom:4px; | |
| border-bottom:2px dotted #b0b8c4; | |
| font-family:"Press Start 2P",monospace; font-size:9.5px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="bloomberg-ticker"> | |
| <div class="brand"><span class="dot"></span>NOMOS42 Β· LIVE</div> | |
| <div id="ticker-viewport"> | |
| <div id="ticker-track"> | |
| <!-- PRIMARY SEGMENTS (ids populated by JS; keep exactly one copy of each id) --> | |
| <div class="seg"><span class="lbl">UTC</span><span class="val" id="bt-utc">β</span></div> | |
| <div class="seg"><span class="lbl">NBA TF</span><span class="val" id="bt-nba-day">β</span><span class="val" id="bt-nba-pnl">β</span></div> | |
| <div class="seg"><span class="lbl">POL TF</span><span class="val" id="bt-pol-day">β</span><span class="val" id="bt-pol-pnl">β</span></div> | |
| <div class="seg"><span class="lbl">ITF</span><span class="val" id="bt-itf-trades">β</span><span class="val" id="bt-itf-open">β</span></div> | |
| <div class="seg"><span class="lbl">FLEET BEST</span><span class="val star" id="bt-fleet">β</span></div> | |
| <div class="seg"><span class="lbl">PACTS</span><span class="val" id="bt-pacts">0</span></div> | |
| <div class="seg"><span class="lbl">LLM CALLS</span><span class="val" id="bt-calls">0</span></div> | |
| <!-- Dynamic event feed (last bets, steam moves, Brier ticks) β filled by JS --> | |
| <span id="tk-feed-primary"></span> | |
| <!-- DUPLICATE (id-less clones; kept in sync by JS so loop is seamless) --> | |
| <div class="seg"><span class="lbl">UTC</span><span class="val" data-mirror="bt-utc">β</span></div> | |
| <div class="seg"><span class="lbl">NBA TF</span><span class="val" data-mirror="bt-nba-day">β</span><span class="val" data-mirror="bt-nba-pnl">β</span></div> | |
| <div class="seg"><span class="lbl">POL TF</span><span class="val" data-mirror="bt-pol-day">β</span><span class="val" data-mirror="bt-pol-pnl">β</span></div> | |
| <div class="seg"><span class="lbl">ITF</span><span class="val" data-mirror="bt-itf-trades">β</span><span class="val" data-mirror="bt-itf-open">β</span></div> | |
| <div class="seg"><span class="lbl">FLEET BEST</span><span class="val star" data-mirror="bt-fleet">β</span></div> | |
| <div class="seg"><span class="lbl">PACTS</span><span class="val" data-mirror="bt-pacts">0</span></div> | |
| <div class="seg"><span class="lbl">LLM CALLS</span><span class="val" data-mirror="bt-calls">0</span></div> | |
| <span id="tk-feed-mirror"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="root"> | |
| <div id="stage"> | |
| <div id="hud-top"> | |
| <div> | |
| <span class="brand">NOMOS42 Β· PIXEL WORLD</span> Β· <span id="build-tag">build: β¦</span> Β· PixiJS Β· live Β· trainer-net | |
| </div> | |
| <div><span class="tick" id="hud-tick">Β· booting</span> <span style="font-family:'Press Start 2P',monospace;color:var(--pk-yellow);text-shadow:1px 1px 0 var(--pk-ink);font-size:8px;margin-left:6px;letter-spacing:0.14em">v2.22 Β· marquee + bigger agents + click-to-talk</span></div> | |
| </div> | |
| <!-- v2.12: minimap deleted Β· v2.15: bet-ticker β PokΓ©mon B/W dialogue box --> | |
| <div id="poke-dialog"> | |
| <div class="pd-head"> | |
| <span>β POKΓMON TRAINER BATTLE LOG Β· LIVE BETS</span> | |
| <span id="pd-status">β NBA Β· POL</span> | |
| </div> | |
| <div class="pd-body" id="pd-body">connecting to trading floorsβ¦<span class="cursor">βΌ</span></div> | |
| </div> | |
| <div id="boot-log"> | |
| <div class="bl-head">β NOMOS42 PIXEL WORLD β BOOT</div> | |
| <div class="bl-line">[ok] PixiJS 8 loaded Β· 56Γ32 grid Β· 54 real agents</div> | |
| <div class="bl-line">[ok] palette=30 Β· badges=12 Β· breathing=on Β· emotes=state</div> | |
| <div class="bl-line">[ok] particles=ambient Β· nameplates=perf-gradient Β· halos=axelrod</div> | |
| <div class="bl-line">[ok] ticker=/api/day-decisions Β· minimap=live Β· paper=/paper</div> | |
| </div> | |
| <div id="scanlines"></div> | |
| <div id="vignette"></div> | |
| <div id="hud-bot"> | |
| <div id="hud-day">β</div> | |
| <div id="hud-rep">β</div> | |
| </div> | |
| <div id="hover"></div> | |
| <div id="event-toast"></div> | |
| <div id="hud-kpi"> | |
| <div class="r"><span class="l">NBA</span><span id="k-nba" class="v warn">β</span></div> | |
| <div class="r"><span class="l">Political</span><span id="k-pol" class="v warn">β</span></div> | |
| <div class="r"><span class="l">Intraday</span><span id="k-itf" class="v warn">β</span></div> | |
| <div class="r"><span class="l">Islands</span><span id="k-evo" class="v warn">β</span></div> | |
| <div class="r"><span class="l">Councils</span><span id="k-councils" class="v warn">β</span></div> | |
| <div class="r"><span class="l">Pacts</span><span id="k-pacts" class="v">β</span></div> | |
| <div class="r"><span class="l">Last sync</span><span id="k-last" class="v">β</span></div> | |
| </div> | |
| <div id="inspect-card"> | |
| <div class="h"><span class="t" id="inspect-name">β</span><span class="x" id="inspect-close">β</span></div> | |
| <div class="r"><span class="l">Model</span><span class="v ok" id="inspect-model">β</span></div> | |
| <div class="r"><span class="l">Provider</span><span class="v warn" id="inspect-provider">β</span></div> | |
| <div class="r"><span class="l">Bankroll</span><span class="v ok" id="inspect-bankroll">β</span></div> | |
| <div class="r"><span class="l">ROI</span><span class="v ok" id="inspect-roi">β</span></div> | |
| <div class="r"><span class="l">Win Rate</span><span class="v" id="inspect-wr">β</span></div> | |
| <div class="r"><span class="l">Strategy</span><span class="v warn" id="inspect-strat">β</span></div> | |
| <div class="r"><span class="l">State</span><span class="v" id="inspect-state">β</span></div> | |
| <div class="r"><span class="l">Brier</span><span class="v" id="inspect-brier">β</span></div> | |
| <div class="r"><span class="l">Generation</span><span class="v warn" id="inspect-gen">β</span></div> | |
| <div id="inspect-extra"></div> | |
| </div> | |
| <div class="loader" id="loader">booting pixiβ¦</div> | |
| </div> | |
| </div> | |
| <!-- v2.18 β three-floor leaderboard (NBA TF / POL TF / PQTF), every trader per day --> | |
| <div id="three-floor-lb"> | |
| <div class="hdr"> | |
| <span class="ttl">β LEADERBOARD Β· ALL TRADERS</span> | |
| <span class="tm" id="tfl-clock">β</span> | |
| </div> | |
| <div class="body" id="tfl-body"> | |
| <div class="empty">connecting to trading floorsβ¦</div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/pixi.js@8.6.6/dist/pixi.min.js"></script> | |
| <script> | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // NOMOS42 PIXEL WORLD v2.8 β diegetic in-world panels (leaderboard wall, | |
| // categories chalkboard, evolution board, research shelf), | |
| // scrolling ticker tape, parallax stars + drifting clouds, | |
| // floating inspect card (no more right sidebar). | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| (async () => { | |
| const TILE = 32; | |
| // v2.10: widen to 16:10 so the world fills desktop screens without dead side margins. | |
| const GRID_W = 52, GRID_H = 32; | |
| const WORLD_W = GRID_W * TILE, WORLD_H = GRID_H * TILE; | |
| // v2.24 2026-04-20: fix "3/4 hat" regression β v2.23 forced 80Γ112 on 28Γ24 | |
| // native frames, so the sprite was stretched non-uniformly (YΓ4.67, XΓ2.86) | |
| // β heads/hats looked enormous, bodies squished. We now LOCK to native aspect | |
| // ratio (28:24 = 7:6). CHAR_H stays tall (112), CHAR_W derives from AR β 131. | |
| // The sprite itself is rendered via `spr.scale` (uniform), not width/height. | |
| const CHAR_H = 112; | |
| const CHAR_W = Math.round(CHAR_H * 28/24); // 131 β native-AR-locked, no distortion | |
| const CHAR_SCALE = CHAR_H / 24; // uniform scale applied to 28Γ24 sprite | |
| // v2.17: +itf (amber-orange, reads as "live tape" β distinct from NBA pink + POL cyan). | |
| const KIND_COLORS = { nba:0xff79c6, political:0x8be9fd, itf:0xffa94d, island:0x50fa7b, infra:0x50fa7b, pol_infra:0x8be9fd, council:0xffb86c, crew:0xffb86c, control:0xffd700 }; | |
| const KIND_COLORS_HEX = { nba:"#ff79c6", political:"#8be9fd", itf:"#ffa94d", island:"#50fa7b", infra:"#50fa7b", pol_infra:"#8be9fd", council:"#ffb86c", control:"#ffd700" }; | |
| const KIND_GLYPHS = { nba:"π", political:"π³", itf:"π", island:"π§¬", infra:"πΌ", pol_infra:"πΌ", council:"β" }; | |
| // v2.14: per-trader role accessory β drawn on top of base procedural body so | |
| // each persona reads at-a-glance. Lookup by agent.id (strips "p-" POL prefix). | |
| // b = PIXI.Graphics, W/H = CHAR_W/CHAR_H, outline = 0x0b0d12. | |
| const ROLE_ACCESSORIES = { | |
| "qwen-quant": (b, W, H) => { | |
| b.rect(-W*0.17, -H*0.84, W*0.13, W*0.09).stroke({ width:1.4, color:0x0b0d12 }); | |
| b.rect( W*0.04, -H*0.84, W*0.13, W*0.09).stroke({ width:1.4, color:0x0b0d12 }); | |
| b.moveTo(-W*0.04, -H*0.81).lineTo(W*0.04, -H*0.81).stroke({ width:1.2, color:0x0b0d12 }); | |
| }, | |
| "qwen-arb": (b, W, H) => { | |
| b.rect(W*0.30, -H*0.42, W*0.20, H*0.16).fill({ color:0x8b4513 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(W*0.36, -H*0.44, W*0.08, H*0.03).fill({ color:0xffd700 }); | |
| b.rect(W*0.38, -H*0.26, W*0.04, H*0.03).fill({ color:0xffd700 }); | |
| }, | |
| "llama-contra": (b, W, H) => { | |
| b.rect(-W*0.24, -H*1.02, W*0.48, H*0.09).fill({ color:0xcc0000 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(-W*0.34, -H*0.98, W*0.14, H*0.04).fill({ color:0xcc0000 }).stroke({ width:1, color:0x0b0d12 }); | |
| }, | |
| "gemini-anl": (b, W, H) => { | |
| b.rect(-W*0.32, -H*0.62, W*0.64, H*0.32).fill({ color:0xffffff, alpha:0.88 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(-W*0.02, -H*0.60, W*0.04, H*0.30).fill({ color:0xe0e0e0 }); | |
| b.rect(-W*0.52, -H*0.36, W*0.14, H*0.20).fill({ color:0xd4a574 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(-W*0.50, -H*0.34, W*0.10, H*0.01).fill({ color:0x0b0d12 }); | |
| b.rect(-W*0.50, -H*0.30, W*0.10, H*0.01).fill({ color:0x0b0d12 }); | |
| }, | |
| "gemini-tact": (b, W, H) => { | |
| b.rect(-W*0.28, -H*1.00, W*0.56, H*0.11).fill({ color:0x2d4a2d }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(-W*0.34, -H*0.95, W*0.68, H*0.04).fill({ color:0x2d4a2d }); | |
| b.circle(0, -H*1.03, W*0.06).fill({ color:0xffd700 }).stroke({ width:1, color:0x0b0d12 }); | |
| }, | |
| "mistral-large": (b, W, H) => { | |
| b.rect(-W*0.17, -H*1.16, W*0.34, H*0.20).fill({ color:0x0b0d12 }).stroke({ width:1, color:0x333 }); | |
| b.rect(-W*0.24, -H*1.00, W*0.48, H*0.04).fill({ color:0x0b0d12 }); | |
| b.rect(-W*0.17, -H*1.08, W*0.34, H*0.02).fill({ color:0xdd0000 }); | |
| }, | |
| "mistral-medium": (b, W, H) => { | |
| b.rect(-W*0.32, -H*0.62, W*0.64, H*0.06).fill({ color:0xff79c6 }).stroke({ width:0.6, color:0x0b0d12 }); | |
| b.rect(-W*0.27, -H*0.58, W*0.54, H*0.04).fill({ color:0x50fa7b }); | |
| b.rect(-W*0.30, -H*0.45, W*0.06, H*0.08).fill({ color:0xff79c6 }); | |
| }, | |
| "mistral-small": (b, W, H) => { | |
| b.rect(W*0.28, -H*0.66, W*0.16, H*0.28).fill({ color:0x2a5f8f }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(W*0.30, -H*0.56, W*0.12, H*0.02).fill({ color:0xffd700 }); | |
| b.rect(W*0.32, -H*0.46, W*0.08, H*0.03).fill({ color:0x1a3f6f }); | |
| }, | |
| "mistral-nemo": (b, W, H) => { | |
| b.rect(-W*0.32, -H*0.62, W*0.64, H*0.34).fill({ color:0x2a1a0a }).stroke({ width:1.5, color:0x0b0d12 }); | |
| b.rect(-W*0.02, -H*0.60, W*0.04, H*0.30).fill({ color:0xffd700 }); | |
| b.circle(-W*0.36, -H*0.90, W*0.09).fill({ color:0xf4c49a }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(-W*0.40, -H*0.98, W*0.08, W*0.05).fill({ color:0x0b0d12 }); | |
| }, | |
| "mistral-ministral": (b, W, H) => { | |
| b.moveTo(-W*0.14, -H*0.96).lineTo(0, -H*1.22).lineTo(W*0.14, -H*0.96).closePath() | |
| .fill({ color:0x4a0080 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.circle(0, -H*1.20, W*0.04).fill({ color:0xffd700 }); | |
| b.rect(-W*0.50, -H*0.36, W*0.16, H*0.18).fill({ color:0x8b0000 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.rect(-W*0.48, -H*0.32, W*0.12, H*0.01).fill({ color:0xffd700 }); | |
| b.rect(-W*0.48, -H*0.28, W*0.12, H*0.01).fill({ color:0xffd700 }); | |
| }, | |
| "nemotron-120b": (b, W, H) => { | |
| for (let i = -3; i <= 3; i++) { | |
| b.circle(i*W*0.055, -H*0.56, W*0.025).stroke({ width:1, color:0xffd700 }); | |
| } | |
| b.circle(0, -H*0.50, W*0.04).fill({ color:0xffd700 }).stroke({ width:1, color:0x0b0d12 }); | |
| }, | |
| "selfhost-qwen4b": (b, W, H) => { | |
| b.rect(-W*0.035, -H*0.60, W*0.07, H*0.05).fill({ color:0x1a4d80 }); | |
| b.moveTo(-W*0.04, -H*0.55).lineTo(0, -H*0.30).lineTo(W*0.04, -H*0.55).closePath() | |
| .fill({ color:0x1a4d80 }).stroke({ width:0.6, color:0x0b0d12 }); | |
| b.moveTo(0, -H*0.40).lineTo(-W*0.02, -H*0.36).lineTo(W*0.02, -H*0.36).closePath().fill({ color:0xffd700 }); | |
| }, | |
| "nvidia-minimax": (b, W, H) => { | |
| b.moveTo(-W*0.10, -H*0.50).lineTo(W*0.10, -H*0.45).lineTo(-W*0.10, -H*0.40).closePath() | |
| .fill({ color:0xffd700 }).stroke({ width:1, color:0x0b0d12 }); | |
| }, | |
| "nvidia-llama70": (b, W, H) => { | |
| b.moveTo(0, -H*0.56).lineTo(0, -H*0.38).stroke({ width:1, color:0x888888 }); | |
| b.circle(0, -H*0.36, W*0.06).fill({ color:0xc0c0c0 }).stroke({ width:1, color:0x0b0d12 }); | |
| b.circle(-W*0.04, -H*0.38, W*0.01).fill({ color:0xffffff }); | |
| }, | |
| "selfhost-gemma3": (b, W, H) => { | |
| b.circle(W*0.08, -H*0.80, W*0.09).stroke({ width:1.4, color:0xffd700 }); | |
| b.moveTo(W*0.17, -H*0.80).lineTo(W*0.28, -H*0.58).stroke({ width:1.2, color:0xffd700 }); | |
| }, | |
| "selfhost-qwen06": (b, W, H) => { | |
| b.moveTo(-W*0.11, -H*0.58).lineTo(0, -H*0.56).lineTo(-W*0.11, -H*0.52).closePath().fill({ color:0x1a1f2a }).stroke({ width:0.6, color:0x0b0d12 }); | |
| b.moveTo( W*0.11, -H*0.58).lineTo(0, -H*0.56).lineTo( W*0.11, -H*0.52).closePath().fill({ color:0x1a1f2a }).stroke({ width:0.6, color:0x0b0d12 }); | |
| b.circle(0, -H*0.55, W*0.028).fill({ color:0x1a1f2a }); | |
| }, | |
| "selfhost-dolphin3": (b, W, H) => { | |
| for (let i = -2; i <= 2; i++) { | |
| const x = i * W*0.09; | |
| b.moveTo(x, -H*0.94).lineTo(x + W*0.025, -H*1.14).lineTo(x + W*0.06, -H*0.94).closePath() | |
| .fill({ color:0x3a2b20 }).stroke({ width:0.6, color:0x0b0d12 }); | |
| } | |
| }, | |
| }; | |
| // Axelrod strategy families β colors (cooperators green, defectors red, random purple, complex amber) | |
| const STRATEGY_FAMILIES = { | |
| Cooperator:"coop", TitForTat:"coop", TitFor2Tats:"coop", SuspiciousTitForTat:"coop", | |
| WinStayLoseShift:"coop", Pavlov:"coop", | |
| AllD:"def", Defector:"def", Grudger:"def", Bully:"def", | |
| Random:"rand", Probabilistic:"rand", | |
| Adaptive:"complex", HardTitForTat:"complex", Forgiver:"complex", SoftGrudger:"complex", | |
| }; | |
| const FAMILY_COLORS = { coop:0x50fa7b, def:0xff5c5c, rand:0xbd93f9, complex:0xffb020, unknown:0x5b6478 }; | |
| // βββ v2.0: 30-color Kenney-ish palette β each trader gets a distinct hue βββ | |
| const TRADER_PALETTE = [ | |
| 0xff5c5c, 0xff79c6, 0xffa500, 0xffd700, 0x50fa7b, 0x00d9a7, 0x8be9fd, 0x6272ff, | |
| 0xbd93f9, 0xff9e64, 0xe0af68, 0x73daca, 0x7aa2f7, 0xc0caf5, 0xf7768e, 0x9ece6a, | |
| 0xe95678, 0xfab387, 0xf9e2af, 0xa6e3a1, 0x94e2d5, 0x89dceb, 0x89b4fa, 0xcba6f7, | |
| 0xf5c2e7, 0xb4befe, 0xff9770, 0x70d6ff, 0x8ac926, 0x1982c4, | |
| ]; | |
| // Model-keyword β single-letter badge (6x6 pixel overlay) | |
| const TRADER_BADGES = [ | |
| ["qwen", "Q"], ["mistral", "M"], ["gemini", "G"], ["gemma", "E"], | |
| ["llama", "L"], ["nemotron", "N"], ["phi", "P"], ["deepseek", "D"], | |
| ["kimi", "K"], ["command", "C"], ["claude", "A"], ["grok", "X"], | |
| ]; | |
| function traderBadge(id) { | |
| const lo = (id || "").toLowerCase(); | |
| for (const [k, b] of TRADER_BADGES) if (lo.includes(k)) return b; | |
| return "?"; | |
| } | |
| // Model-keyword β home island (for gameplay loop: walk from island β match floor) | |
| const MODEL_HOME = [ | |
| ["qwen", "S14"], ["mistral", "S13"], ["gemini", "S17"], | |
| ["gemma", "S10"], ["llama", "S11"], ["nemotron", "S18"], | |
| ["phi", "S16"], ["deepseek", "S19"], ["selfhost", "S20"], | |
| ["gpt", "S21"], ["claude", "S22"], ["grok", "S12"], | |
| ]; | |
| function homeIslandFor(id) { | |
| const lo = (id || "").toLowerCase(); | |
| for (const [k, s] of MODEL_HOME) if (lo.includes(k)) return s; | |
| return null; | |
| } | |
| // v2.18 (2026-04-18): TRADER PERSONAS β each trader_id gets a human-readable name. | |
| // Technical model/provider info is hidden on the world but surfaced in the inspect card. | |
| const TRADER_PERSONAS = { | |
| "qwen-quant": { persona: "The Oracle", model: "Qwen 3 235B-A22B", provider: "Cerebras", risk: 0.55, style: "quantitative" }, | |
| "qwen-arb": { persona: "The Gatekeeper", model: "Qwen 3 235B-A22B", provider: "Cerebras", risk: 0.65, style: "arbitrage" }, | |
| "llama-contra": { persona: "The Contrarian", model: "Llama 3.1 8B", provider: "Cerebras", risk: 0.55, style: "contrarian" }, | |
| "gemini-anl": { persona: "The Analyst", model: "Gemini 3 Flash", provider: "Google", risk: 0.55, style: "analytical" }, | |
| "gemini-tact": { persona: "The Tactician", model: "Gemini 3 Flash", provider: "Google", risk: 0.60, style: "tactical" }, | |
| "mistral-large": { persona: "The General", model: "mistral-large-latest", provider: "Mistral", risk: 0.50, style: "ensemble" }, | |
| "mistral-medium": { persona: "The Diplomat", model: "mistral-medium-latest",provider: "Mistral", risk: 0.45, style: "diversified" }, | |
| "mistral-small": { persona: "The Scout", model: "mistral-small-latest", provider: "Mistral", risk: 0.35, style: "wide-coverage" }, | |
| "mistral-nemo": { persona: "The Wildcard", model: "open-mistral-nemo", provider: "Mistral", risk: 0.70, style: "aggressive" }, | |
| "mistral-ministral": { persona: "The Professor", model: "ministral-8b-latest", provider: "Mistral", risk: 0.35, style: "theoretical" }, | |
| "nemotron-120b": { persona: "The Architect", model: "Nemotron-3-Super 120B",provider: "OpenRouter", risk: 0.55, style: "chainthought" }, | |
| "selfhost-qwen4b": { persona: "The Monk", model: "Qwen3-4B (GGUF)", provider: "SelfHost CPU", risk: 0.40, style: "disciplined" }, | |
| "nvidia-minimax": { persona: "The Decider", model: "MiniMax M2.7", provider: "NVIDIA NIM", risk: 0.58, style: "decisive" }, | |
| "nvidia-llama70": { persona: "The Swingman", model: "Llama 3.3 70B", provider: "NVIDIA NIM", risk: 0.50, style: "swing" }, | |
| "selfhost-gemma3": { persona: "The Apprentice", model: "Gemma-3-4B (GGUF)", provider: "SelfHost CPU", risk: 0.45, style: "analytical" }, | |
| "selfhost-qwen06": { persona: "The Featherweight",model: "Qwen3-0.6B (GGUF)", provider: "SelfHost CPU", risk: 0.30, style: "conservative" }, | |
| "selfhost-dolphin3": { persona: "The Rebel", model: "Dolphin3-Llama-3.2-3B",provider: "SelfHost CPU", risk: 0.55, style: "uncensored" }, | |
| // v2.17: ITF personas β each trader is a distinct intraday style. Models are | |
| // the winner-routed choices from `config_agents` (see /api/status on ITF). | |
| "scalper-1": { persona: "The Scalper", model: "Gemini 3 Flash", provider: "Google", risk: 0.45, style: "sub-hour micro-edges" }, | |
| "momentum-1": { persona: "The Momentum", model: "mistral-large-latest", provider: "Mistral", risk: 0.55, style: "sector trend-follow" }, | |
| "mean-rev-1": { persona: "The Mean-Reverter",model: "open-mistral-nemo", provider: "Mistral", risk: 0.40, style: "fade-extremes" }, | |
| "breakout-1": { persona: "The Breakout", model: "mistral-medium-latest",provider: "Mistral", risk: 0.50, style: "range-break continuation" }, | |
| "pairs-1": { persona: "The Pairs", model: "Gemini 2.5 Flash", provider: "Google", risk: 0.40, style: "market-neutral spread" }, | |
| "vol-1": { persona: "The Vol-Regime", model: "Qwen 3 235B-A22B", provider: "Cerebras", risk: 0.50, style: "VIX-aware position sizing" }, | |
| "options-1": { persona: "The Options Desk", model: "mistral-large-latest", provider: "Mistral", risk: 0.50, style: "crypto-perp + equity leverage" }, | |
| }; | |
| function personaFor(id) { | |
| const p = TRADER_PERSONAS[id]; | |
| if (p) return p; | |
| return { persona: id, model: id, provider: "β", risk: 0, style: "β" }; | |
| } | |
| // Match floor center (in tile coords) β agents pilgrimage here when betting | |
| const MATCH_FLOOR = { x: 20, y: 17 }; | |
| // Layout: infra + councils at TOP (click-only, no bubbles), traders in the MIDDLE (moving, ephemeral bubbles) | |
| // v2.12 unified layout β 52Γ32, ZERO dead columns. NBA and POL zones now touch at x=26. | |
| // y=1..4 NBA evo (x 1..25) | POL evo (x 26..50) β halves share the center wall | |
| // y=5..8 councils (x 1..50) β single wide chamber | |
| // y=9..19 NBA TF (x 1..25) | POL TF (x 26..50) β halves share the center wall | |
| // y=20..30 CONTROL ROOM (x 1..50) β 2Γ6 panel grid, full width | |
| // v2.17 (2026-04-19): +ITF zone (Intraday Trading Floor Β· 7 agents, live at | |
| // lbjlincoln26-intraday-trading-floor.hf.space). Inserted between the trading | |
| // floors and the control room by shrinking control-room from h=11 β h=7. | |
| // v2.20 2026-04-20: zones widened so agents roam the full 52Γ32 world. Previous | |
| // heights (4-11 tiles) bottled 41 agents into narrow bands; user reported "agents | |
| // dont move accross all the space". Horizontal zones now ~18 tiles tall each so | |
| // a trader's wanderTarget spans vastly more of the available canvas. | |
| // v2.22 2026-04-20: regroup buildings into dense bands at the top + bottom of the | |
| // world, leaving a LARGE central courtyard (y=8..26 = 18 tiles tall Γ 52 wide) | |
| // where traders roam. User ask: "il faut laisser bien plus de place aux agents | |
| // en regroupant bien mieux les buildings infras". Prior layout had NBA/POL floors | |
| // eating 16 tiles of vertical space β now 18 tiles are pure free-roam canvas. | |
| const ZONES = { | |
| // TOP BAND (y=1..6, 5 tiles) β infra towers, tight grid, no wander | |
| "nba-infra": { x:1, y:1, w:22, h:5, floor:6, label:"NBA EVO Β· 6 ISLANDS" }, | |
| "pol-infra": { x:30, y:1, w:21, h:5, floor:6, label:"POL EVO Β· 5 ISLANDS" }, | |
| // v2.24: councils zone repurposed for the 14-agent Claude Code crew (councils deleted 2026-04-20). | |
| councils: { x:23, y:1, w:7, h:5, floor:1, label:"CLAUDE CREW Β· 14" }, | |
| // CENTRAL COURTYARD (y=7..25, 18 tiles tall Γ 52 wide) β all traders roam here. | |
| // NBA + POL share left/right halves for coloring, but actual wanderTarget spans | |
| // the full courtyard so traders cross between floors freely. | |
| nba: { x:1, y:7, w:25, h:19, floor:3, label:"NBA TRADING FLOOR" }, | |
| political: { x:26, y:7, w:25, h:19, floor:5, label:"POLITICAL FLOOR" }, | |
| // BOTTOM BAND (y=26..31, 6 tiles) β ITF + control room, stacked tight. | |
| itf: { x:1, y:26, w:50, h:3, floor:2, label:"INTRADAY Β· 14 AGENTS 24/7" }, | |
| "control-room":{x:1, y:29, w:50, h:3, floor:8, label:"CONTROL ROOM" }, | |
| }; | |
| // v2.13 (2026-04-17): roster parity with live TF β 17 NBA + 17 POL. | |
| // Was stale at 12 NBA / 10 POL missing T13-T17 + renamed gemma4-selfhostβselfhost-qwen4b. | |
| const NBA_TRADERS = [ | |
| "qwen-quant","qwen-arb","llama-contra","gemini-anl","gemini-tact", | |
| "mistral-large","mistral-medium","mistral-small","mistral-nemo","mistral-ministral", | |
| "nemotron-120b","selfhost-qwen4b","nvidia-minimax","nvidia-llama70", | |
| "selfhost-gemma3","selfhost-qwen06","selfhost-dolphin3" | |
| ]; | |
| const POL_TRADERS = [ | |
| "qwen-quant","qwen-arb","llama-contra","gemini-anl","gemini-tact", | |
| "mistral-large","mistral-medium","mistral-small","mistral-nemo","mistral-ministral", | |
| "nemotron-120b","selfhost-qwen4b","nvidia-minimax","nvidia-llama70", | |
| "selfhost-gemma3","selfhost-qwen06","selfhost-dolphin3" | |
| ]; | |
| // v2.17 (2026-04-19): ITF traders β 7 intraday strategies, live status from | |
| // lbjlincoln26-intraday-trading-floor.hf.space/api/leaderboard. Schema is | |
| // different from NBA/POL (no bankroll β emits decisions/trades/passes/open_positions) | |
| // so the refresh path uses its own wiring. | |
| // v2.25 (2026-04-20): ITF expanded 7 β 14 personas. New 7 are the aggressive | |
| // layer that makes ITF the most active TF (pass rate 84% β 7% after deploy). | |
| const ITF_TRADERS = [ | |
| "scalper-1","momentum-1","mean-rev-1","breakout-1","pairs-1","vol-1","options-1", | |
| "arbitrage-1","news-catalyst-1","crypto-whale-1","earnings-gap-1", | |
| "iv-crush-1","macro-rotate-1","leveraged-momentum-1" | |
| ]; | |
| // v2.17: ISLANDS trimmed to 11 SURVIVORS (was 21). 10 "nul" islands | |
| // eliminated on 2026-04-17 to free HF concurrency for selfhost LLMs. | |
| // Never re-introduce S10/S11/S12/S16/S19/S20/S21/P3/P6/P8 β per MEMORY. | |
| const ISLANDS = [ | |
| ["S13","https://nomos42-nba-evo-4.hf.space/api/status"], | |
| ["S14","https://nomos42-nba-evo-5.hf.space/api/status"], | |
| ["S15","https://nomos42-nba-evo-6.hf.space/api/status"], | |
| ["S17","https://lbjlincoln26-nba-evo-s17.hf.space/api/status"], | |
| ["S18","https://testforge42-nba-evo-s18.hf.space/api/status"], | |
| ["S22","https://testforge42-nba-evo-s22.hf.space/api/status"], | |
| ["P1","https://nomos42-political-alpha.hf.space/api/status"], | |
| ["P2","https://nomos42-political-alpha-2.hf.space/api/status"], | |
| ["P4","https://lbjlincoln-political-alpha-4.hf.space/api/status"], | |
| ["P5","https://lbjlincoln-political-alpha-5.hf.space/api/status"], | |
| ["P7","https://lbjlincoln-political-alpha-7.hf.space/api/status"], | |
| ]; | |
| // v2.24 (2026-04-21): 9 dept councils were DELETED 2026-04-20 (dead HF Spaces). | |
| // Replaced by the 14-agent Claude Code crew β the non-trader subagents that | |
| // actually run research, infra, audits, deploys, revenue, QA. Data source | |
| // changes from dead `nomos-dept-d*` state.json β single dispatch-latest.json | |
| // fetched from GitHub main (public mirror via raw content / Contents API). | |
| const CLAUDE_CREW = [ | |
| // [codename, role, track] | |
| ["THE BOSS","dispatches all 13","L1"], | |
| ["SWISH","NBA islands S10-S22","T1"], | |
| ["LOBBYIST","Political islands P1-P8","T1"], | |
| ["HAWKEYE","daily arXiv/GitHub scout","T1"], | |
| ["DR FRANKENSTEIN","impl research β engine.py","T1"], | |
| ["INTERNAL AFFAIRS","scientific integrity audit","T1"], | |
| ["THE BLACKSMITH","forge Karpathy loops","T2"], | |
| ["SWITCHBOARD","LLM gateway + TF keepalive","T2"], | |
| ["THE PLUMBER","data pipeline + ETL","T2"], | |
| ["LAUNCHPAD","CI/CD + deploy orchestrator","T2"], | |
| ["THE HERALD","Telegram publisher","T3"], | |
| ["THE ACCOUNTANT","Stripe / Whop revenue","T3"], | |
| ["PIXEL","dashboard + world visual QA","T3"], | |
| ["THE TICKER","live odds + CLV + steam","T4"], | |
| ]; | |
| const ENDPOINTS = { | |
| nba: "https://lbjlincoln26-nba-llm-trading-floor.hf.space/api/status", | |
| nbaLB: "https://lbjlincoln26-nba-llm-trading-floor.hf.space/api/leaderboard", | |
| nbaDays: "https://lbjlincoln26-nba-llm-trading-floor.hf.space/api/day-decisions", | |
| political: "https://lbjlincoln26-political-llm-trading-floor.hf.space/api/status", | |
| politicalLB: "https://lbjlincoln26-political-llm-trading-floor.hf.space/api/leaderboard", | |
| politicalDays:"https://lbjlincoln26-political-llm-trading-floor.hf.space/api/day-decisions", | |
| pqtf: "https://lbjlincoln26-political-quant-trading-floor.hf.space/api/status", | |
| pqtfLB: "https://lbjlincoln26-political-quant-trading-floor.hf.space/api/leaderboard", | |
| // v2.17: ITF β 7-agent intraday crypto+equities fleet (24/7). Status returns | |
| // {agents: {tid: {decisions, trades, passes}}}, leaderboard adds tier/name/open_positions. | |
| itf: "https://lbjlincoln26-intraday-trading-floor.hf.space/api/status", | |
| itfLB: "https://lbjlincoln26-intraday-trading-floor.hf.space/api/leaderboard", | |
| }; | |
| // v2.24: dispatch-latest.json lives in the main repo; read the public HF | |
| // mirror (pixel-world is a static Space, so we can't reach VM-local files). | |
| // Nomos42/pixel-world serves a copy pushed on each deploy; THE BOSS cron also | |
| // posts a mirror to the static Space via HfApi.upload_file at :00 every 4h. | |
| const CREW_DISPATCH_URL = | |
| "https://huggingface.co/spaces/Nomos42/pixel-world/resolve/main/data/ops/dispatch-latest.json"; | |
| const AGENTS = [ | |
| ...NBA_TRADERS.map((id,i)=>{ const p = personaFor(id); return { | |
| id, key:id, label: p.persona, persona: p.persona, model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| kind:"nba", zone:"nba", idx:i, char:i%6, | |
| tint: TRADER_PALETTE[i % TRADER_PALETTE.length], badge: traderBadge(id), homeIsland: homeIslandFor(id) }; }), | |
| ...POL_TRADERS.map((id,i)=>{ const p = personaFor(id); return { | |
| id:`p-${id}`, key:id, label: p.persona, persona: p.persona, model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| kind:"political", zone:"political", idx:i, char:(i+2)%6, | |
| tint: TRADER_PALETTE[(i + 15) % TRADER_PALETTE.length], badge: traderBadge(id), homeIsland: homeIslandFor(id) }; }), | |
| // v2.17: ITF agents β id prefix `itf-` so per-agent lookup doesn't collide | |
| // with the NBA/POL namespaces, key = raw tid so we match the leaderboard payload. | |
| ...ITF_TRADERS.map((id,i)=>{ const p = personaFor(id); return { | |
| id:`itf-${id}`, key:id, label: p.persona, persona: p.persona, model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| kind:"itf", zone:"itf", idx:i, char:(i+4)%6, | |
| tint: TRADER_PALETTE[(i + 24) % TRADER_PALETTE.length], badge: traderBadge(id), homeIsland: null }; }), | |
| ...ISLANDS.map(([lbl,url],i)=>({ | |
| id:`island-${lbl.toLowerCase()}`, key:lbl, label:lbl, | |
| kind: lbl.startsWith("P") ? "pol_infra" : "infra", | |
| zone: lbl.startsWith("P") ? "pol-infra" : "nba-infra", | |
| idx:i, char:0, url, _isInfra:true, | |
| })), | |
| // v2.24 crew β 14 Claude Code subagents, rendered as buildings (no wander). | |
| // Use _isInfra:true so they sit still + render via updateInfraTower path. | |
| ...CLAUDE_CREW.map(([codename,role,track],i)=>({ | |
| id:`crew-${codename.toLowerCase().replace(/\s+/g,"-")}`, | |
| key:codename, label:codename, | |
| kind:"crew", zone:"councils", idx:i, char:(i+3)%6, | |
| role, track, _isInfra:true, mission:role, | |
| })), | |
| ]; | |
| // ββ SEATING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function assignSeats() { | |
| for (const [zoneId, z] of Object.entries(ZONES)) { | |
| const list = AGENTS.filter(a => a.zone === zoneId); | |
| if (!list.length) continue; | |
| const isInfraZone = zoneId === "nba-infra" || zoneId === "pol-infra"; | |
| const isCouncil = zoneId === "councils"; | |
| // v2.11 (2026-04-17): cols chosen by zone aspect ratio so rows always fit inside z.h. | |
| // Previous logic hardcoded cols=4/cellH=5 for trader floors β 16 agents Γ 4 rows Γ 5t | |
| // = 20 tiles vertical, zone only 11t tall β negative padY, agents stacked on top of each | |
| // other and visually "doubled" (user report 2026-04-17). | |
| let cols, rows; | |
| if (isCouncil) { | |
| // v2.24: 14 crew agents in the 7Γ5 zone β 7 cols Γ 2 rows (was forced single row). | |
| cols = Math.min(list.length, 7); | |
| rows = Math.ceil(list.length / cols); | |
| } else if (isInfraZone) { | |
| cols = Math.min(list.length, Math.ceil(list.length / 2)); | |
| rows = Math.ceil(list.length / cols); | |
| } else { | |
| cols = Math.max(1, Math.ceil(Math.sqrt(list.length * z.w / z.h))); | |
| rows = Math.ceil(list.length / cols); | |
| } | |
| const cellW = z.w / cols; | |
| const cellH = z.h / rows; | |
| list.forEach((a, i) => { | |
| const c = i % cols, r = Math.floor(i / cols); | |
| a.seat = { | |
| x: z.x + cellW * (c + 0.5), | |
| y: z.y + cellH * (r + 0.5), | |
| }; | |
| a.home = { ...a.seat }; | |
| a.phase = Math.random() * Math.PI * 2; | |
| // v2.13 (2026-04-17): calmer still. User report "erratic and too fast". | |
| // freq 0.15-0.40 β 0.06-0.14 (slower bob cycle) | |
| // wamp 0.10-0.28 β 0.04-0.10 (smaller wobble) | |
| // speed 0.9-1.4 β 0.30-0.50 (3Γ slower linear convergence) | |
| // v2.24: councils are infrastructure too β they don't iterate, so stop | |
| // wandering. Only the 3 live TFs (NBA / POL / ITF) move. | |
| const isStatic = isInfraZone || isCouncil; | |
| a.freq = isStatic ? 0 : 0.06 + Math.random() * 0.08; | |
| a.wamp = isStatic ? 0 : 0.04 + Math.random() * 0.06; | |
| a.speed = isStatic ? 0 : 0.30 + Math.random() * 0.20; | |
| // Occasional longer-range wander target (every 8-15s a trader picks a new point in its floor) | |
| a.wanderNext = 0; | |
| a.x = a.seat.x; a.y = a.seat.y; | |
| a.vx = 0; a.vy = 0; | |
| a.state = "idle"; a.data = null; a.bubble = `${a.label}\nbootingβ¦`; a.activeUntil = 0; | |
| a.pactPartner = null; a.sacrificial = null; a.strategy = null; a.recent = ""; | |
| }); | |
| } | |
| } | |
| assignSeats(); | |
| const MEETING_SPOTS = [ | |
| { x:20, y:14 }, { x:20, y:17 }, { x:20, y:20 }, { x:10, y:17 }, { x:30, y:17 }, | |
| ]; | |
| // ββ PIXI INIT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const stageEl = document.getElementById("stage"); | |
| const app = new PIXI.Application(); | |
| await app.init({ | |
| resizeTo: stageEl, background: 0x0b0d12, antialias: false, | |
| autoDensity: true, resolution: window.devicePixelRatio || 1, | |
| }); | |
| stageEl.appendChild(app.canvas); | |
| app.canvas.style.imageRendering = "pixelated"; | |
| const world = new PIXI.Container(); | |
| app.stage.addChild(world); | |
| // v2.8 2026-04-17: bgLayer (stars+clouds) below floor, boardLayer (diegetic signs) above decor, tickerLayer (top scroll) above world. | |
| const bgLayer = new PIXI.Container(); | |
| const floorLayer = new PIXI.Container(); | |
| const decorLayer = new PIXI.Container(); | |
| const boardLayer = new PIXI.Container(); // in-world diegetic signs (leaderboard/categories) | |
| const particleLayer = new PIXI.Container(); | |
| const pactLayer = new PIXI.Graphics(); | |
| const agentLayer = new PIXI.Container(); | |
| agentLayer.sortableChildren = true; | |
| const labelLayer = new PIXI.Container(); | |
| const tickerLayer = new PIXI.Container(); // scrolling ticker over top of world | |
| world.addChild(bgLayer, floorLayer, decorLayer, boardLayer, particleLayer, pactLayer, agentLayer, labelLayer, tickerLayer); | |
| // βββ v2.10 Diegetic Panel System β FULLY PIXI, inside CONTROL ROOM (no DOM rail) βββ | |
| // 12 data panels arrange in a 2Γ6 grid on the CONTROL ROOM wall. | |
| // Each panel: Bloomberg-style β colored header bar, provider tag, live value, data lines, status LED. | |
| // States: live (green) | stale (amber) | no_signal (red) | fake (red) | |
| const DIEGETIC_PANELS = []; | |
| const _PANEL_SLOT = { row: 0, col: 0 }; // autoincrement slot allocator | |
| const _PANEL_COLS = 6; | |
| const _PANEL_MAX_ROWS = 1; | |
| // v2.22 Panel grid pinned to CONTROL ROOM: zone (x=1, y=29, w=50, h=3) β thinner band. | |
| // Grid collapses to 1 row Γ 6 cols so panels stay readable inside 3 tiles. | |
| const _CTRL_X0 = 1 * TILE + 8; // 40 px | |
| const _CTRL_Y0 = 29 * TILE + 4; // 932 px (just inside zone) | |
| const _CTRL_W = 50 * TILE - 16; // 1584 px inner | |
| const _CTRL_H = 3 * TILE - 8; // 88 px inner (single row) | |
| const _PANEL_GAP = 6; | |
| const _PANEL_W = Math.floor((_CTRL_W - _PANEL_GAP * (_PANEL_COLS - 1)) / _PANEL_COLS); // β258 | |
| const _PANEL_H = Math.floor((_CTRL_H - _PANEL_GAP * (_PANEL_MAX_ROWS - 1)) / _PANEL_MAX_ROWS); // β40 | |
| function _panelStatusColor(s) { | |
| return { live: 0x00d9a7, stale: 0xffa500, no_signal: 0xff5c5c, fake: 0xff5c5c }[s] || 0x9aa3b5; | |
| } | |
| // Each panel is a PIXI.Container with a Bloomberg-terminal look. | |
| // Title-bar: colored bevel stripe, glyph badge, title text, status LED. | |
| // Body: 8 rows of monospace text, colored on LED state. | |
| function makeDiegeticPanel({ title, dataUrl, render, staleSec = 600, glyph }) { | |
| if (_PANEL_SLOT.row >= _PANEL_MAX_ROWS) { | |
| console.warn("panel grid full, skipping", title); | |
| return null; | |
| } | |
| const col = _PANEL_SLOT.col, row = _PANEL_SLOT.row; | |
| const x = _CTRL_X0 + col * (_PANEL_W + _PANEL_GAP); | |
| const y = _CTRL_Y0 + row * (_PANEL_H + _PANEL_GAP); | |
| _PANEL_SLOT.col++; | |
| if (_PANEL_SLOT.col >= _PANEL_COLS) { _PANEL_SLOT.col = 0; _PANEL_SLOT.row++; } | |
| const w = _PANEL_W, h = _PANEL_H; | |
| const accent = 0x00d9a7; // recolored on state change | |
| const g = glyph || (title.split(" ")[0] || "β").slice(0, 1); | |
| const c = new PIXI.Container(); | |
| c.x = x; c.y = y; | |
| c.eventMode = "static"; c.cursor = "pointer"; | |
| // Drop shadow (pixel style) | |
| const shadow = new PIXI.Graphics(); | |
| shadow.rect(3, 3, w, h).fill({ color: 0x000000, alpha: 0.55 }); | |
| c.addChild(shadow); | |
| // Body with bevel | |
| const body = new PIXI.Graphics(); | |
| body.rect(0, 0, w, h).fill({ color: 0x0d1118, alpha: 0.97 }); | |
| body.rect(0, 0, w, 1).fill({ color: 0xffffff, alpha: 0.22 }); // top bevel | |
| body.rect(0, h - 1, w, 1).fill({ color: 0x000000, alpha: 0.7 }); | |
| body.rect(0, 0, 1, h).fill({ color: 0xffffff, alpha: 0.12 }); | |
| body.rect(w - 1, 0, 1, h).fill({ color: 0x000000, alpha: 0.55 }); | |
| c.addChild(body); | |
| // Titlebar β colored stripe | |
| const tb = new PIXI.Graphics(); | |
| tb.rect(0, 0, w, 22).fill({ color: accent, alpha: 0.20 }); | |
| tb.rect(0, 21, w, 1).fill({ color: accent, alpha: 0.95 }); // neon underline | |
| tb.rect(0, 22, w, 1).fill({ color: 0x000000, alpha: 0.5 }); | |
| // Glyph badge square | |
| tb.rect(5, 5, 14, 12).fill({ color: accent, alpha: 0.85 }).stroke({ width: 1, color: 0x000000, alpha: 0.55 }); | |
| c.addChild(tb); c._tb = tb; | |
| const badgeTxt = new PIXI.Text({ text: g, style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 10, fontWeight: "700", fill: 0x11151e, | |
| }}); | |
| badgeTxt.x = 8; badgeTxt.y = 5; | |
| c.addChild(badgeTxt); | |
| const cleanTitle = title.replace(/^[βββ²βΌβββββΆββ£β¦β‘β¦ββ ββ]+\s*/, "").toUpperCase(); | |
| const titleTxt = new PIXI.Text({ text: cleanTitle.slice(0, 32), style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 9.5, fontWeight: "700", | |
| fill: accent, letterSpacing: 1.5, | |
| }}); | |
| titleTxt.x = 25; titleTxt.y = 6; | |
| c.addChild(titleTxt); c._titleTxt = titleTxt; | |
| // Status LED (right side of titlebar) | |
| const led = new PIXI.Graphics(); | |
| led.circle(w - 10, 11, 3).fill({ color: 0x00d9a7 }); | |
| led.circle(w - 10, 11, 5).fill({ color: 0x00d9a7, alpha: 0.35 }); | |
| c.addChild(led); c._led = led; | |
| // State tag text (LIVE / STALE / NO SIGNAL) | |
| const stateTag = new PIXI.Text({ text: "LIVE", style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 8, fontWeight: "700", | |
| fill: 0x00d9a7, letterSpacing: 1, | |
| }}); | |
| stateTag.anchor.set(1, 0); | |
| stateTag.x = w - 20; stateTag.y = 7; | |
| c.addChild(stateTag); c._stateTag = stateTag; | |
| // Content area β 8 lines of data | |
| const MAX_LINES = 8; | |
| const lineTexts = []; | |
| for (let i = 0; i < MAX_LINES; i++) { | |
| const t = new PIXI.Text({ text: "", style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 10, fill: 0xe6e8ee, | |
| }}); | |
| t.x = 8; t.y = 28 + i * 13; | |
| c.addChild(t); lineTexts.push(t); | |
| } | |
| // Hint strip at bottom | |
| const hint = new PIXI.Graphics(); | |
| hint.rect(0, h - 12, w, 12).fill({ color: 0x000000, alpha: 0.45 }); | |
| c.addChild(hint); | |
| const hintTxt = new PIXI.Text({ text: `β¦ ${dataUrl.replace(/^https?:\/\//,"").slice(0,36)}`, style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 8, fill: 0x5b6478, | |
| }}); | |
| hintTxt.x = 6; hintTxt.y = h - 11; | |
| c.addChild(hintTxt); | |
| boardLayer.addChild(c); | |
| const p = { | |
| _ctr: c, _led: led, _tb: tb, _titleTxt: titleTxt, _stateTag: stateTag, _lineTexts: lineTexts, | |
| title, dataUrl, render, staleSec, _state: "live", _lastText: "", | |
| setState(s) { | |
| this._state = s; | |
| const col = _panelStatusColor(s); | |
| led.clear(); | |
| led.circle(w - 10, 11, 3).fill({ color: col }); | |
| led.circle(w - 10, 11, 5).fill({ color: col, alpha: 0.4 }); | |
| tb.clear(); | |
| tb.rect(0, 0, w, 22).fill({ color: col, alpha: 0.20 }); | |
| tb.rect(0, 21, w, 1).fill({ color: col, alpha: 0.95 }); | |
| tb.rect(0, 22, w, 1).fill({ color: 0x000000, alpha: 0.5 }); | |
| tb.rect(5, 5, 14, 12).fill({ color: col, alpha: 0.85 }).stroke({ width: 1, color: 0x000000, alpha: 0.55 }); | |
| titleTxt.style.fill = col; | |
| stateTag.style.fill = col; | |
| stateTag.text = ({ live: "β LIVE", stale: "β² STALE", no_signal: "β NO DATA", fake: "β SUSPECT" })[s] || s.toUpperCase(); | |
| }, | |
| setText(txt) { | |
| this._lastText = txt || ""; | |
| const lines = (txt || "").split("\n").slice(0, MAX_LINES); | |
| for (let i = 0; i < MAX_LINES; i++) { | |
| this._lineTexts[i].text = lines[i] || ""; | |
| } | |
| }, | |
| }; | |
| DIEGETIC_PANELS.push(p); | |
| return p; | |
| } | |
| async function refreshDiegeticPanels() { | |
| for (const p of DIEGETIC_PANELS) { | |
| try { | |
| const r = await fetch(p.dataUrl, { cache: "no-store" }); | |
| if (!r.ok) { p.setState("no_signal"); p.setText(""); continue; } | |
| const data = await r.json(); | |
| const out = p.render(data); | |
| if (!out || out.state === "no_signal") { p.setState("no_signal"); p.setText(""); continue; } | |
| p.setText(out.text || ""); | |
| p.setState(out.state || "live"); | |
| } catch (e) { p.setState("no_signal"); p.setText(""); } | |
| } | |
| } | |
| // v2.18 (2026-04-18): 18 CONTROL ROOM panels deleted β duplicated info already | |
| // shown in HUD/ticker/leaderboard. Replaced by a single clear 3-floor leaderboard | |
| // DOM overlay (see #three-floor-lb below). | |
| // Sidebar removed in v2.8 β all panels are now diegetic (in-world PIXI signs). | |
| // v2.3: Ambient floating particles β data-flow visualization between zones | |
| const PARTICLES = []; | |
| const PARTICLE_COUNT = 35; | |
| for (let i = 0; i < PARTICLE_COUNT; i++) { | |
| const p = new PIXI.Graphics(); | |
| const size = 1.5 + Math.random() * 2; | |
| const colors = [0x00d9a7, 0x8be9fd, 0xff79c6, 0xffd700, 0x50fa7b, 0xbd93f9]; | |
| const col = colors[Math.floor(Math.random() * colors.length)]; | |
| p.circle(0, 0, size).fill({ color: col, alpha: 0.4 + Math.random() * 0.3 }); | |
| p.x = Math.random() * WORLD_W; | |
| p.y = Math.random() * WORLD_H; | |
| p._vx = (Math.random() - 0.5) * 0.3; | |
| p._vy = (Math.random() - 0.5) * 0.2; | |
| p._drift = Math.random() * Math.PI * 2; | |
| p._speed = 0.3 + Math.random() * 0.4; | |
| particleLayer.addChild(p); | |
| PARTICLES.push(p); | |
| } | |
| // v2.3: Bet-placement pulse rings β expand from agent on new bet | |
| const PULSE_RINGS = []; | |
| function spawnPulseRing(x, y, color) { | |
| const ring = new PIXI.Graphics(); | |
| ring._cx = x; ring._cy = y; ring._r = 5; ring._maxR = 40; | |
| ring._color = color; ring._alpha = 0.7; | |
| particleLayer.addChild(ring); | |
| PULSE_RINGS.push(ring); | |
| } | |
| // Room camera β target scale + center computed from active room | |
| let activeRoom = "world"; | |
| let camTargetScale = 1, camTargetX = 0, camTargetY = 0; | |
| let camScale = 1, camX = 0, camY = 0; | |
| function computeCamera() { | |
| const W = stageEl.clientWidth, H = stageEl.clientHeight; | |
| if (activeRoom === "world") { | |
| // v2.18: cover-fit (was contain-fit) β fills stage, small edge crop instead of | |
| // letterbox. Agents live in inner 80% of WORLD so no critical content clipped. | |
| const s = Math.max(W / WORLD_W, H / WORLD_H); | |
| camTargetScale = s; | |
| camTargetX = (W - WORLD_W*s) / 2; | |
| camTargetY = (H - WORLD_H*s) / 2; | |
| } else { | |
| const z = ZONES[activeRoom]; | |
| const zw = z.w * TILE, zh = z.h * TILE; | |
| const pad = 80; | |
| const s = Math.min((W - pad*2) / zw, (H - pad*2) / zh); | |
| camTargetScale = s; | |
| camTargetX = pad - z.x*TILE*s + ((W - pad*2) - zw*s) / 2; | |
| camTargetY = pad - z.y*TILE*s + ((H - pad*2) - zh*s) / 2; | |
| } | |
| } | |
| window.addEventListener("resize", computeCamera); | |
| // Room tabs removed in v2.18 β the world view is the only view. | |
| // ββ ASSET LOAD ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function loadTex(url) { | |
| try { | |
| const tex = await PIXI.Assets.load(url); | |
| if (tex && tex.source) tex.source.scaleMode = "nearest"; | |
| return tex; | |
| } catch(e) { return null; } | |
| } | |
| const TEX = { chars:[], floors:[], furn:{} }; | |
| // Spritesheets are 112Γ96 = 4Γ4 grid of 28Γ24 frames. Extract front-facing idle (0,0). | |
| for (let i=0;i<6;i++) { | |
| const sheet = await loadTex(`assets/characters/char_${i}.png`); | |
| if (sheet) { | |
| const frame = new PIXI.Rectangle(0, 0, 28, 24); | |
| const sub = new PIXI.Texture({ source: sheet.source, frame }); | |
| TEX.chars.push(sub); | |
| } else { | |
| TEX.chars.push(null); | |
| } | |
| } | |
| for (let i=0;i<9;i++) TEX.floors.push(await loadTex(`assets/floors/floor_${i}.png`)); | |
| const FURN_FILES = { | |
| DESK:"DESK/DESK_FRONT.png", PC:"PC/PC_FRONT_ON_1.png", | |
| PLANT:"PLANT/PLANT.png", PLANT_2:"PLANT_2/PLANT_2.png", CACTUS:"CACTUS/CACTUS.png", | |
| BOOKSHELF:"BOOKSHELF/BOOKSHELF.png", COFFEE:"COFFEE/COFFEE.png", | |
| WHITEBOARD:"WHITEBOARD/WHITEBOARD.png", CLOCK:"CLOCK/CLOCK.png", | |
| SOFA:"SOFA/SOFA.png", COFFEE_TABLE:"COFFEE_TABLE/COFFEE_TABLE.png", | |
| }; | |
| for (const [k,f] of Object.entries(FURN_FILES)) TEX.furn[k] = await loadTex(`assets/furniture/${f}`); | |
| document.getElementById("loader").style.display = "none"; | |
| // ββ WORLD BUILD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function zoneIdToKind(zoneId) { | |
| if (zoneId === "nba") return "nba"; | |
| if (zoneId === "political") return "political"; | |
| if (zoneId === "itf") return "itf"; | |
| if (zoneId === "nba-infra") return "infra"; | |
| if (zoneId === "pol-infra") return "pol_infra"; | |
| if (zoneId === "control-room") return "control"; | |
| return "council"; | |
| } | |
| for (const [zoneId, z] of Object.entries(ZONES)) { | |
| const floorTex = TEX.floors[z.floor] || TEX.floors[0]; | |
| for (let ty=z.y; ty<z.y+z.h; ty++) { | |
| for (let tx=z.x; tx<z.x+z.w; tx++) { | |
| if (!floorTex) continue; | |
| const spr = new PIXI.Sprite(floorTex); | |
| spr.x = tx*TILE; spr.y = ty*TILE; spr.width = TILE; spr.height = TILE; | |
| floorLayer.addChild(spr); | |
| } | |
| } | |
| // v2.3: Subtle checkerboard overlay on zone floors | |
| const chk = new PIXI.Graphics(); | |
| for (let ty=z.y; ty<z.y+z.h; ty++) { | |
| for (let tx=z.x; tx<z.x+z.w; tx++) { | |
| if ((tx + ty) % 2 === 0) { | |
| chk.rect(tx*TILE, ty*TILE, TILE, TILE).fill({ color: 0xffffff, alpha: 0.025 }); | |
| } | |
| } | |
| } | |
| floorLayer.addChild(chk); | |
| // βββ v2.15: PokΓ©mon B/W tilemap motif βββ per-zone 16Γ16 pattern overlay. | |
| // Gen5 floor-tile feel: wood strips (NBA), marble (POL), circuit (infra), carpet (councils), grid (control). | |
| const tilemap = new PIXI.Graphics(); | |
| const TM = 16; // 16px sub-tile (half of TILE=32) | |
| const zx0 = z.x*TILE, zy0 = z.y*TILE, zxw = z.w*TILE, zyh = z.h*TILE; | |
| if (zoneId === "nba") { | |
| // Hardwood basketball court β vertical plank lines + crossgrain dots | |
| for (let px = zx0; px < zx0 + zxw; px += TILE) { | |
| tilemap.rect(px, zy0, 1, zyh).fill({ color: 0x4a2810, alpha: 0.35 }); | |
| } | |
| for (let py = zy0 + 16; py < zy0 + zyh; py += 32) { | |
| for (let px = zx0 + 8; px < zx0 + zxw; px += 32) { | |
| tilemap.rect(px, py, 2, 1).fill({ color: 0x3a1810, alpha: 0.4 }); | |
| } | |
| } | |
| // v2.21 HOOP-LAND β full basketball court markings (chasersgaming Basketball-NES | |
| // inspiration, pure SVG/vector since the pack is paid; CC0 substitute). | |
| // Midcourt line + center circle + 2 hoops (left+right end with rim+backboard+net). | |
| const cyMid = zy0 + zyh / 2; | |
| const cxMid = zx0 + zxw / 2; | |
| // Midcourt line | |
| tilemap.rect(cxMid, zy0 + 8, 1, zyh - 16).fill({ color: 0xe8d690, alpha: 0.55 }); | |
| // Center circle (outer + inner) | |
| tilemap.circle(cxMid, cyMid, 36).stroke({ width: 1.5, color: 0xe8d690, alpha: 0.55 }); | |
| tilemap.circle(cxMid, cyMid, 4).fill({ color: 0xe8d690, alpha: 0.45 }); | |
| // 3-point arcs on each side (half-circles facing out) | |
| tilemap.arc(zx0 + 40, cyMid, 60, -Math.PI/2, Math.PI/2).stroke({ width: 1.5, color: 0xe8d690, alpha: 0.5 }); | |
| tilemap.arc(zx0 + zxw - 40, cyMid, 60, Math.PI/2, -Math.PI/2).stroke({ width: 1.5, color: 0xe8d690, alpha: 0.5 }); | |
| // Free-throw key rectangles | |
| tilemap.rect(zx0 + 4, cyMid - 22, 32, 44).stroke({ width: 1, color: 0xe8d690, alpha: 0.45 }); | |
| tilemap.rect(zx0 + zxw - 36, cyMid - 22, 32, 44).stroke({ width: 1, color: 0xe8d690, alpha: 0.45 }); | |
| // Free-throw circles | |
| tilemap.circle(zx0 + 36, cyMid, 14).stroke({ width: 1, color: 0xe8d690, alpha: 0.45 }); | |
| tilemap.circle(zx0 + zxw - 36, cyMid, 14).stroke({ width: 1, color: 0xe8d690, alpha: 0.45 }); | |
| // LEFT HOOP β red backboard rect + orange rim circle + white net triangle | |
| tilemap.rect(zx0 + 2, cyMid - 10, 2, 20).fill({ color: 0xd63030, alpha: 0.85 }); // backboard | |
| tilemap.circle(zx0 + 10, cyMid, 4).stroke({ width: 1.5, color: 0xff8c2a, alpha: 0.95 }); // rim | |
| tilemap.moveTo(zx0 + 6, cyMid + 4).lineTo(zx0 + 10, cyMid + 10).lineTo(zx0 + 14, cyMid + 4) | |
| .stroke({ width: 0.8, color: 0xffffff, alpha: 0.6 }); // net | |
| // RIGHT HOOP β mirrored | |
| tilemap.rect(zx0 + zxw - 4, cyMid - 10, 2, 20).fill({ color: 0xd63030, alpha: 0.85 }); | |
| tilemap.circle(zx0 + zxw - 10, cyMid, 4).stroke({ width: 1.5, color: 0xff8c2a, alpha: 0.95 }); | |
| tilemap.moveTo(zx0 + zxw - 6, cyMid + 4).lineTo(zx0 + zxw - 10, cyMid + 10).lineTo(zx0 + zxw - 14, cyMid + 4) | |
| .stroke({ width: 0.8, color: 0xffffff, alpha: 0.6 }); | |
| } else if (zoneId === "political") { | |
| // Marble capitol floor β diamond grid + specks | |
| for (let py = zy0; py < zy0 + zyh; py += TM) { | |
| for (let px = zx0; px < zx0 + zxw; px += TM) { | |
| const odd = ((px/TM + py/TM) % 2 === 0); | |
| if (odd) tilemap.rect(px + 6, py + 6, 4, 4).fill({ color: 0xc0d8f0, alpha: 0.12 }); | |
| else tilemap.rect(px + 7, py + 7, 2, 2).fill({ color: 0x8ba6c8, alpha: 0.22 }); | |
| } | |
| } | |
| } else if (zoneId === "nba-infra" || zoneId === "pol-infra") { | |
| // Circuit board β green traces + solder pads | |
| const trace = zoneId === "nba-infra" ? 0x50fa7b : 0x8be9fd; | |
| for (let py = zy0; py < zy0 + zyh; py += TILE) { | |
| tilemap.rect(zx0, py + 15, zxw, 1).fill({ color: trace, alpha: 0.20 }); | |
| } | |
| for (let py = zy0 + 8; py < zy0 + zyh; py += TILE) { | |
| for (let px = zx0 + 8; px < zx0 + zxw; px += TILE) { | |
| tilemap.circle(px, py, 1.5).fill({ color: trace, alpha: 0.35 }); | |
| } | |
| } | |
| } else if (zoneId === "councils") { | |
| // Red carpet with gold trim β diagonal weave + edge stripes | |
| for (let py = zy0 + 4; py < zy0 + zyh - 4; py += 6) { | |
| tilemap.rect(zx0 + 4, py, zxw - 8, 1).fill({ color: 0xd04040, alpha: 0.18 }); | |
| } | |
| // Gold trim at top + bottom | |
| tilemap.rect(zx0 + 4, zy0 + 2, zxw - 8, 2).fill({ color: 0xffc830, alpha: 0.4 }); | |
| tilemap.rect(zx0 + 4, zy0 + zyh - 4, zxw - 8, 2).fill({ color: 0xffc830, alpha: 0.4 }); | |
| } else if (zoneId === "control-room") { | |
| // Dark tech grid β thin cyan crosshatch with diode dots | |
| for (let py = zy0; py <= zy0 + zyh; py += TILE) { | |
| tilemap.rect(zx0, py, zxw, 1).fill({ color: 0x00d9a7, alpha: 0.10 }); | |
| } | |
| for (let px = zx0; px <= zx0 + zxw; px += TILE) { | |
| tilemap.rect(px, zy0, 1, zyh).fill({ color: 0x00d9a7, alpha: 0.10 }); | |
| } | |
| for (let py = zy0 + 8; py < zy0 + zyh; py += 64) { | |
| for (let px = zx0 + 8; px < zx0 + zxw; px += 64) { | |
| tilemap.circle(px, py, 1.2).fill({ color: 0xffd700, alpha: 0.45 }); | |
| } | |
| } | |
| } else if (zoneId === "itf") { | |
| // v2.17 ITF β ticker tape: horizontal scan lines + candlestick silhouettes. | |
| // Reads instantly as a trading floor without needing animation. | |
| for (let py = zy0; py < zy0 + zyh; py += 4) { | |
| tilemap.rect(zx0, py, zxw, 1).fill({ color: 0xffa94d, alpha: 0.07 }); | |
| } | |
| // Candles spaced every 32px β alternating green/red wicks | |
| for (let px = zx0 + 16, i = 0; px < zx0 + zxw - 8; px += 24, i++) { | |
| const bull = (i * 7 + 3) % 5 < 3; | |
| const col = bull ? 0x50fa7b : 0xff5c5c; | |
| const h = 8 + ((i * 13) % 14); | |
| const cy = zy0 + zyh/2 - h/2; | |
| tilemap.rect(px, cy, 3, h).fill({ color: col, alpha: 0.22 }); | |
| tilemap.rect(px + 1, cy - 4, 1, 4).fill({ color: col, alpha: 0.22 }); // upper wick | |
| tilemap.rect(px + 1, cy + h, 1, 4).fill({ color: col, alpha: 0.22 }); // lower wick | |
| } | |
| } | |
| floorLayer.addChild(tilemap); | |
| // v2.9: brighter zone tints β 0.10 β 0.18, more distinct separations | |
| const tint = new PIXI.Graphics(); | |
| tint.rect(z.x*TILE, z.y*TILE, z.w*TILE, z.h*TILE).fill({ color: KIND_COLORS[zoneIdToKind(zoneId)], alpha: 0.12 }); | |
| floorLayer.addChild(tint); | |
| // v2.9: Triple-stroke zone border: outer glow + neon tube + inner highlight | |
| const border = new PIXI.Graphics(); | |
| const zkCol = KIND_COLORS[zoneIdToKind(zoneId)]; | |
| border.rect(z.x*TILE-3, z.y*TILE-3, z.w*TILE+6, z.h*TILE+6).stroke({ width:5, color: zkCol, alpha:0.14 }); // outer glow | |
| border.rect(z.x*TILE-1, z.y*TILE-1, z.w*TILE+2, z.h*TILE+2).stroke({ width:3, color: zkCol, alpha:0.85 }); // neon tube | |
| border.rect(z.x*TILE+1, z.y*TILE+1, z.w*TILE-2, z.h*TILE-2).stroke({ width:1, color: 0xffffff, alpha:0.25 }); // inner highlight | |
| floorLayer.addChild(border); | |
| // v2.3: Corner brackets (pixel art flourish) | |
| const bk = new PIXI.Graphics(); | |
| const bkLen = 12, bkX = z.x*TILE, bkY = z.y*TILE, bkW = z.w*TILE, bkH = z.h*TILE; | |
| const bkCol = KIND_COLORS[zoneIdToKind(zoneId)]; | |
| for (const [cx,cy,dx,dy] of [[bkX,bkY,1,1],[bkX+bkW,bkY,-1,1],[bkX,bkY+bkH,1,-1],[bkX+bkW,bkY+bkH,-1,-1]]) { | |
| bk.moveTo(cx, cy+dy*bkLen).lineTo(cx, cy).lineTo(cx+dx*bkLen, cy); | |
| } | |
| bk.stroke({ width:2, color:bkCol, alpha:0.8 }); | |
| floorLayer.addChild(bk); | |
| const txt = new PIXI.Text({ text: z.label, style:{ | |
| fontFamily:"JetBrains Mono, monospace", fontSize:16, fontWeight:"700", | |
| fill: KIND_COLORS[zoneIdToKind(zoneId)], letterSpacing:2.5, | |
| }}); | |
| txt.x = z.x*TILE + 10; txt.y = z.y*TILE + 8; txt.alpha = 0.80; | |
| labelLayer.addChild(txt); | |
| // v2.14: zone-specific iconic decor β one big silhouette per zone, drawn faintly | |
| // in decorLayer so it reads as "environmental flavor" without stealing attention | |
| // from agents. Placed toward zone upper-right so it doesn't collide with label | |
| // (top-left) or active agent area (center-bottom). | |
| const dec = new PIXI.Graphics(); | |
| const dx = (z.x + z.w - 5) * TILE; | |
| const dy = (z.y + 2) * TILE; | |
| const dcol = KIND_COLORS[zoneIdToKind(zoneId)]; | |
| const dalpha = 0.16; | |
| if (zoneId === "nba") { | |
| // basketball hoop: backboard + rim + net | |
| dec.rect(dx, dy - 4, 48, 4).fill({ color: 0xffffff, alpha: dalpha * 1.2 }); | |
| dec.circle(dx + 24, dy + 12, 10).stroke({ width: 2, color: 0xff6600, alpha: dalpha * 2 }); | |
| for (let i = 0; i < 5; i++) { | |
| dec.moveTo(dx + 14 + i*5, dy + 14).lineTo(dx + 18 + i*5, dy + 28).stroke({ width: 1, color: 0xffffff, alpha: dalpha }); | |
| } | |
| } else if (zoneId === "political") { | |
| // capitol dome: dome + pillars | |
| dec.circle(dx + 24, dy + 12, 18).fill({ color: dcol, alpha: dalpha }); | |
| dec.rect(dx, dy + 12, 48, 4).fill({ color: dcol, alpha: dalpha * 1.5 }); | |
| for (let i = 0; i < 5; i++) { | |
| dec.rect(dx + 4 + i*10, dy + 16, 4, 20).fill({ color: dcol, alpha: dalpha }); | |
| } | |
| dec.moveTo(dx + 24, dy - 6).lineTo(dx + 24, dy + 4).stroke({ width: 2, color: dcol, alpha: dalpha * 2 }); | |
| } else if (zoneId === "nba-infra" || zoneId === "pol-infra") { | |
| // server rack grid: 3Γ4 rack units | |
| for (let rr = 0; rr < 4; rr++) { | |
| for (let cc = 0; cc < 3; cc++) { | |
| dec.rect(dx + cc*14, dy + rr*10, 12, 8).stroke({ width: 1, color: dcol, alpha: dalpha * 2 }); | |
| dec.circle(dx + cc*14 + 10, dy + rr*10 + 4, 1).fill({ color: 0x00d9a7, alpha: dalpha * 3 }); | |
| } | |
| } | |
| } else if (zoneId === "councils") { | |
| // round table | |
| dec.circle(dx + 24, dy + 12, 20).stroke({ width: 2, color: dcol, alpha: dalpha * 2 }); | |
| dec.circle(dx + 24, dy + 12, 20).fill({ color: dcol, alpha: dalpha * 0.6 }); | |
| for (let i = 0; i < 6; i++) { | |
| const ang = (i / 6) * Math.PI * 2; | |
| dec.circle(dx + 24 + Math.cos(ang)*14, dy + 12 + Math.sin(ang)*14, 2).fill({ color: dcol, alpha: dalpha * 3 }); | |
| } | |
| } else if (zoneId === "control-room") { | |
| // control panel: oscilloscope box | |
| dec.rect(dx, dy, 48, 28).stroke({ width: 1.5, color: dcol, alpha: dalpha * 2 }); | |
| dec.moveTo(dx + 4, dy + 14).lineTo(dx + 12, dy + 8).lineTo(dx + 20, dy + 18).lineTo(dx + 28, dy + 6) | |
| .lineTo(dx + 36, dy + 20).lineTo(dx + 44, dy + 14).stroke({ width: 1, color: 0x00d9a7, alpha: dalpha * 3 }); | |
| } else if (zoneId === "itf") { | |
| // v2.17 ITF silhouette: stock ticker board + bull/bear pair | |
| dec.rect(dx, dy, 48, 20).stroke({ width: 1.5, color: dcol, alpha: dalpha * 2 }); | |
| dec.rect(dx + 2, dy + 2, 44, 16).fill({ color: 0x000000, alpha: dalpha }); | |
| // scrolling ticker dots | |
| for (let i = 0; i < 7; i++) { | |
| dec.rect(dx + 4 + i*6, dy + 8, 4, 2).fill({ color: (i%2)?0x50fa7b:0xffa94d, alpha: dalpha * 3 }); | |
| } | |
| // bull + bear horns | |
| dec.circle(dx + 8, dy + 30, 3).fill({ color: 0x50fa7b, alpha: dalpha * 2 }); | |
| dec.circle(dx + 40, dy + 30, 3).fill({ color: 0xff5c5c, alpha: dalpha * 2 }); | |
| } | |
| decorLayer.addChild(dec); | |
| } | |
| // v2.5: Data-flow cables β top infra feeds councils, councils feed traders below | |
| const cables = new PIXI.Graphics(); | |
| // infra row β council row (vertical drops) | |
| cables.moveTo(10*TILE, 5*TILE).lineTo(10*TILE, 6*TILE); | |
| cables.moveTo(30*TILE, 5*TILE).lineTo(30*TILE, 6*TILE); | |
| // council row horizontal bus | |
| cables.moveTo(1*TILE, 10*TILE).lineTo(38*TILE, 10*TILE); | |
| // council β trading floors (vertical drops) | |
| cables.moveTo(10*TILE, 10*TILE).lineTo(10*TILE, 11*TILE); | |
| cables.moveTo(30*TILE, 10*TILE).lineTo(30*TILE, 11*TILE); | |
| cables.stroke({ width:1, color:0x3a2600, alpha:0.5 }); | |
| const cableDots = new PIXI.Graphics(); | |
| for (const [cx,cy] of [[10,5],[30,5],[10,10],[30,10],[1,10],[38,10]]) { | |
| cableDots.circle(cx*TILE, cy*TILE, 4).fill({ color:0xffa500, alpha:0.6 }); | |
| } | |
| decorLayer.addChild(cables, cableDots); | |
| // v2.21 FACTORIO CONVEYOR OVERLAY β Factorio-style belt chevrons on top of the | |
| // existing cable mesh to read as "data conveyor with explicit direction". Data | |
| // flow: infra (y=1-10) β councils (y=10) β trading floors (y=11+). Each chevron | |
| // is a tiny arrow painted along the existing cable path, pointing downstream. | |
| const belts = new PIXI.Graphics(); | |
| const BELT_COL_A = 0xffc830; // gold (primary flow) | |
| const BELT_COL_B = 0xff8c2a; // orange (alt flow) | |
| // Helper: draw chevron at (x,y) pointing in direction "down" or "right" | |
| function _chev(g, x, y, dir, col) { | |
| if (dir === "down") { | |
| g.moveTo(x - 4, y - 3).lineTo(x, y + 3).lineTo(x + 4, y - 3).stroke({ width: 1.2, color: col, alpha: 0.72 }); | |
| } else { | |
| g.moveTo(x - 3, y - 4).lineTo(x + 3, y).lineTo(x - 3, y + 4).stroke({ width: 1.2, color: col, alpha: 0.72 }); | |
| } | |
| } | |
| // NBA infra β NBA floor (down the vertical spine at x=10*TILE, y=5*TILEβ6*TILE) | |
| for (let cy = 5*TILE + 8; cy < 6*TILE; cy += 10) _chev(belts, 10*TILE, cy, "down", BELT_COL_A); | |
| // POL infra β POL floor (x=30*TILE, y=5β6) | |
| for (let cy = 5*TILE + 8; cy < 6*TILE; cy += 10) _chev(belts, 30*TILE, cy, "down", BELT_COL_A); | |
| // Council bus (horizontal, x=1*TILE β 38*TILE at y=10*TILE) β rightward flow | |
| for (let cx = 2*TILE; cx < 38*TILE; cx += 48) _chev(belts, cx, 10*TILE, "right", BELT_COL_B); | |
| // Council β trading floors drops (y=10β11 at x=10 and x=30) | |
| for (let cy = 10*TILE + 6; cy < 11*TILE; cy += 10) _chev(belts, 10*TILE, cy, "down", BELT_COL_A); | |
| for (let cy = 10*TILE + 6; cy < 11*TILE; cy += 10) _chev(belts, 30*TILE, cy, "down", BELT_COL_A); | |
| // ITF band (y=22) β horizontal conveyor the full width, shows 24/7 tape direction | |
| for (let cx = 2*TILE; cx < 49*TILE; cx += 40) _chev(belts, cx, 22*TILE - 4, "right", BELT_COL_B); | |
| // Inserter "nodes" at each junction β small factorio-style pulse dot | |
| const inserters = new PIXI.Graphics(); | |
| for (const [ix, iy] of [[10,5],[30,5],[10,10],[30,10],[1,10],[38,10],[10,11],[30,11]]) { | |
| inserters.rect(ix*TILE - 3, iy*TILE - 3, 6, 6).fill({ color: 0xffc830, alpha: 0.85 }); | |
| inserters.rect(ix*TILE - 2, iy*TILE - 2, 4, 4).fill({ color: 0x000000, alpha: 0.55 }); | |
| inserters.rect(ix*TILE - 1, iy*TILE - 1, 2, 2).fill({ color: 0xff8c2a, alpha: 1.0 }); | |
| } | |
| decorLayer.addChild(belts, inserters); | |
| // Furniture: scaled to TILE (canonical pixel-art decor) | |
| const FURN_SIZE = Math.round(TILE * 0.85); | |
| const decorList = []; | |
| for (const [zoneId, z] of Object.entries(ZONES)) { | |
| // v2.17: itf is a thin 4-tall full-width strip β standard furniture | |
| // template would stack objects on top of agents. Handle separately below. | |
| if (zoneId === "nba-infra" || zoneId === "pol-infra" || zoneId === "control-room" || zoneId === "itf") continue; | |
| decorList.push(["WHITEBOARD", z.x + z.w/2 - 0.5, z.y + 0.2, FURN_SIZE*2, FURN_SIZE*1.2]); | |
| decorList.push(["CLOCK", z.x + 0.5, z.y + 0.3, FURN_SIZE, FURN_SIZE]); | |
| decorList.push(["PLANT", z.x + 0.3, z.y + z.h - 1.5, FURN_SIZE*1.1, FURN_SIZE*1.1]); | |
| decorList.push(["CACTUS", z.x + z.w - 1.5, z.y + z.h - 1.5, FURN_SIZE, FURN_SIZE]); | |
| decorList.push(["BOOKSHELF", z.x + z.w - 1.5, z.y + 0.3, FURN_SIZE, FURN_SIZE*1.4]); | |
| } | |
| // v2.17: ITF decor β single clock for "market-hours" cue + corner plants | |
| { | |
| const z = ZONES.itf; | |
| decorList.push(["CLOCK", z.x + 0.4, z.y + 0.4, FURN_SIZE*0.9, FURN_SIZE*0.9]); | |
| decorList.push(["PLANT", z.x + z.w - 1.5, z.y + 0.2, FURN_SIZE*0.9, FURN_SIZE*0.9]); | |
| } | |
| // v2.9: CONTROL ROOM decor β operator chairs along bottom, plants at corners | |
| decorList.push(["PLANT", 1.3, 30, FURN_SIZE*1.1, FURN_SIZE*1.1]); | |
| decorList.push(["PLANT", 37.0, 30, FURN_SIZE*1.1, FURN_SIZE*1.1]); | |
| decorList.push(["SOFA", 6, 30.2, FURN_SIZE*1.3, FURN_SIZE*0.9]); | |
| decorList.push(["SOFA", 18, 30.2, FURN_SIZE*1.3, FURN_SIZE*0.9]); | |
| decorList.push(["SOFA", 30, 30.2, FURN_SIZE*1.3, FURN_SIZE*0.9]); | |
| decorList.push(["COFFEE_TABLE", 19, 13.2, FURN_SIZE*1.5, FURN_SIZE]); | |
| decorList.push(["COFFEE", 19.3, 12.5, FURN_SIZE*0.9, FURN_SIZE*0.9]); | |
| decorList.push(["SOFA", 18, 13.8, FURN_SIZE*1.5, FURN_SIZE]); | |
| decorList.push(["SOFA", 21, 13.8, FURN_SIZE*1.5, FURN_SIZE]); | |
| for (const [k, tx, ty, w, h] of decorList) { | |
| const tex = TEX.furn[k]; if (!tex) continue; | |
| const spr = new PIXI.Sprite(tex); | |
| spr.x = tx*TILE; spr.y = ty*TILE; spr.width = w; spr.height = h; | |
| decorLayer.addChild(spr); | |
| } | |
| // βββ v2.8 BACKGROUND: twilight gradient + parallax stars + drifting clouds βββ | |
| { | |
| const W = WORLD_W, H = WORLD_H; | |
| // Night sky gradient (top 40% of world) | |
| const sky = new PIXI.Graphics(); | |
| const bands = 14; | |
| for (let i = 0; i < bands; i++) { | |
| const t = i / (bands - 1); | |
| const r = Math.round(0x0b + (0x1a - 0x0b) * t); | |
| const g = Math.round(0x0d + (0x0f - 0x0d) * t); | |
| const b = Math.round(0x12 + (0x2a - 0x12) * t); | |
| const color = (r << 16) | (g << 8) | b; | |
| sky.rect(0, (H * 0.42) * (i / bands), W, (H * 0.42) / bands).fill({ color, alpha: 0.55 }); | |
| } | |
| bgLayer.addChild(sky); | |
| // Stars β small white dots with twinkle (stored so tickRender can pulse them) | |
| const stars = []; | |
| const starGfx = new PIXI.Graphics(); | |
| bgLayer.addChild(starGfx); | |
| for (let i = 0; i < 120; i++) { | |
| stars.push({ | |
| x: Math.random() * W, | |
| y: Math.random() * H * 0.36, | |
| r: Math.random() * 1.4 + 0.4, | |
| phase: Math.random() * Math.PI * 2, | |
| speed: 0.002 + Math.random() * 0.004, | |
| }); | |
| } | |
| window._starField = { stars, gfx: starGfx }; | |
| // Clouds β 3 layers at different parallax speeds, each a fuzzy blob | |
| const clouds = []; | |
| const cloudGfx = new PIXI.Graphics(); | |
| bgLayer.addChild(cloudGfx); | |
| for (let i = 0; i < 8; i++) { | |
| clouds.push({ | |
| x: Math.random() * W, | |
| y: 30 + Math.random() * (H * 0.30), | |
| w: 80 + Math.random() * 140, | |
| h: 18 + Math.random() * 12, | |
| vx: 0.08 + Math.random() * 0.22, | |
| alpha: 0.06 + Math.random() * 0.08, | |
| }); | |
| } | |
| window._cloudField = { clouds, gfx: cloudGfx }; | |
| // v2.9: dropped the dark floor vignette β world was too dark. Add warm floor glow band above control room. | |
| const floorGlow = new PIXI.Graphics(); | |
| floorGlow.rect(0, (25 * TILE) - 4, W, 8).fill({ color: 0xffd700, alpha: 0.12 }); | |
| bgLayer.addChild(floorGlow); | |
| } | |
| // βββ v2.8 DIEGETIC SIGNS: leaderboard wall + categories chalkboard + evolution board βββ | |
| // Three large in-world panels that replace the old sidebar. Each is a PIXI.Container | |
| // with a background frame + PIXI.Text lines, positioned on empty floor/wall space. | |
| // Updated each refresh() via renderDiegetic*. | |
| const DIEGETIC = {}; | |
| // v2.9: proper pixel-art UI β titlebar stripe, 2-tone bevel, inner gradient, status LED. | |
| function makeDiegeticBoard(opts) { | |
| const { x, y, w, h, title, titleColor, maxLines, bg = 0x11151e, glyph = "β£" } = opts; | |
| const c = new PIXI.Container(); c.x = x; c.y = y; | |
| // Outer glow (neon halo) | |
| const glow = new PIXI.Graphics(); | |
| glow.rect(-2, -2, w + 4, h + 4).stroke({ width: 3, color: titleColor, alpha: 0.18 }); | |
| c.addChild(glow); | |
| // Shadow under board (drop-shadow pixel-style) | |
| const shadow = new PIXI.Graphics(); | |
| shadow.rect(3, 3, w, h).fill({ color: 0x000000, alpha: 0.55 }); | |
| c.addChild(shadow); | |
| // Body: top bevel (lighter), bottom bevel (darker) | |
| const body = new PIXI.Graphics(); | |
| body.rect(0, 0, w, h).fill({ color: bg, alpha: 0.96 }); | |
| body.rect(0, 0, w, 1).fill({ color: 0xffffff, alpha: 0.28 }); // top bevel | |
| body.rect(0, h - 1, w, 1).fill({ color: 0x000000, alpha: 0.65 }); // bottom bevel | |
| body.rect(0, 0, 1, h).fill({ color: 0xffffff, alpha: 0.15 }); // left bevel | |
| body.rect(w - 1, 0, 1, h).fill({ color: 0x000000, alpha: 0.55 }); // right bevel | |
| c.addChild(body); | |
| // Titlebar: colored stripe with glyph badge + label | |
| const tb = new PIXI.Graphics(); | |
| tb.rect(0, 0, w, 20).fill({ color: titleColor, alpha: 0.22 }); | |
| tb.rect(0, 20, w, 1).fill({ color: titleColor, alpha: 0.9 }); // neon underline | |
| tb.rect(0, 19, w, 1).fill({ color: 0x000000, alpha: 0.4 }); // shadow under | |
| // Badge (glyph square, left) | |
| tb.rect(4, 4, 14, 12).fill({ color: titleColor, alpha: 0.85 }).stroke({ width: 1, color: 0x000000, alpha: 0.5 }); | |
| c.addChild(tb); | |
| const badge = new PIXI.Text({ text: glyph, style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 10, fontWeight: "700", fill: 0x11151e, | |
| }}); | |
| badge.x = 7; badge.y = 4; | |
| c.addChild(badge); | |
| const titleTxt = new PIXI.Text({ text: title, style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 10, fontWeight: "700", | |
| fill: titleColor, letterSpacing: 2, | |
| }}); | |
| titleTxt.x = 24; titleTxt.y = 5; | |
| c.addChild(titleTxt); | |
| // Status LED (top right, pulses via tickRender) | |
| const led = new PIXI.Graphics(); | |
| led.circle(w - 10, 10, 3).fill({ color: 0x00d9a7 }); | |
| led.circle(w - 10, 10, 5).fill({ color: 0x00d9a7, alpha: 0.35 }); | |
| c.addChild(led); c._led = led; | |
| // Content area with subtle top fade | |
| const content = new PIXI.Graphics(); | |
| content.rect(2, 22, w - 4, 6).fill({ color: 0x000000, alpha: 0.35 }); // darker cap below titlebar | |
| c.addChild(content); | |
| const lines = []; | |
| for (let i = 0; i < maxLines; i++) { | |
| const t = new PIXI.Text({ text: "", style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 10, fill: 0xe6e8ee, | |
| }}); | |
| t.x = 8; t.y = 30 + i * 13; | |
| c.addChild(t); lines.push(t); | |
| } | |
| boardLayer.addChild(c); | |
| return { container: c, lines, w, h }; | |
| } | |
| // v2.10: 4 big boards removed β data now flows through the 12 Control-Room panels below. | |
| // (Old renderers check `if (!board) return`, so they silently no-op.) | |
| // Keeping DIEGETIC object for compatibility with renderers. | |
| // βββ v2.8 TICKER TAPE: scrolling live events across top of world βββ | |
| // The ticker is world-space (moves with camera) but visually anchored to y=0. | |
| const TICKER = { text: null, raw: "", speed: 0.8, x: 0 }; | |
| { | |
| const bar = new PIXI.Graphics(); | |
| bar.rect(0, 0, WORLD_W, 24).fill({ color: 0x000000, alpha: 0.82 }); | |
| bar.rect(0, 23, WORLD_W, 1).fill({ color: 0xffa500, alpha: 0.6 }); | |
| tickerLayer.addChild(bar); | |
| const label = new PIXI.Text({ text: "LIVE βΈ", style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 11, fontWeight: "700", | |
| fill: 0xff3b3b, letterSpacing: 2, | |
| }}); | |
| label.x = 8; label.y = 5; | |
| tickerLayer.addChild(label); | |
| const txt = new PIXI.Text({ text: "", style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 11, fill: 0xe6e8ee, | |
| }}); | |
| txt.x = WORLD_W; txt.y = 5; | |
| tickerLayer.addChild(txt); | |
| TICKER.text = txt; | |
| TICKER.x = WORLD_W; | |
| } | |
| // ββ AGENT SPRITES (v2.24: bubble hover/click-only β no persistent overlay) βββ | |
| function makeAgentContainer(a) { | |
| const c = new PIXI.Container(); | |
| c.eventMode = "static"; c.cursor = "pointer"; | |
| // Drop shadow | |
| const shadow = new PIXI.Graphics(); | |
| shadow.ellipse(0, CHAR_H*0.05, CHAR_W*0.3, 6).fill({ color:0x000000, alpha:0.45 }); | |
| c.addChild(shadow); | |
| // v2.0: Reputation halo (axelrod strategy color), drawn beneath the sprite | |
| const halo = new PIXI.Graphics(); | |
| c.addChild(halo); c._halo = halo; | |
| // Kind pip at feet | |
| const pip = new PIXI.Graphics(); | |
| pip.circle(0, CHAR_H*0.05, 5).fill(KIND_COLORS[a.kind]); | |
| c.addChild(pip); | |
| c._pip = pip; | |
| if (a._isInfra) { | |
| // Render as a data-center tower: stacked rack blocks, roof antenna, | |
| // indicator LEDs. Tower height grows with generation; crown glows on | |
| // fleet-best. Drawn procedurally so it updates live in refresh(). | |
| const tower = new PIXI.Graphics(); | |
| c.addChild(tower); c._tower = tower; | |
| // seed draw (empty) β real fill happens in updateInfraTower(a) each refresh | |
| tower.rect(-CHAR_W*0.22, -CHAR_H*0.72, CHAR_W*0.44, CHAR_H*0.72) | |
| .fill({ color:0x1a1f2a, alpha:0.96 }) | |
| .stroke({ width:2, color:KIND_COLORS[a.kind], alpha:0.85 }); | |
| // v2.0: Sxx/Pxx label + current Brier under the rack | |
| const iLbl = new PIXI.Text({ text: a.label, style:{ | |
| fontFamily:"IBM Plex Mono, monospace", fontSize:11, fontWeight:"700", | |
| fill: 0xffd700, letterSpacing:0.8, | |
| }}); | |
| iLbl.anchor.set(0.5, 0); iLbl.y = CHAR_H*0.08; | |
| c.addChild(iLbl); c._iLbl = iLbl; | |
| const iBr = new PIXI.Text({ text: "β", style:{ | |
| fontFamily:"IBM Plex Mono, monospace", fontSize:10, | |
| fill: 0x00d9a7, | |
| }}); | |
| iBr.anchor.set(0.5, 0); iBr.y = CHAR_H*0.08 + 14; | |
| c.addChild(iBr); c._iBr = iBr; | |
| } else { | |
| // Character sprite β BIG, with v2.0 per-trader tint | |
| const tex = TEX.chars[a.char]; | |
| if (tex) { | |
| const spr = new PIXI.Sprite(tex); | |
| // v2.24: bottom-anchor + UNIFORM scale via scale.set (not width/height | |
| // override) β preserves native 28Γ24 aspect ratio, no vertical stretch. | |
| spr.anchor.set(0.5, 1); spr.y = 0; | |
| spr.scale.set(CHAR_SCALE); | |
| if (a.tint) spr.tint = a.tint; | |
| c.addChild(spr); | |
| c._spr = spr; | |
| } else { | |
| // v2.6 (2026-04-17): procedural pixel-person body when asset missing. | |
| // Shape: head + torso (agent tint) + arms + legs + shoes, flat-shaded w/ outline. | |
| const body = new PIXI.Graphics(); | |
| const tint = a.tint || KIND_COLORS[a.kind] || 0xffa500; | |
| const hair = 0x3a2b20; // dark brown hair | |
| const skin = 0xf4c49a; // neutral skin | |
| const pants = 0x1a1f2a; // dark navy pants | |
| const shoes = 0x0b0d12; // black shoes | |
| const outline = 0x0b0d12; | |
| const W = CHAR_W, H = CHAR_H; | |
| // Legs | |
| body.rect(-W*0.22, -H*0.30, W*0.17, H*0.26).fill({ color: pants }).stroke({ width: 1, color: outline }); | |
| body.rect( W*0.05, -H*0.30, W*0.17, H*0.26).fill({ color: pants }).stroke({ width: 1, color: outline }); | |
| // Shoes | |
| body.rect(-W*0.23, -H*0.06, W*0.18, H*0.06).fill({ color: shoes }); | |
| body.rect( W*0.05, -H*0.06, W*0.18, H*0.06).fill({ color: shoes }); | |
| // Arms (tint = torso color) | |
| body.rect(-W*0.40, -H*0.58, W*0.12, H*0.26).fill({ color: tint }).stroke({ width: 1, color: outline }); | |
| body.rect( W*0.28, -H*0.58, W*0.12, H*0.26).fill({ color: tint }).stroke({ width: 1, color: outline }); | |
| // Hands | |
| body.circle(-W*0.34, -H*0.30, W*0.07).fill({ color: skin }).stroke({ width: 1, color: outline }); | |
| body.circle( W*0.34, -H*0.30, W*0.07).fill({ color: skin }).stroke({ width: 1, color: outline }); | |
| // Torso (agent tint) | |
| body.rect(-W*0.28, -H*0.62, W*0.56, H*0.34).fill({ color: tint }).stroke({ width: 1.5, color: outline }); | |
| // Neck | |
| body.rect(-W*0.06, -H*0.65, W*0.12, H*0.05).fill({ color: skin }); | |
| // Head | |
| body.circle(0, -H*0.80, W*0.20).fill({ color: skin }).stroke({ width: 1.5, color: outline }); | |
| // Hair (top swoosh) | |
| body.rect(-W*0.20, -H*0.98, W*0.40, H*0.12).fill({ color: hair }); | |
| body.circle(-W*0.15, -H*0.86, W*0.05).fill({ color: hair }); | |
| body.circle( W*0.15, -H*0.86, W*0.05).fill({ color: hair }); | |
| // Eyes | |
| body.circle(-W*0.08, -H*0.80, 1.6).fill({ color: outline }); | |
| body.circle( W*0.08, -H*0.80, 1.6).fill({ color: outline }); | |
| // Tiny smile | |
| body.rect(-W*0.05, -H*0.72, W*0.10, 1.2).fill({ color: outline }); | |
| // v2.14: per-role accessory (glasses, cap, briefcase, etc). POL prefix stripped. | |
| const roleKey = (a.id || "").replace(/^p-/, ""); | |
| const accessory = ROLE_ACCESSORIES[roleKey]; | |
| if (accessory) { | |
| try { accessory(body, W, H); } catch (_) { /* accessory draw failed β base body still valid */ } | |
| } | |
| // v2.14: POL agents get a blue sash across the torso to separate from NBA at a glance. | |
| if (a.kind === "political") { | |
| body.moveTo(-W*0.28, -H*0.45).lineTo(W*0.28, -H*0.55).lineTo(W*0.30, -H*0.50).lineTo(-W*0.26, -H*0.40).closePath() | |
| .fill({ color:0x8be9fd, alpha:0.92 }).stroke({ width:0.8, color:0x0b0d12 }); | |
| } | |
| c.addChild(body); | |
| c._spr = body; // expose as _spr so breathing/flip animations still work | |
| } | |
| // v2.0: 6x6 letter badge overlay (model family: Q/M/G/L/N/P/E/D/β¦) | |
| if (a.badge) { | |
| const bg = new PIXI.Graphics(); | |
| bg.roundRect(-12, -CHAR_H*0.78, 24, 24, 4) | |
| .fill({ color: a.tint || 0xffa500, alpha: 0.95 }) | |
| .stroke({ width: 1.5, color: 0x000000, alpha: 0.85 }); | |
| c.addChild(bg); | |
| const bTxt = new PIXI.Text({ text: a.badge, style:{ | |
| fontFamily:"IBM Plex Mono, monospace", fontSize:15, fontWeight:"900", | |
| fill:0x0b0d12, align:"center", | |
| }}); | |
| bTxt.anchor.set(0.5, 0.5); | |
| bTxt.x = 0; bTxt.y = -CHAR_H*0.78 + 12; | |
| c.addChild(bTxt); | |
| } | |
| } | |
| // State emote above head β changes with agent state (replaces static kind glyph) | |
| const glyph = new PIXI.Text({ text: KIND_GLYPHS[a.kind], style:{ fontSize:18 }}); | |
| glyph.anchor.set(0.5, 1); | |
| glyph.y = -CHAR_H*1.02; | |
| c.addChild(glyph); c._glyph = glyph; | |
| // Speech bubble β v2.24: hover/click-only (was always-on, obscured sprites). | |
| const bubble = new PIXI.Container(); | |
| bubble.y = -CHAR_H*1.18; | |
| const bubbleBg = new PIXI.Graphics(); | |
| bubble.addChild(bubbleBg); bubble._bg = bubbleBg; | |
| const bubbleText = new PIXI.Text({ text:"β¦", style:{ | |
| fontFamily:"JetBrains Mono, monospace", fontSize:11, fill:0xe6e8ee, | |
| wordWrap:true, wordWrapWidth:240, align:"left", lineHeight:14, | |
| }}); | |
| bubbleText.anchor.set(0.5, 1); | |
| bubble.addChild(bubbleText); bubble._text = bubbleText; | |
| c.addChild(bubble); c._bubble = bubble; | |
| // Nameplate | |
| const plate = new PIXI.Container(); | |
| plate.y = CHAR_H*0.14; | |
| const plateBg = new PIXI.Graphics(); | |
| plate.addChild(plateBg); plate._bg = plateBg; | |
| const plateText = new PIXI.Text({ text:a.label.slice(0,16), style:{ | |
| fontFamily:"JetBrains Mono, monospace", fontSize:11, fill:0xe6e8ee, fontWeight:"600", | |
| }}); | |
| plateText.anchor.set(0.5, 0.5); | |
| plate.addChild(plateText); plate._text = plateText; | |
| c.addChild(plate); c._plate = plate; | |
| // βββ v2.15: PokΓ©mon HP bar + persistent bet tag (above nameplate, below sprite) βββ | |
| // HP bar: Gen5-style 3-segment bar, greenβyellowβred as bankroll % of $100 start falls. | |
| // Bet tag: persistent chip showing latest active bet "MIN@NYK ml_home 15%" β visible ALWAYS. | |
| if (!a._isInfra) { | |
| const hp = new PIXI.Container(); | |
| hp.y = -CHAR_H*0.08; | |
| const hpBg = new PIXI.Graphics(); | |
| hp.addChild(hpBg); hp._bg = hpBg; | |
| const hpFill = new PIXI.Graphics(); | |
| hp.addChild(hpFill); hp._fill = hpFill; | |
| const hpLbl = new PIXI.Text({ text: "$100", style: { | |
| fontFamily: "Press Start 2P, VT323, monospace", fontSize: 10, fontWeight: "700", | |
| fill: 0xffffff, stroke: { color: 0x101820, width: 2 }, | |
| }}); | |
| hpLbl.anchor.set(0.5, 0.5); | |
| hp.addChild(hpLbl); hp._lbl = hpLbl; | |
| c.addChild(hp); c._hp = hp; | |
| // Persistent bet tag β below nameplate, pokΓ©mon-blue chip, shows latest active bet | |
| const bet = new PIXI.Container(); | |
| bet.y = CHAR_H*0.14 + 32; | |
| const betBg = new PIXI.Graphics(); | |
| bet.addChild(betBg); bet._bg = betBg; | |
| const betTxt = new PIXI.Text({ text: "", style: { | |
| fontFamily: "JetBrains Mono, monospace", fontSize: 9.5, fontWeight: "700", | |
| fill: 0xfff8c0, letterSpacing: 0.3, | |
| }}); | |
| betTxt.anchor.set(0.5, 0.5); | |
| bet.addChild(betTxt); bet._text = betTxt; | |
| bet.visible = false; | |
| c.addChild(bet); c._betTag = bet; | |
| } | |
| // Strategy badge (permanent β below nameplate, color-coded by family) | |
| const badge = new PIXI.Container(); | |
| badge.y = CHAR_H*0.14 + 18; | |
| const badgeBg = new PIXI.Graphics(); | |
| badge.addChild(badgeBg); badge._bg = badgeBg; | |
| const badgeText = new PIXI.Text({ text:"", style:{ | |
| fontFamily:"JetBrains Mono, monospace", fontSize:10, fill:0xe6e8ee, fontWeight:"600", letterSpacing:0.5, | |
| }}); | |
| badgeText.anchor.set(0.5, 0.5); | |
| badge.addChild(badgeText); badge._text = badgeText; | |
| badge.visible = false; | |
| c.addChild(badge); c._badge = badge; | |
| // Activity pulse dot (profit/loss) | |
| const dot = new PIXI.Graphics(); dot.visible = false; | |
| c.addChild(dot); c._dot = dot; | |
| c.on("pointerover", () => { a._hover = true; if (!a._clicked) showHoverCard(a); }); | |
| c.on("pointerout", () => { a._hover = false; hideHoverCard(); }); | |
| // v2.23 click handler: PokΓ©mon stop-and-talk with typewriter reveal + "!" ack. | |
| c.on("pointertap", () => { | |
| // Unclick and reset any previously-clicked agent β typewriter state cleared | |
| for (const other of AGENTS) if (other !== a) { | |
| other._clicked = false; | |
| other._typeIdx = 0; | |
| other._typeStart = 0; | |
| } | |
| a._clicked = true; | |
| a.vx = 0; a.vy = 0; // hard-zero velocity (v2.23) | |
| a.activeUntil = performance.now() + 60000; | |
| a.bubbleUntil = performance.now() + 60000; | |
| a.bubble = buildClickBubble(a); | |
| // Typewriter: start revealing 0 chars at click time; tickRender increments. | |
| a._typeIdx = 0; | |
| a._typeStart = performance.now(); | |
| // "!" exclamation ack β PokΓ©mon notice sprite above the head for 300ms. | |
| a._noticeUntil = performance.now() + 300; | |
| showInspectPanel(a); | |
| hideHoverCard(); | |
| }); | |
| a._cnt = c; | |
| agentLayer.addChild(c); | |
| return c; | |
| } | |
| for (const a of AGENTS) makeAgentContainer(a); | |
| // ββ MOTION ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // v2.23: user reported agents "se rentrent tous dedans" β raised SEP_RADIUS 4.0β5.5 | |
| // and added a HARD overlap resolution pass after velocity integration so sprites | |
| // physically cannot overlap by more than CHAR_W/2 in world units. | |
| const SEP_RADIUS = 5.5, SEP_WEIGHT = 0.14; | |
| // Hard-overlap min distance in tile units (CHAR_W is 80px / TILE 32 = 2.5 tiles). | |
| const HARD_MIN_DIST = 2.6; | |
| function tickMotion(ms) { | |
| const t = ms / 1000; | |
| for (const a of AGENTS) { | |
| // v2.22: clicked agent = PokΓ©mon-style STOP. User ask: "que l'agent s'arrΓͺte". | |
| // Zero velocity, zero wander, zero wobble β the inspect card + speech bubble | |
| // take over until the user clicks Close or clicks another agent. | |
| if (a._clicked) { a.vx = 0; a.vy = 0; continue; } | |
| let tx = a.home.x, ty = a.home.y; | |
| // v2.20 2026-04-20: user reported "agents dont move accross all the space" | |
| // β previously isAlive gating meant every idle trader sat frozen at home, | |
| // and a.zone truncated wanderTarget to a tiny band. New rule: every non-infra | |
| // agent wanders its full zone continuously; idle agents use a gentler, slower | |
| // drift so they still populate the canvas visually. Infra agents stay pinned. | |
| if (!a._isInfra) { | |
| const z = (a.zone && ZONES[a.zone]) ? ZONES[a.zone] : null; | |
| if (!a.wanderTarget || (a.wanderNext || 0) < ms) { | |
| if (z) { | |
| a.wanderTarget = { | |
| x: z.x + 1.2 + Math.random() * Math.max(2, z.w - 2.4), | |
| y: z.y + 1.2 + Math.random() * Math.max(2, z.h - 2.4), | |
| }; | |
| } else { | |
| a.wanderTarget = { | |
| x: 2 + Math.random() * (GRID_W - 4), | |
| y: 2 + Math.random() * (GRID_H - 4), | |
| }; | |
| } | |
| const idleMult = (a.state === "idle") ? 1.8 : 1.0; | |
| a.wanderNext = ms + (8000 + Math.random() * 9000) * idleMult; // 8-17s active, ~15-30s idle | |
| } | |
| tx = a.wanderTarget.x; ty = a.wanderTarget.y; | |
| } | |
| // If trader just placed a bet, override with MATCH_FLOOR for a huddle. | |
| if (!a._isInfra && a.activeUntil > ms) { | |
| tx = MATCH_FLOOR.x + ((a.idx % 5) - 2) * 0.7; | |
| ty = MATCH_FLOOR.y + (Math.floor(a.idx / 5) - 1) * 0.7; | |
| } | |
| if (a.pactPartner) { | |
| const p = AGENTS.find(x => x.id === a.pactPartner); | |
| if (p) { const m = MEETING_SPOTS[(a.idx + p.idx) % MEETING_SPOTS.length]; tx = m.x; ty = m.y; } | |
| } | |
| const wx = Math.cos(t*a.freq + a.phase) * a.wamp; | |
| const wy = Math.sin(t*a.freq*0.8 + a.phase*1.3) * a.wamp; | |
| let fx = (tx + wx - a.x) * 0.035 * a.speed; | |
| let fy = (ty + wy - a.y) * 0.035 * a.speed; | |
| for (const b of AGENTS) { | |
| if (b === a) continue; | |
| const dx = a.x - b.x, dy = a.y - b.y; | |
| const d2 = dx*dx + dy*dy; | |
| if (d2 < SEP_RADIUS*SEP_RADIUS && d2 > 0.0001) { | |
| const d = Math.sqrt(d2); | |
| const push = (SEP_RADIUS - d) / SEP_RADIUS; | |
| fx += (dx/d) * push * SEP_WEIGHT; | |
| fy += (dy/d) * push * SEP_WEIGHT; | |
| } | |
| } | |
| // v2.22: even tighter damping 0.92 β 0.955 (kills "nanosecond jumps" on throttled RAF). | |
| a.vx = a.vx * 0.955 + fx; a.vy = a.vy * 0.955 + fy; | |
| // Clamp per-tick displacement so a single long RAF-delta can't teleport the agent. | |
| const maxStep = 0.18; | |
| if (a.vx > maxStep) a.vx = maxStep; else if (a.vx < -maxStep) a.vx = -maxStep; | |
| if (a.vy > maxStep) a.vy = maxStep; else if (a.vy < -maxStep) a.vy = -maxStep; | |
| a.x += a.vx; a.y += a.vy; | |
| if (a._cnt && a._cnt._spr && Math.abs(a.vx) > 0.004) { | |
| a._cnt._spr.scale.x = (a.vx < 0 ? -1 : 1) * Math.abs(a._cnt._spr.scale.x || 1); | |
| } | |
| } | |
| // v2.23: HARD overlap pass β push any remaining overlapping pairs apart so | |
| // two sprites can never visually occupy the same tile. One pass/tick is enough | |
| // given the small population (β€60) and soft separation already reduced most | |
| // contact. Skip clicked & infra agents (they are pinned). | |
| for (let i = 0; i < AGENTS.length; i++) { | |
| const a = AGENTS[i]; | |
| if (a._clicked || a._isInfra) continue; | |
| for (let j = i + 1; j < AGENTS.length; j++) { | |
| const b = AGENTS[j]; | |
| if (b._clicked || b._isInfra) continue; | |
| const dx = a.x - b.x, dy = a.y - b.y; | |
| const d2 = dx*dx + dy*dy; | |
| if (d2 < HARD_MIN_DIST*HARD_MIN_DIST && d2 > 0.0001) { | |
| const d = Math.sqrt(d2); | |
| const push = (HARD_MIN_DIST - d) / 2; | |
| const ux = dx / d, uy = dy / d; | |
| a.x += ux * push; a.y += uy * push; | |
| b.x -= ux * push; b.y -= uy * push; | |
| } | |
| } | |
| } | |
| } | |
| function tickRender(ms) { | |
| // Camera ease | |
| camScale += (camTargetScale - camScale) * 0.12; | |
| camX += (camTargetX - camX) * 0.12; | |
| camY += (camTargetY - camY) * 0.12; | |
| world.scale.set(camScale); | |
| world.x = camX; world.y = camY; | |
| // v2.8: twinkle stars | |
| if (window._starField) { | |
| const { stars, gfx } = window._starField; | |
| gfx.clear(); | |
| for (const s of stars) { | |
| const a = 0.4 + Math.sin(ms * s.speed + s.phase) * 0.45; | |
| gfx.circle(s.x, s.y, s.r).fill({ color: 0xffffff, alpha: a }); | |
| } | |
| } | |
| // v2.8: drift clouds | |
| if (window._cloudField) { | |
| const { clouds, gfx } = window._cloudField; | |
| gfx.clear(); | |
| for (const c of clouds) { | |
| c.x += c.vx; | |
| if (c.x > WORLD_W + c.w) c.x = -c.w; | |
| gfx.ellipse(c.x, c.y, c.w * 0.6, c.h).fill({ color: 0xe6e8ee, alpha: c.alpha }); | |
| gfx.ellipse(c.x + c.w * 0.2, c.y - c.h * 0.3, c.w * 0.45, c.h * 0.9).fill({ color: 0xe6e8ee, alpha: c.alpha }); | |
| } | |
| } | |
| // v2.8: scroll ticker | |
| if (TICKER.text && TICKER.raw) { | |
| TICKER.x -= TICKER.speed; | |
| TICKER.text.x = TICKER.x; | |
| if (TICKER.x + TICKER.text.width < 60) TICKER.x = WORLD_W; | |
| } | |
| for (const a of AGENTS) { | |
| if (!a._cnt) continue; | |
| // v2.22 render-side easing: decouple visual position from sim position so | |
| // irregular RAF deltas (or a spike after a background-tab resume) never | |
| // produce a visible "nanosecond jump". _renderX/Y catch up to a.x/y at a | |
| // fixed rate every frame, yielding buttery motion regardless of sim jitter. | |
| if (a._renderX == null) { a._renderX = a.x; a._renderY = a.y; } | |
| const easeK = a._clicked ? 0.0 : 0.18; // clicked agents are fully frozen | |
| a._renderX += (a.x - a._renderX) * easeK; | |
| a._renderY += (a.y - a._renderY) * easeK; | |
| a._cnt.x = a._renderX * TILE; a._cnt.y = a._renderY * TILE; | |
| a._cnt.zIndex = a._renderY * 1000; | |
| // Dim agents not in active room (highlight current room) | |
| if (activeRoom !== "world" && a.zone !== activeRoom) { a._cnt.alpha = 0.22; } | |
| else { a._cnt.alpha = 1.0; } | |
| // v2.7: Walking animation β bob, tilt, and step when moving | |
| if (!a._isInfra) { | |
| const speed = Math.sqrt((a.vx||0)**2 + (a.vy||0)**2); | |
| const isWalking = speed > 0.008; | |
| const spr = a._cnt._spr || a._cnt._body; | |
| if (spr) { | |
| if (isWalking) { | |
| const stepFreq = 220; | |
| const bob = Math.sin(ms / stepFreq + a.phase * 5) * 2.5; | |
| const tilt = Math.sin(ms / stepFreq + a.phase * 5) * 0.06; | |
| spr.y = bob; | |
| spr.rotation = tilt; | |
| spr.scale.y = 1.0 + Math.abs(Math.sin(ms / stepFreq + a.phase * 5)) * 0.04; | |
| } else { | |
| const breathe = 1.0 + Math.sin(ms / 800 + a.phase * 3) * 0.025; | |
| spr.y = spr.y * 0.9; | |
| spr.rotation = spr.rotation * 0.85; | |
| spr.scale.y = breathe; | |
| } | |
| } | |
| } | |
| // v2.3: State emote β dynamic icon above head based on agent state | |
| if (a._cnt._glyph && !a._isInfra) { | |
| const EMOTES = { profit:"π°", loss:"π", error:"β οΈ", thinking:"π€", idle:"π€" }; | |
| const em = EMOTES[a.state] || EMOTES.idle; | |
| if (a._cnt._glyph.text !== em) a._cnt._glyph.text = em; | |
| a._cnt._glyph.y = -CHAR_H*1.02 + Math.sin(ms / 600 + a.idx) * 2; | |
| } | |
| // v2.23 Speech bubble β Bloomberg-dark fill, teal border, typewriter reveal | |
| // when clicked. Palette matches #inspect-card and #hover exactly. | |
| const bubble = a._cnt._bubble; | |
| const isClicked = !!a._clicked; | |
| if (isClicked) { | |
| a.bubble = buildClickBubble(a); // keep fresh | |
| } | |
| const showBubble = bubble && a.bubble && (isClicked || (a.bubbleUntil || 0) > ms); | |
| if (showBubble) { | |
| // Typewriter char count β only applies while clicked; ephemeral post-bet | |
| // shows full text immediately (it's already a quick flash). | |
| let display = a.bubble; | |
| let typingDone = true; | |
| if (isClicked) { | |
| const TYPE_MS = 40; | |
| const n = Math.max(0, Math.floor((ms - (a._typeStart || ms)) / TYPE_MS)); | |
| const full = a.bubble; | |
| typingDone = n >= full.length; | |
| const shown = Math.min(n, full.length); | |
| display = full.slice(0, shown); | |
| a._typeIdx = shown; | |
| } | |
| // Blinking βΌ glyph once typing completes | |
| const showCursor = isClicked && typingDone && Math.floor(ms / 500) % 2 === 0; | |
| bubble._text.text = display + (showCursor ? " βΌ" : ""); | |
| bubble._text.style.wordWrapWidth = isClicked ? 240 : 180; | |
| bubble._text.style.fontSize = 11; | |
| bubble._text.style.align = "left"; | |
| const tw = Math.max(60, Math.min(bubble._text.width + 18, isClicked ? 260 : 200)); | |
| const th = bubble._text.height + 14; | |
| const fade = isClicked ? 1.0 : Math.min(1, ((a.bubbleUntil || 0) - ms) / 500); | |
| const brdW = 2; | |
| bubble._bg.clear() | |
| .rect(-tw/2, -th, tw, th).fill({ color: 0x0a1520, alpha: 0.96 * fade }) | |
| .rect(-tw/2, -th, tw, th).stroke({ width:brdW, color: 0x5eead4, alpha: fade }) | |
| // Tail pointing DOWN toward sprite β crisp 8px triangle | |
| .moveTo(-6, 0).lineTo(6, 0).lineTo(0, 8).closePath().fill({ color: 0x0a1520, alpha: 0.96 * fade }) | |
| .moveTo(-6, 0).lineTo(0, 8).lineTo(6, 0).stroke({ width:brdW, color: 0x5eead4, alpha: fade }); | |
| bubble._text.style.fill = 0xe6e8ee; | |
| bubble._text.alpha = fade; | |
| bubble.visible = true; | |
| } else if (bubble) { | |
| bubble.visible = false; | |
| } | |
| // v2.23 "!" tap-ack β brief exclamation mark above the head for 300ms after | |
| // a click lands. Gives PokΓ©mon-style feedback that the tap was registered. | |
| if (a._noticeUntil && a._noticeUntil > ms) { | |
| if (!a._cnt._notice) { | |
| const notice = new PIXI.Text({ text: "!", style: { | |
| fontFamily: "Press Start 2P, VT323, monospace", fontSize: 20, | |
| fontWeight: "700", fill: 0xfbbf24, stroke: { color: 0x101820, width: 3 }, | |
| }}); | |
| notice.anchor.set(0.5, 1); | |
| notice.y = -CHAR_H * 1.22; | |
| a._cnt.addChild(notice); | |
| a._cnt._notice = notice; | |
| } | |
| const nLeft = (a._noticeUntil - ms) / 300; | |
| a._cnt._notice.alpha = nLeft; | |
| a._cnt._notice.y = -CHAR_H * 1.22 - (1 - nLeft) * 12; | |
| a._cnt._notice.visible = true; | |
| } else if (a._cnt._notice) { | |
| a._cnt._notice.visible = false; | |
| } | |
| // v2.22: Names HIDDEN by default β only shown when the agent is clicked | |
| // (or infra, which always needs its Sxx/Pxx label, handled separately). | |
| // User ask: "avec leur nom seulement affichΓ© quand je clique dessus". | |
| if (a._cnt._plate && !a._isInfra) { | |
| const showPlate = !!a._clicked; | |
| a._cnt._plate.visible = showPlate; | |
| if (showPlate) { | |
| const tw = a._cnt._plate._text.width + 14; | |
| let plateColor = KIND_COLORS[a.kind]; | |
| let plateBorderW = 2; | |
| if (a.data && typeof a.data.bankroll === "number") { | |
| const roi = a.data.bankroll - 100; | |
| plateColor = roi > 10 ? 0x00d9a7 : roi > 0 ? 0x50fa7b : roi > -10 ? 0xffb020 : 0xff5c5c; | |
| plateBorderW = roi > 20 || roi < -20 ? 3 : 2; | |
| } | |
| a._cnt._plate._bg.clear() | |
| .rect(-tw/2, -9, tw, 18).fill({ color:0x0b0d12, alpha:0.96 }) | |
| .rect(-tw/2, -9, tw, 18).stroke({ width:plateBorderW, color:plateColor, alpha:0.95 }); | |
| } | |
| } | |
| // v2.19 (2026-04-20): DRASTIC panel surgery β strategy badge hidden by default | |
| // to declutter the world. User reported agent bubbles invisible behind panel | |
| // superposition. Badge is still visible on hover (via hover-card) and in the | |
| // full inspect modal on click, so no information is lost. | |
| if (a._cnt._badge) { | |
| const isActive = (a.activeUntil || 0) > ms; | |
| const showBadge = a.strategy && (a._hover || isActive); | |
| if (showBadge) { | |
| const fam = STRATEGY_FAMILIES[a.strategy] || "unknown"; | |
| const col = FAMILY_COLORS[fam]; | |
| a._cnt._badge._text.text = a.strategy.length > 14 ? a.strategy.slice(0,14) : a.strategy; | |
| const bw = a._cnt._badge._text.width + 10; | |
| a._cnt._badge._bg.clear() | |
| .rect(-bw/2, -6, bw, 12).fill({ color: col, alpha: 0.2 }) | |
| .rect(-bw/2, -6, bw, 12).stroke({ width: 1, color: col, alpha: 0.9 }); | |
| a._cnt._badge.visible = true; | |
| } else { | |
| a._cnt._badge.visible = false; | |
| } | |
| } | |
| // Pip state color | |
| const pipCol = a.state === "profit" ? 0x00d9a7 : a.state === "loss" ? 0xff5c5c : a.state === "error" ? 0xff5c5c : KIND_COLORS[a.kind]; | |
| a._cnt._pip.clear().circle(0, CHAR_H*0.05, 5).fill(pipCol); | |
| // βββ v2.19 (2026-04-20): HP bar slimmed + hidden when idle βββ | |
| // Was always-on for 41 agents simultaneously β created the superposition the | |
| // user flagged. Now shows only when agent is alive (profit/loss/thinking) OR | |
| // under hover, and is 60% shorter. Inspect modal still shows full bankroll. | |
| if (a._cnt._hp && a.data && typeof a.data.bankroll === "number" && (a.state !== "idle" || a._hover || (a.activeUntil || 0) > ms)) { | |
| const HP_W = CHAR_W * 0.7, HP_H = 5; | |
| const bankroll = a.data.bankroll || 0; | |
| const maxRef = Math.max(200, bankroll); | |
| const pct = Math.max(0.02, Math.min(1, bankroll / maxRef)); | |
| const hpPct = Math.min(1, bankroll / 100); | |
| const hpCol = hpPct > 1 ? 0x1aa030 : hpPct > 0.5 ? 0x30c030 : hpPct > 0.2 ? 0xf0c010 : 0xd02020; | |
| a._cnt._hp._bg.clear() | |
| .rect(-HP_W/2 - 1, -HP_H/2 - 1, HP_W + 2, HP_H + 2).fill({ color: 0x101820 }) | |
| .rect(-HP_W/2, -HP_H/2, HP_W, HP_H).fill({ color: 0x404850 }); | |
| a._cnt._hp._fill.clear() | |
| .rect(-HP_W/2, -HP_H/2, HP_W * pct, HP_H).fill({ color: hpCol }) | |
| .rect(-HP_W/2, -HP_H/2, HP_W * pct, 2).fill({ color: 0xffffff, alpha: 0.4 }); | |
| // Hide the numeric label unless hovered β reduce text density | |
| if (a._cnt._hp._lbl) { | |
| a._cnt._hp._lbl.text = bankroll >= 1000 ? `$${(bankroll/1000).toFixed(2)}K` : `$${bankroll.toFixed(0)}`; | |
| a._cnt._hp._lbl.y = -HP_H - 6; | |
| a._cnt._hp._lbl.visible = !!a._hover; | |
| } | |
| a._cnt._hp.visible = true; | |
| } else if (a._cnt._hp) { | |
| a._cnt._hp.visible = false; | |
| } | |
| // βββ v2.19: Persistent bet tag β hidden by default. The tag is | |
| // redundant with the speech bubble (shows allocation when trade happens) | |
| // and the inspect card (click = full allocation list). Now shows only on | |
| // hover or while the bet is active (activeUntil window). Drastically reduces | |
| // superposition in crowded zones. | |
| if (a._cnt._betTag && a.dayDecision && Array.isArray(a.dayDecision.allocations) && a.dayDecision.allocations.length && | |
| (a._hover || (a.activeUntil || 0) > ms)) { | |
| const al = a.dayDecision.allocations.reduce((m, x) => | |
| ((x.stake || x.stake_amount || 0) > (m.stake || m.stake_amount || 0)) ? x : m, a.dayDecision.allocations[0]); | |
| const pct = Math.round((al.pct || 0) * 100); | |
| const game = (al.game || "").slice(0, 10); | |
| const cat = (al.category || "").slice(0, 9); | |
| const won = al.won === true ? "β" : al.won === false ? "β" : "Β·"; | |
| const txt = `${won} ${game} ${cat} ${pct}%`.trim(); | |
| a._cnt._betTag._text.text = txt; | |
| const tw = Math.min(a._cnt._betTag._text.width + 12, 180); | |
| const betCol = al.won === true ? 0x207838 : al.won === false ? 0xb82030 : 0x4060b0; | |
| a._cnt._betTag._bg.clear() | |
| .roundRect(-tw/2 - 1, -7 - 1, tw + 2, 16, 4).fill({ color: 0x101820 }) | |
| .roundRect(-tw/2, -7, tw, 14, 4).fill({ color: betCol }) | |
| .roundRect(-tw/2, -7, tw, 5, 4).fill({ color: 0xffffff, alpha: 0.18 }); | |
| a._cnt._betTag.visible = true; | |
| } else if (a._cnt._betTag) { | |
| a._cnt._betTag.visible = false; | |
| } | |
| // v2.0 reputation halo β 3px ring underfoot, color = axelrod family, | |
| // pulses when a bet was just placed (activeUntil in future) | |
| if (a._cnt._halo && !a._isInfra) { | |
| const h = a._cnt._halo; h.clear(); | |
| const fam = STRATEGY_FAMILIES[a.strategy] || "unknown"; | |
| const haloCol = FAMILY_COLORS[fam]; | |
| const pulsing = a.activeUntil > ms; | |
| const pulseBoost = pulsing ? (0.6 + 0.4 * Math.sin(ms/180)) : 0.35; | |
| h.ellipse(0, CHAR_H*0.06, CHAR_W*0.34, 6) | |
| .stroke({ width:3, color: haloCol, alpha: pulseBoost }); | |
| if (pulsing) { | |
| h.ellipse(0, CHAR_H*0.06, CHAR_W*0.42, 8) | |
| .stroke({ width:1, color: haloCol, alpha: 0.22 }); | |
| } | |
| } | |
| // v2.0 infra tower label refresh | |
| if (a._isInfra && a._cnt._iBr && a.data) { | |
| const b = (a.data.best_brier || 0); | |
| a._cnt._iBr.text = b > 0 ? b.toFixed(5) : "β"; | |
| const isBest = b > 0 && b <= _fleetBestBrier + 0.00001; | |
| a._cnt._iBr.style.fill = isBest ? 0xffd700 : (a.data.stagnation > 100 ? 0xff5c5c : 0x00d9a7); | |
| } | |
| // Pulse dot + floating particles | |
| if (a.state === "profit" || a.state === "loss") { | |
| const pulse = (Math.sin(ms/350 + a.idx) + 1) / 2; | |
| a._cnt._dot.visible = true; | |
| a._cnt._dot.clear().circle(CHAR_W*0.3, -CHAR_H*0.7, 4).fill({ color:pipCol, alpha:0.45+0.55*pulse }); | |
| if (!a._particles) a._particles = []; | |
| if (a._particles.length < 4 && Math.random() < 0.03) { | |
| const pc = new PIXI.Graphics(); | |
| const col = a.state === "profit" ? 0x00d9a7 : 0xff5c5c; | |
| pc.circle(0, 0, 2).fill({ color: col, alpha: 0.9 }); | |
| pc.x = (Math.random()-0.5) * CHAR_W; pc.y = -CHAR_H*0.5; | |
| pc._life = 60; pc._vy = -0.8 - Math.random()*0.5; | |
| a._cnt.addChild(pc); a._particles.push(pc); | |
| } | |
| for (let i = a._particles.length - 1; i >= 0; i--) { | |
| const pc = a._particles[i]; pc.y += pc._vy; pc._life--; | |
| pc.alpha = pc._life / 60; | |
| if (pc._life <= 0) { a._cnt.removeChild(pc); a._particles.splice(i, 1); } | |
| } | |
| } else { | |
| a._cnt._dot.visible = false; | |
| if (a._particles) { a._particles.forEach(pc => a._cnt.removeChild(pc)); a._particles = []; } | |
| } | |
| } | |
| pactLayer.clear(); | |
| // Outer glow pass β wide & dim, pulses with time | |
| const _pactPulse = 0.5 + 0.4 * Math.sin(performance.now() / 520); | |
| for (const a of AGENTS) { | |
| if (!a.pactPartner) continue; | |
| const p = AGENTS.find(x => x.id === a.pactPartner); | |
| if (!p || a.id > p.id) continue; | |
| pactLayer.moveTo(a.x*TILE, a.y*TILE).lineTo(p.x*TILE, p.y*TILE); | |
| } | |
| pactLayer.stroke({ width:6, color:0x00d9a7, alpha:0.14 * _pactPulse }); | |
| // Inner bright pass β crisp coalition wire | |
| for (const a of AGENTS) { | |
| if (!a.pactPartner) continue; | |
| const p = AGENTS.find(x => x.id === a.pactPartner); | |
| if (!p || a.id > p.id) continue; | |
| pactLayer.moveTo(a.x*TILE, a.y*TILE).lineTo(p.x*TILE, p.y*TILE); | |
| } | |
| pactLayer.stroke({ width:1.5, color:0x50fa7b, alpha:0.55 + 0.35 * _pactPulse }); | |
| } | |
| app.ticker.add(() => { | |
| const now = performance.now(); | |
| tickMotion(now); tickRender(now); tickPokeDialog(now); | |
| // v2.3: Animate ambient particles | |
| for (const p of PARTICLES) { | |
| p.x += p._vx + Math.sin(now / 2000 + p._drift) * 0.15 * p._speed; | |
| p.y += p._vy + Math.cos(now / 2500 + p._drift) * 0.1 * p._speed; | |
| if (p.x < 0) p.x = WORLD_W; if (p.x > WORLD_W) p.x = 0; | |
| if (p.y < 0) p.y = WORLD_H; if (p.y > WORLD_H) p.y = 0; | |
| p.alpha = 0.25 + 0.25 * Math.sin(now / 1200 + p._drift); | |
| } | |
| // v2.3: Animate pulse rings | |
| for (let i = PULSE_RINGS.length - 1; i >= 0; i--) { | |
| const ring = PULSE_RINGS[i]; | |
| ring._r += 1.2; ring._alpha -= 0.018; | |
| ring.clear().circle(ring._cx, ring._cy, ring._r) | |
| .stroke({ width: 2, color: ring._color, alpha: Math.max(0, ring._alpha) }); | |
| if (ring._alpha <= 0 || ring._r > ring._maxR) { | |
| particleLayer.removeChild(ring); ring.destroy(); | |
| PULSE_RINGS.splice(i, 1); | |
| } | |
| } | |
| }); | |
| // ββ BUBBLE TEXT BUILDERS ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // v2.23: Human-formatted PokΓ©mon-style dialogue bubble. No JSON dumps, no raw | |
| // archetype tags. Line 1 = "ClassName Β· tid", line 2 = action in plain English. | |
| // Previous version leaked "ARCHETYPE[scalp]:", category codes, and empty braces. | |
| function buildClickBubble(a) { | |
| const d = a.data || {}; | |
| const dd = a.dayDecision; | |
| // Line 1 β persona: "Scalper Β· scalper-1" or "Fundamentalist Β· qwen-quant" | |
| const persona = a.persona || a.role || a.strategy || (a.kind === "nba" ? "NBA Trader" : a.kind === "political" ? "Political Trader" : a.kind === "itf" ? "Intraday Trader" : a.kind === "island" ? "Island" : a.kind === "crew" ? "Claude Crew" : a.kind === "council" ? "Council" : a._isInfra ? "Infrastructure" : "Agent"); | |
| const ln1 = `${persona} Β· ${a.id || a.label}`; | |
| // Line 2 β current decision, human-formatted | |
| let ln2 = ""; | |
| if (a.kind === "island" || a._isInfra) { | |
| const brier = (d.best_brier || 0); | |
| const gen = d.generation || 0; | |
| const model = d.best_model_type || "?"; | |
| ln2 = brier > 0 ? `${model.toUpperCase()} Β· Brier ${brier.toFixed(5)} Β· gen ${gen}` : "bootingβ¦"; | |
| } else if (a.kind === "crew") { | |
| const last = a._lastDispatch ? ` Β· dispatched ${a._lastDispatch}` : ""; | |
| ln2 = `${(a.role || "sub-agent").replace(/\s+/g, " ").slice(0, 60)}${last}`; | |
| } else if (a.kind === "council") { | |
| ln2 = (a.mission || "Karpathy loop").replace(/\s+/g, " ").slice(0, 70); | |
| } else if (dd && dd.allocations && dd.allocations.length) { | |
| // TF agent with a concrete decision today. Prefer top allocation. | |
| const al = dd.allocations[0]; | |
| const ticker = (al.game || al.category || "?").toString().toUpperCase().slice(0, 12); | |
| const side = (al.side || al.pick || "").toString().toUpperCase(); | |
| const pct = Math.round((al.pct || 0) * 100); | |
| const entry = al.entry_price != null ? ` @ $${Number(al.entry_price).toFixed(2)}` : ""; | |
| const edge = al.edge != null ? ` Β· edge ${(al.edge*100).toFixed(1)}%` : ""; | |
| ln2 = `${side ? side + " " : ""}${ticker}${entry} Β· ${pct}% stake${edge}`; | |
| } else if (dd && dd.action === "pass") { | |
| ln2 = "STANDING DOWN β flat tape"; | |
| } else if (a.kind === "itf" && d.open_positions != null) { | |
| const pos = d.open_positions | 0; | |
| const tr = d.trades | 0; | |
| ln2 = `${pos} open Β· ${tr} trades today`; | |
| } else if (typeof d.bankroll === "number") { | |
| const roi = d.bankroll - 100; | |
| const sign = roi >= 0 ? `+$${roi.toFixed(0)}` : `-$${(-roi).toFixed(0)}`; | |
| const w = d.wins || 0, l = d.losses || 0; | |
| ln2 = `$${d.bankroll.toFixed(0)} (${sign}) Β· ${w}W/${l}L`; | |
| } else { | |
| ln2 = "THINKINGβ¦"; | |
| } | |
| // Trim to max 60 chars per line so bubble width is predictable. | |
| return ln1.slice(0, 60) + "\n" + ln2.slice(0, 80); | |
| } | |
| function buildTraderBubble(a) { | |
| const d = a.data; | |
| const stratShort = a.strategy ? `[${a.strategy.slice(0,10)}]` : ""; | |
| if (!d) return `${a.label}\nwaitingβ¦`; | |
| const br = (d.bankroll || 0).toFixed(0); | |
| const roi = (d.bankroll || 100) - 100; | |
| const roiTxt = roi >= 0 ? `+$${roi.toFixed(0)}` : `-$${(-roi).toFixed(0)}`; | |
| if (d.llm_ok < d.llm_calls) return `${stratShort} β err\n$${br}`; | |
| const dd = a.dayDecision; | |
| if (dd && dd.day_strategy) { | |
| let think = dd.day_strategy.replace(/^(ARCHETYPE\[[^\]]+\]:|CHALLENGE_RESPONSE:)\s*/, "").trim(); | |
| if (think.length > 60) think = think.slice(0, 57) + "β¦"; | |
| const allocs = dd.allocations || []; | |
| const nBets = allocs.length ? `${allocs.length}bet` : "pass"; | |
| return `${stratShort} ${nBets}\n"${think}"\n$${br} (${roiTxt})`; | |
| } | |
| const bets = d.total_bets || 0; | |
| if (bets === 0) return `${stratShort}\nobserving⦠$${br}`; | |
| const wl = `${d.wins||0}WΒ·${d.losses||0}L`; | |
| return `${stratShort} ${wl}\n$${br} (${roiTxt})`; | |
| } | |
| function buildIslandBubble(a) { | |
| const d = a.data; | |
| if (!d) return `${a.label}\nwaitingβ¦`; | |
| const b = (d.best_brier || 0).toFixed(5); | |
| return `${a.label} Β· ${d.best_model_type || "?"}\ngen ${d.generation||0} Β· Brier ${b}\npop ${d.pop_size||"?"} Β· stag ${d.stagnation||0}`; | |
| } | |
| function buildCouncilBubble(a) { | |
| return `${a.label}\n${a.mission}`; | |
| } | |
| // ββ HOVER CARD ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const hoverEl = document.getElementById("hover"); | |
| function showHoverCard(a) { | |
| const pt = a._cnt.getGlobalPosition(); | |
| const kcol = KIND_COLORS_HEX[a.kind]; | |
| let rows = "", tags = "", note = ""; | |
| if (a.data && (a.kind === "nba" || a.kind === "political")) { | |
| const d = a.data; | |
| const roi = +(((d.bankroll || 100) - 100)).toFixed(2); | |
| const roiCls = roi >= 0 ? "up" : "down"; | |
| rows += `<div class="h-row"><span class="l">Bankroll</span><span class="v ${roiCls}">$${(d.bankroll||0).toFixed(2)}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Record</span><span class="v">${d.wins||0}W Β· ${d.losses||0}L Β· ${d.total_bets||0} bets</span></div>`; | |
| rows += `<div class="h-row"><span class="l">LLM health</span><span class="v">${d.llm_ok||0}/${d.llm_calls||0}</span></div>`; | |
| if (d.max_drawdown != null) rows += `<div class="h-row"><span class="l">Max DD</span><span class="v">${(d.max_drawdown||0).toFixed(1)}%</span></div>`; | |
| if (d.days_traded != null) rows += `<div class="h-row"><span class="l">Days traded</span><span class="v">${d.days_traded}</span></div>`; | |
| if (a.strategy) tags += `<span class="h-tag">${a.strategy}</span>`; | |
| if (a.sacrificial) tags += `<span class="h-tag sac">β ${a.sacrificial}</span>`; | |
| if (a.pactPartner) { | |
| const p = AGENTS.find(x => x.id === a.pactPartner); | |
| tags += `<span class="h-tag pact">β pact with ${p ? p.label : "?"}</span>`; | |
| } | |
| // Rich reasoning block from /api/day-decisions | |
| const dd = a.dayDecision; | |
| if (dd) { | |
| let strat = (dd.day_strategy || "").replace(/^(ARCHETYPE\[[^\]]+\]:|CHALLENGE_RESPONSE:)\s*/, ""); | |
| note = `<b style="color:var(--ink)">Today (${dd.date||""}):</b><br/>${escapeHtml(strat)}`; | |
| const allocs = (dd.allocations || []).slice(0, 3); | |
| if (allocs.length) { | |
| note += `<div style="margin-top:5px; padding-top:5px; border-top:1px dotted var(--line);">`; | |
| for (const al of allocs) { | |
| const wcls = al.won === true ? "up" : al.won === false ? "down" : ""; | |
| const sign = al.profit == null ? "" : (al.profit >= 0 ? `+$${al.profit.toFixed(2)}` : `-$${(-al.profit).toFixed(2)}`); | |
| note += `<div style="font-size:9.5px; margin:2px 0;"><span style="color:var(--warn)">${escapeHtml(al.game||"")}</span> Β· <span style="color:var(--ink)">${escapeHtml(al.category||"")}</span> Β· <span class="${wcls}" style="font-weight:600">${sign}</span><br/><span style="color:var(--ink-3)">${escapeHtml((al.rationale||"").slice(0,110))}</span></div>`; | |
| } | |
| note += `</div>`; | |
| } | |
| if (dd.cash_held_pct > 0 && dd.cash_rationale) note += `<div style="margin-top:5px; color:var(--ink-3); font-size:9.5px;">cash ${(dd.cash_held_pct*100).toFixed(0)}%: ${escapeHtml(dd.cash_rationale.slice(0,80))}</div>`; | |
| } else if (a.recent) note = `Last: ${a.recent}`; | |
| } else if (a.data && a._isInfra) { | |
| const d = a.data; | |
| rows += `<div class="h-row"><span class="l">Best Brier</span><span class="v up">${(d.best_brier||0).toFixed(5)}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Generation</span><span class="v">${d.generation||0}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Model</span><span class="v">${d.best_model_type || "β"}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Population</span><span class="v">${d.pop_size||0}</span></div>`; | |
| if (d.stagnation != null) rows += `<div class="h-row"><span class="l">Stagnation</span><span class="v">${d.stagnation} gens</span></div>`; | |
| if (d.mutation_rate != null) rows += `<div class="h-row"><span class="l">Mutation</span><span class="v">${(d.mutation_rate*100).toFixed(1)}%</span></div>`; | |
| } else if (a.kind === "crew") { | |
| rows += `<div class="h-row"><span class="l">Codename</span><span class="v">${escapeHtml(a.label||"")}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Role</span><span class="v">${escapeHtml(a.role||"sub-agent")}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Track</span><span class="v">${escapeHtml(a.track||"β")}</span></div>`; | |
| rows += `<div class="h-row"><span class="l">Status</span><span class="v ${a.state==="running"?"up":""}">${a.state||"idle"}</span></div>`; | |
| if (a._lastDispatch) rows += `<div class="h-row"><span class="l">Last dispatch</span><span class="v">${escapeHtml(a._lastDispatch)}</span></div>`; | |
| note = a.mission || a.role || ""; | |
| } else if (a.kind === "council") { | |
| note = a.mission || ""; rows += `<div class="h-row"><span class="l">Mode</span><span class="v">Karpathy loop Β· 30min</span></div>`; | |
| } else { | |
| rows += `<div class="h-row"><span class="l">Status</span><span class="v">waiting for live dataβ¦</span></div>`; | |
| } | |
| hoverEl.innerHTML = ` | |
| <div class="h-head"> | |
| <span class="h-dot" style="background:${kcol}"></span> | |
| <span class="h-title">${a.label}</span> | |
| <span class="h-sub">${a.kind}</span> | |
| </div>${rows} | |
| ${tags ? `<div style="margin-top:6px">${tags}</div>` : ""} | |
| ${note ? `<div class="h-note">${note}</div>` : ""}`; | |
| hoverEl.style.display = "block"; | |
| hoverEl.style.left = Math.min(pt.x + 20, stageEl.clientWidth - 320) + "px"; | |
| hoverEl.style.top = Math.max(pt.y - 40, 20) + "px"; | |
| } | |
| function hideHoverCard() { hoverEl.style.display = "none"; } | |
| // ββ INSPECT CARD (v2.8: floating overlay, positioned next to the clicked agent) ββ | |
| const inspectCard = document.getElementById("inspect-card"); | |
| let _inspectAgent = null; | |
| document.getElementById("inspect-close").addEventListener("click", () => { | |
| inspectCard.style.display = "none"; | |
| if (_inspectAgent) _inspectAgent._clicked = false; | |
| _inspectAgent = null; | |
| }); | |
| // v2.23: clicking empty canvas closes inspect + resumes motion on all agents. | |
| // Typewriter state cleared too so a re-click starts fresh. | |
| app.canvas.addEventListener("click", (ev) => { | |
| if (ev.target !== app.canvas) return; | |
| // Defer to next tick so a sprite's pointertap (fires ~same frame) wins the race. | |
| setTimeout(() => { | |
| // Only clear if no agent actually got clicked this frame (sprite tap would | |
| // have set exactly one a._clicked=true within the last few ms). | |
| const anyRecentClick = AGENTS.some(x => x._clicked && (performance.now() - (x._typeStart || 0)) < 30); | |
| if (anyRecentClick) return; | |
| for (const a of AGENTS) { | |
| a._clicked = false; | |
| a._typeIdx = 0; | |
| a._typeStart = 0; | |
| } | |
| inspectCard.style.display = "none"; | |
| _inspectAgent = null; | |
| }, 0); | |
| }); | |
| function positionInspectCard() { | |
| // v2.23: inspect-card is now FIXED top-right via CSS so it never overlaps the | |
| // speech bubble or sprite. No per-frame positioning needed. Kept as a no-op to | |
| // preserve the app.ticker.add() call below without a runtime error. | |
| } | |
| function showInspectPanel(a) { | |
| _inspectAgent = a; | |
| inspectCard.style.display = "block"; | |
| const d = a.data || {}; | |
| const setv = (id, text, cls) => { | |
| const el = document.getElementById(id); | |
| el.textContent = text; | |
| if (cls) el.className = "v " + cls; | |
| }; | |
| setv("inspect-name", a.label || a.id); | |
| setv("inspect-model", a.model || d.model || "β", "ok"); | |
| setv("inspect-provider", a.provider || d.provider || "β", "warn"); | |
| const bankroll = d.bankroll != null ? `$${Number(d.bankroll).toFixed(2)}` : "β"; | |
| const roi = d.bankroll != null ? `${((d.bankroll - 100) / 100 * 100).toFixed(1)}%` : "β"; | |
| const cls = d.bankroll > 100 ? "ok" : d.bankroll != null ? "warn" : ""; | |
| setv("inspect-bankroll", bankroll, cls); | |
| setv("inspect-roi", roi, cls); | |
| setv("inspect-wr", d.win_rate != null ? `${(d.win_rate * 100).toFixed(0)}%` : d.wins != null ? `${d.wins}W/${d.losses||0}L` : "β"); | |
| setv("inspect-strat", a.strategy || "β", "warn"); | |
| setv("inspect-state", a.state || "idle"); | |
| setv("inspect-brier", d.best_brier != null ? d.best_brier.toFixed(5) : "β"); | |
| setv("inspect-gen", d.generation != null ? `Gen ${d.generation}` : "β", "warn"); | |
| const extra = []; | |
| if (d.total_bets) extra.push(`${d.total_bets} bets`); | |
| if (d.last_bet) extra.push(`Last: ${d.last_bet}`); | |
| // v2.17 ITF-specific extras β no bankroll, so show trades/decisions/passes/open positions. | |
| if (a.kind === "itf") { | |
| if (d.trades != null) extra.push(`${d.trades} trades`); | |
| if (d.passes != null) extra.push(`${d.passes} passes`); | |
| if (d.open_positions != null) extra.push(`${d.open_positions} open`); | |
| if (d.tier) extra.push(`tier ${d.tier}`); | |
| if (d.decisions != null) extra.push(`${d.decisions} decisions`); | |
| } | |
| if (a.pactPartner) extra.push(`Pact: ${a.pactPartner}`); | |
| if (d.stagnation) extra.push(`Stag: ${d.stagnation}`); | |
| if (a.kind) extra.push(a.kind); | |
| document.getElementById("inspect-extra").textContent = extra.join(" Β· ") || "No additional data"; | |
| positionInspectCard(); | |
| } | |
| // Keep the card anchored to the agent as the camera moves | |
| app.ticker.add(positionInspectCard); | |
| // ββ DIEGETIC PANEL HELPERS (v2.8: feed in-world PIXI signs, not DOM) βββββββββ | |
| // Rolling ticker queue β last 40 events, composed into a single scrolling string | |
| const TICKER_EVENTS = []; | |
| let tradesCounter = 0; | |
| function logTrade(kind, agent, msg, roi) { | |
| const t = new Date().toISOString().slice(11, 19); | |
| const roiStr = roi == null ? "" : (roi >= 0 ? ` +$${roi.toFixed(1)}` : ` -$${(-roi).toFixed(1)}`); | |
| TICKER_EVENTS.unshift(`${t} Β· ${agent} Β· ${msg}${roiStr}`); | |
| if (TICKER_EVENTS.length > 40) TICKER_EVENTS.pop(); | |
| TICKER.raw = " β " + TICKER_EVENTS.join(" β ") + " β "; | |
| if (TICKER.text) TICKER.text.text = TICKER.raw; | |
| tradesCounter++; | |
| } | |
| // Redraw an infrastructure tower sprite from live island data. | |
| // Height ramps with log(generation). Color hue by best_brier. Crown on | |
| // fleet-best. Stagnation (>100 gens) dims the body. | |
| let _fleetBestBrier = 1.0; | |
| function updateInfraTower(a) { | |
| if (!a._isInfra || !a._cnt || !a._cnt._tower) return; | |
| const g = a._cnt._tower; | |
| const d = a.data || {}; | |
| const gen = d.generation || d.gen || d.current_gen || 0; | |
| const brier = d.best_brier || d.brier || 1.0; | |
| const stag = d.stagnation || 0; | |
| const tint = KIND_COLORS[a.kind] || 0x50fa7b; | |
| // Height: 0.3..1.0 of CHAR_H based on log10(gen+1)/4 clamped. | |
| const hFrac = Math.min(1.0, Math.max(0.32, Math.log10(gen + 1) / 4)); | |
| const H = CHAR_H * hFrac; | |
| const W = CHAR_W * 0.46; | |
| const nRacks = Math.min(8, Math.max(2, Math.floor(hFrac * 9))); | |
| const rackH = H / nRacks; | |
| const dim = stag > 100 ? 0.55 : 1.0; | |
| g.clear(); | |
| // Base shadow plinth | |
| g.rect(-W/2 - 2, -2, W + 4, 5).fill({ color:0x000000, alpha:0.35 }); | |
| // Rack stack (bottom up) | |
| for (let i = 0; i < nRacks; i++) { | |
| const yTop = -rackH * (i + 1); | |
| const alpha = 0.72 + (i / nRacks) * 0.24; | |
| g.rect(-W/2, yTop, W, rackH - 1.5) | |
| .fill({ color:0x1a1f2a, alpha:0.96 * dim }) | |
| .stroke({ width:1, color:tint, alpha: alpha * dim }); | |
| // LEDs: 2 per rack, blink when activeUntil recent | |
| const ledCol = (performance.now() - (a.activeUntil - 4000)) < 4000 && a.activeUntil > performance.now() | |
| ? 0x50fa7b : (stag > 100 ? 0xff5c5c : tint); | |
| g.rect(-W/2 + 4, yTop + rackH/2 - 1.5, 3, 2).fill({ color: ledCol, alpha:0.9 * dim }); | |
| g.rect(-W/2 + 9, yTop + rackH/2 - 1.5, 3, 2).fill({ color: ledCol, alpha:0.5 * dim }); | |
| } | |
| // Roof | |
| g.rect(-W/2 - 3, -H - 4, W + 6, 4).fill({ color: tint, alpha:0.9 * dim }); | |
| // Antenna | |
| g.rect(-1, -H - 14, 2, 10).fill({ color: tint, alpha:0.85 * dim }); | |
| g.circle(0, -H - 16, 2.5).fill({ color: 0xffd700, alpha: 0.9 }); | |
| // Crown if fleet best | |
| if (brier > 0 && brier <= _fleetBestBrier + 0.00001) { | |
| g.moveTo(-W/2 + 3, -H - 6) | |
| .lineTo(-W/4, -H - 14) | |
| .lineTo(0, -H - 8) | |
| .lineTo(W/4, -H - 14) | |
| .lineTo(W/2 - 3, -H - 6) | |
| .lineTo(W/2 - 3, -H - 3) | |
| .lineTo(-W/2 + 3, -H - 3) | |
| .closePath().fill({ color:0xffd700, alpha:0.95 }); | |
| } | |
| } | |
| function _clearLines(board) { for (const ln of board.lines) ln.text = ""; } | |
| function renderCategories(cats) { | |
| const board = DIEGETIC.categories; if (!board) return; | |
| _clearLines(board); | |
| const entries = Object.entries(cats).sort((a,b) => b[1] - a[1]).slice(0, board.lines.length); | |
| if (!entries.length) { board.lines[0].text = "β no day-decisions yet β"; board.lines[0].style.fill = 0x5b6478; return; } | |
| for (let i = 0; i < entries.length; i++) { | |
| const [cat, n] = entries[i]; | |
| const line = board.lines[i]; | |
| line.text = `${String(i+1).padStart(2,"0")} Β· ${cat.slice(0,20).padEnd(20," ")} Γ${n}`; | |
| line.style.fill = i === 0 ? 0xff79c6 : (i < 3 ? 0xffd700 : 0xe6e8ee); | |
| } | |
| } | |
| function renderEvolution() { | |
| const board = DIEGETIC.evolution; if (!board) return; | |
| _clearLines(board); | |
| const islands = AGENTS.filter(a => a._isInfra && a.data).sort((a,b) => (a.data.best_brier||1) - (b.data.best_brier||1)); | |
| if (!islands.length) { board.lines[0].text = "β waiting for islands β"; board.lines[0].style.fill = 0x5b6478; return; } | |
| for (let i = 0; i < Math.min(islands.length, board.lines.length); i++) { | |
| const a = islands[i]; const d = a.data; | |
| const b = (d.best_brier || 0).toFixed(5); | |
| const stag = d.stagnation > 50; | |
| const medal = i === 0 ? "β " : (i < 3 ? "Β·" : " "); | |
| const line = board.lines[i]; | |
| line.text = `${medal} ${a.label.padEnd(5," ")} ${(d.best_model_type||"?").slice(0,9).padEnd(9," ")} ${b} g${d.generation||0}`; | |
| line.style.fill = i === 0 ? 0xffd700 : (stag ? 0xff5c5c : 0x50fa7b); | |
| } | |
| } | |
| function renderLeaderboard() { | |
| const board = DIEGETIC.leaderboard; if (!board) return; | |
| _clearLines(board); | |
| // PQTF banner: $100KΓ6 derivatives fleet ($600K start, target 10Γ by Nov 3). | |
| let startIdx = 0; | |
| if (LATEST_PQTF && LATEST_PQTF.agents) { | |
| const ag = LATEST_PQTF.agents; | |
| const total = Object.values(ag).reduce((s,a) => s + (a.bankroll || 0), 0); | |
| const best = Object.entries(ag).sort((a,b) => (b[1].bankroll||0) - (a[1].bankroll||0))[0]; | |
| const bestName = best ? best[0].slice(0,10) : "β"; | |
| const bestBr = best ? (best[1].bankroll || 0) : 0; | |
| const fleetRoi = ((total - 600000) / 600000) * 100; | |
| const d = LATEST_PQTF.days_processed || 0, dt = LATEST_PQTF.days_total || 50; | |
| if (board.lines[0]) { | |
| board.lines[0].text = `πΌ PQTF d${d}/${dt} Β· fleet $${(total/1000).toFixed(0)}K ${fleetRoi>=0?"+":""}${fleetRoi.toFixed(0)}% Β· top ${bestName} $${(bestBr/1000).toFixed(0)}K`; | |
| board.lines[0].style.fill = 0xffd700; | |
| startIdx = 1; | |
| } | |
| } | |
| const traders = AGENTS.filter(a => (a.kind === "nba" || a.kind === "political") && a.data && typeof a.data.bankroll === "number"); | |
| traders.sort((a,b) => (b.data.bankroll||0) - (a.data.bankroll||0)); | |
| if (!traders.length && startIdx === 0) { | |
| board.lines[0].text = "β waiting for trading floors β"; | |
| board.lines[0].style.fill = 0x5b6478; | |
| return; | |
| } | |
| for (let i = 0; i < Math.min(traders.length, board.lines.length - startIdx); i++) { | |
| const a = traders[i]; const br = a.data.bankroll || 0; const roi = br - 100; | |
| const medal = i === 0 ? "π" : i === 1 ? "π₯" : i === 2 ? "π₯" : `#${i+1}`; | |
| const tag = a.kind === "nba" ? "N" : "P"; | |
| const line = board.lines[i + startIdx]; | |
| line.text = `${medal} [${tag}] ${a.label.slice(0,11).padEnd(11," ")} $${br.toFixed(0).padStart(5," ")} ${roi>=0?"+":""}${roi.toFixed(1)}%`; | |
| line.style.fill = i === 0 ? 0xffd700 : (roi >= 0 ? 0x50fa7b : 0xff5c5c); | |
| } | |
| } | |
| // ββ v2.18: THREE-FLOOR LEADERBOARD (DOM right-side panel) ββββββββββββββββββββ | |
| // Tracks previous bankroll snapshot per (floor, trader_id) so each refresh shows | |
| // today's delta. All three floors (NBA / POL / PQTF) rendered stacked β every | |
| // trader ranked, persona visible, click a row β inspect card with full model info. | |
| const _PREV_BANKROLL = {}; // { "nba:qwen-quant": 100.0, ... } | |
| function _deltaFor(floorKey, id, curr) { | |
| const k = `${floorKey}:${id}`; | |
| const prev = _PREV_BANKROLL[k]; | |
| _PREV_BANKROLL[k] = curr; | |
| if (prev == null || curr == null) return null; | |
| return curr - prev; | |
| } | |
| function _floorSection(kindLabel, kindClass, day, dayTotal, traders, startCap, fleetLeader) { | |
| // traders: [{ id, persona, model, provider, risk, style, bankroll, wins, losses, total_bets, pctDelta? }] | |
| // Sorted by bankroll desc. | |
| const total = traders.reduce((s,t) => s + (t.bankroll||0), 0); | |
| const fleetRoi = startCap > 0 ? ((total - startCap) / startCap) * 100 : 0; | |
| const fltCls = fleetRoi > 0 ? "up" : fleetRoi < 0 ? "down" : "flat"; | |
| const fltTxt = `${fleetRoi>=0?"+":""}${fleetRoi.toFixed(1)}%`; | |
| const dayStr = dayTotal ? `day ${day||0}/${dayTotal}` : (day ? `day ${day}` : "β"); | |
| const rows = traders.map((t,i) => { | |
| const br = t.bankroll || 0; | |
| const dl = t.delta; | |
| const dlCls = dl == null ? "flat" : dl > 0.01 ? "up" : dl < -0.01 ? "down" : "flat"; | |
| const dlTxt = dl == null ? "β" : (dl > 0 ? "+" : "") + (Math.abs(dl) >= 1000 ? (dl/1000).toFixed(1)+"K" : dl.toFixed(0)); | |
| const rkClass = i < 3 ? "rk top" : "rk"; | |
| const rkTxt = i === 0 ? "β " : i === 1 ? "2" : i === 2 ? "3" : String(i+1); | |
| const brTxt = br >= 1000 ? `$${(br/1000).toFixed(1)}K` : `$${br.toFixed(0)}`; | |
| const hot = fleetLeader && t.id === fleetLeader ? " hot" : ""; | |
| return `<div class="r${hot}" data-floor="${kindClass}" data-trader="${escapeHtml(t.id)}"> | |
| <span class="${rkClass}">${rkTxt}</span> | |
| <span class="nm" title="${escapeHtml(t.model)} Β· ${escapeHtml(t.provider)}">${escapeHtml(t.persona)}</span> | |
| <span class="br">${brTxt}</span> | |
| <span class="dl ${dlCls}">${dlTxt}</span> | |
| </div>`; | |
| }).join(""); | |
| return `<div class="sec"> | |
| <div class="sh"> | |
| <span class="kind ${kindClass}">${kindLabel}</span> | |
| <span><span class="day">${dayStr}</span><span class="flt ${fltCls}">${fltTxt}</span></span> | |
| </div> | |
| ${rows || '<div class="empty">no traders yet</div>'} | |
| </div>`; | |
| } | |
| function renderThreeFloorLB() { | |
| const body = document.getElementById("tfl-body"); | |
| if (!body) return; | |
| const sections = []; | |
| // NBA TF | |
| { | |
| const d = LATEST_NBA; | |
| const agents = (d && d.agents) || {}; | |
| const list = Object.entries(agents).map(([id, v]) => { | |
| const p = personaFor(id); | |
| const br = (v && typeof v.bankroll === "number") ? v.bankroll : null; | |
| return { id, persona: p.persona, model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| bankroll: br, wins: v.wins, losses: v.losses, total_bets: v.total_bets, | |
| delta: br == null ? null : _deltaFor("nba", id, br) }; | |
| }).filter(t => t.bankroll != null); | |
| list.sort((a,b) => b.bankroll - a.bankroll); | |
| const day = d && (d.days_processed ?? 0); | |
| const dayT = d && (d.days_total ?? 175); | |
| const startCap = 100 * list.length; | |
| sections.push(_floorSection("NBA TF Β· 17 agents", "nba", day, dayT, list, startCap, d && d.fleet_leader)); | |
| } | |
| // POL TF | |
| { | |
| const d = LATEST_POL; | |
| const agents = (d && d.agents) || {}; | |
| const list = Object.entries(agents).map(([id, v]) => { | |
| const p = personaFor(id); | |
| const br = (v && typeof v.bankroll === "number") ? v.bankroll : null; | |
| return { id, persona: p.persona, model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| bankroll: br, wins: v.wins, losses: v.losses, total_bets: v.total_bets, | |
| delta: br == null ? null : _deltaFor("pol", id, br) }; | |
| }).filter(t => t.bankroll != null); | |
| list.sort((a,b) => b.bankroll - a.bankroll); | |
| const day = d && (d.days_processed ?? 0); | |
| const dayT = d && (d.days_total ?? 50); | |
| const startCap = 100 * list.length; | |
| sections.push(_floorSection("POLITICAL TF Β· 17 agents", "pol", day, dayT, list, startCap, d && d.fleet_leader)); | |
| } | |
| // PQTF (derivatives day-traders β 6 agents, $100K start each) | |
| { | |
| const d = LATEST_PQTF; | |
| const agents = (d && d.agents) || {}; | |
| const list = Object.entries(agents).map(([id, v]) => { | |
| const p = personaFor(id); | |
| const br = (v && typeof v.bankroll === "number") ? v.bankroll : null; | |
| return { id, persona: p.persona, model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| bankroll: br, wins: v.wins, losses: v.losses, total_bets: v.total_bets, | |
| delta: br == null ? null : _deltaFor("pqtf", id, br) }; | |
| }).filter(t => t.bankroll != null); | |
| list.sort((a,b) => b.bankroll - a.bankroll); | |
| const day = d && (d.days_processed ?? 0); | |
| const dayT = d && (d.days_total ?? 50); | |
| const startCap = 100000 * list.length; | |
| sections.push(_floorSection("PQTF Β· 6 derivatives", "pqtf", day, dayT, list, startCap, d && d.fleet_leader)); | |
| } | |
| body.innerHTML = sections.join(""); | |
| // Clock | |
| const clk = document.getElementById("tfl-clock"); | |
| if (clk) clk.textContent = new Date().toISOString().slice(11,19) + " UTC"; | |
| // Row click β open inspect card (synthesize agent if PQTF / fallback) | |
| for (const el of body.querySelectorAll(".r")) { | |
| el.addEventListener("click", () => { | |
| const floor = el.dataset.floor; // nba / pol / pqtf | |
| const id = el.dataset.trader; | |
| let a = null; | |
| if (floor === "nba") a = AGENTS.find(x => x.kind === "nba" && x.id === id); | |
| else if (floor === "pol") a = AGENTS.find(x => x.kind === "political" && x.key === id); | |
| if (!a) { | |
| // PQTF or unknown β synthesize an inspect-only agent | |
| const p = personaFor(id); | |
| const src = (floor === "pqtf" ? LATEST_PQTF : floor === "pol" ? LATEST_POL : LATEST_NBA) || {}; | |
| const vv = (src.agents || {})[id] || {}; | |
| a = { | |
| id, key: id, label: p.persona, persona: p.persona, | |
| model: p.model, provider: p.provider, risk: p.risk, style: p.style, | |
| kind: floor, state: src.running ? "thinking" : "idle", | |
| strategy: p.style, data: vv, _synth: true, | |
| x: WORLD_W / (2*TILE), y: WORLD_H / (2*TILE), | |
| }; | |
| } | |
| showInspectPanel(a); | |
| }); | |
| } | |
| } | |
| // ββ LIVE DATA FETCHER βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function fetchJSON(url) { | |
| try { const r = await fetch(url, { cache:"no-store" }); if (!r.ok) return null; return await r.json(); } | |
| catch(e) { return null; } | |
| } | |
| function escapeHtml(s) { return String(s==null?"":s).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } | |
| const categoryCounter = {}; // rolling count across both TFs | |
| // v2.16: remember latest TF status so the warming-up fallback (no day-decisions yet) | |
| // can still fill the dialogue box with live signals (fleet_leader, council_plan, llm_calls). | |
| let LATEST_NBA = null, LATEST_POL = null, LATEST_PQTF = null, LATEST_ITF = null; | |
| async function refresh() { | |
| document.getElementById("hud-tick").textContent = "Β· fetchingβ¦"; | |
| let nbaOK = 0, polOK = 0, evoOK = 0, pacts = 0, repEntries = 0; | |
| for (const a of AGENTS) a.pactPartner = null; | |
| // NBA TF | |
| const nba = await fetchJSON(ENDPOINTS.nba); | |
| LATEST_NBA = nba; | |
| if (nba) { | |
| const agents = nba.agents || {}; | |
| const strategies = nba.axelrod_strategies || {}; | |
| const sacrificials = nba.sacrificial_assignments || {}; | |
| const reputation = nba.reputation || {}; | |
| pacts += nba.cooperation_pacts_count || 0; | |
| repEntries += Object.keys(reputation).length; | |
| const repKeys = Object.keys(reputation); | |
| for (let i=0; i<repKeys.length-1; i+=2) { | |
| const a = AGENTS.find(x => x.kind==="nba" && x.id===repKeys[i]); | |
| const b = AGENTS.find(x => x.kind==="nba" && x.id===repKeys[i+1]); | |
| if (a && b) { a.pactPartner = b.id; b.pactPartner = a.id; } | |
| } | |
| // v2.12: mid-day fallback. Agents dict is empty until day 1 completes, but llm_calls ticks | |
| // up every prompt. Without this, all 16 trader sprites show "awaiting live dataβ¦" for hours. | |
| const midDay = nba.running && nba.llm_calls > 0 && !Object.keys(agents).length; | |
| for (const a of AGENTS.filter(x => x.kind==="nba")) { | |
| let d = agents[a.id]; | |
| if (!d && midDay) { | |
| d = { bankroll: 100, total_bets: 0, wins: 0, losses: 0, llm_calls: 0, llm_ok: 0, _synth: true }; | |
| a.state = "thinking"; | |
| a.bubble = `${a.label}\nday 1 in progress Β· llm ${nba.llm_calls}/β`; | |
| // v2.24: no persistent bubble β content ready for hover/click only | |
| a.data = d; | |
| nbaOK++; | |
| continue; | |
| } | |
| if (!d) continue; | |
| nbaOK++; | |
| const prev = a.data; | |
| a.data = d; a.strategy = strategies[a.id]; a.sacrificial = sacrificials[a.id]; | |
| const roi = (d.bankroll || 100) - 100; | |
| a.state = d.llm_ok < d.llm_calls ? "error" : roi > 2 ? "profit" : roi < -2 ? "loss" : "thinking"; | |
| if (prev && prev.total_bets != null && d.total_bets > prev.total_bets) { | |
| const deltaW = (d.wins || 0) - (prev.wins || 0); | |
| a.recent = deltaW > 0 ? "β W" : (d.losses > prev.losses) ? "β L" : "bet"; | |
| a.activeUntil = performance.now() + 4000; | |
| logTrade("nba", a.label, `bet #${d.total_bets}`, d.bankroll - prev.bankroll); | |
| spawnPulseRing(a.x * TILE, a.y * TILE, deltaW > 0 ? 0x00d9a7 : 0xff5c5c); | |
| a.bubble = buildTraderBubble(a); | |
| a.bubbleUntil = performance.now() + 4500; | |
| } else { | |
| // v2.24: bubble content kept fresh for hover/click, but NO auto-persistent | |
| // display (was + 20000 β every agent always showed bubble, obscuring sprites). | |
| a.bubble = `${a.label}\n$${(d.bankroll||100).toFixed(0)} Β· ${d.total_bets||0} bets`; | |
| } | |
| } | |
| document.getElementById("hud-day").textContent = `NBA day ${nba.days_processed||0}/${nba.days_total||175} Β· games ${nba.games_processed||0}/${nba.games_total||1247}`; | |
| } | |
| // Political TF | |
| const pol = await fetchJSON(ENDPOINTS.political); | |
| LATEST_POL = pol; | |
| if (pol) { | |
| const agents = pol.agents || {}; | |
| const strategies = pol.axelrod_strategies || {}; | |
| const sacrificials = pol.sacrificial_assignments || {}; | |
| pacts += pol.cooperation_pacts_count || 0; | |
| const midDayP = pol.running && pol.llm_calls > 0 && !Object.keys(agents).length; | |
| for (const a of AGENTS.filter(x => x.kind==="political")) { | |
| let d = agents[a.key]; | |
| if (!d && midDayP) { | |
| d = { bankroll: 100, total_bets: 0, wins: 0, losses: 0, llm_calls: 0, llm_ok: 0, _synth: true }; | |
| a.state = "thinking"; | |
| a.bubble = `${a.label}\nday 1 in progress Β· llm ${pol.llm_calls}/β`; | |
| // v2.24: bubble text only, no auto-persistence (hover/click to see) | |
| a.data = d; | |
| polOK++; | |
| continue; | |
| } | |
| if (!d) continue; | |
| polOK++; | |
| const prev = a.data; | |
| a.data = d; a.strategy = strategies[a.key]; a.sacrificial = sacrificials[a.key]; | |
| const roi = (d.bankroll || 100) - 100; | |
| a.state = d.llm_ok < d.llm_calls ? "error" : roi > 2 ? "profit" : roi < -2 ? "loss" : "thinking"; | |
| if (prev && prev.total_bets != null && d.total_bets > prev.total_bets) { | |
| const deltaW = (d.wins || 0) - (prev.wins || 0); | |
| a.recent = deltaW > 0 ? "β W" : (d.losses > prev.losses) ? "β L" : "bet"; | |
| a.activeUntil = performance.now() + 4000; | |
| logTrade("political", a.label, `trade #${d.total_bets}`, d.bankroll - prev.bankroll); | |
| spawnPulseRing(a.x * TILE, a.y * TILE, deltaW > 0 ? 0x00d9a7 : 0xff5c5c); | |
| a.bubble = buildTraderBubble(a); | |
| a.bubbleUntil = performance.now() + 4500; | |
| } else { | |
| // v2.24: no persistent bubble | |
| a.bubble = `${a.label}\n$${(d.bankroll||100).toFixed(0)} Β· ${d.total_bets||0} trades`; | |
| } | |
| } | |
| } | |
| // v2.17 ITF β 7 intraday agents. Status schema: | |
| // { agents: { tid: { decisions, trades, passes } }, config_agents: [...] } | |
| // Leaderboard adds { tier, name, open_positions }. | |
| // No bankroll field β state derives from (trades, open_positions, passes) ratios. | |
| let itfOK = 0; | |
| const itfLB = await fetchJSON(ENDPOINTS.itfLB); | |
| const itfStatus = await fetchJSON(ENDPOINTS.itf); | |
| LATEST_ITF = itfStatus; | |
| if (itfLB && Array.isArray(itfLB.agents)) { | |
| for (const entry of itfLB.agents) { | |
| const a = AGENTS.find(x => x.kind === "itf" && x.key === entry.tid); | |
| if (!a) continue; | |
| const prev = a.data; | |
| a.data = entry; | |
| itfOK++; | |
| const trades = entry.trades || 0; | |
| const passes = entry.passes || 0; | |
| const openPos = entry.open_positions || 0; | |
| // state: open position β profit (reads as "in the tape"), all-passes β idle, | |
| // never-traded β thinking, traded-and-closed β neutral/thinking | |
| if (openPos > 0) a.state = "profit"; | |
| else if (passes > 0 && trades === 0) a.state = "thinking"; | |
| else if (trades > 0) a.state = (entry.tier === "S") ? "profit" : "thinking"; | |
| else a.state = "thinking"; | |
| if (prev && prev.trades != null && trades > prev.trades) { | |
| a.recent = "β trade"; | |
| a.activeUntil = performance.now() + 4000; | |
| logTrade("itf", a.label, `trade #${trades} (${entry.tier})`, 0); | |
| spawnPulseRing(a.x * TILE, a.y * TILE, 0xffa94d); | |
| } | |
| // v2.24: no persistent bubble β hover/click to see | |
| a.bubble = `${a.label}\n${trades} trade${trades===1?"":"s"} Β· ${passes} pass Β· ${openPos} open`; | |
| } | |
| } | |
| // PQTF (derivatives day-trading fleet) β fleet-level banner only; agents render via leaderboard | |
| const pqtf = await fetchJSON(ENDPOINTS.pqtf); | |
| LATEST_PQTF = pqtf; | |
| // Day-decisions β per-agent LLM reasoning + categories aggregation | |
| async function wireDayDecisions(summaryUrl, detailUrl, kind) { | |
| const summary = await fetchJSON(summaryUrl); | |
| if (!summary || !summary.days || !summary.days.length) return; | |
| const latest = summary.days[summary.days.length - 1]; | |
| if (!latest || !latest.date) return; | |
| const detail = await fetchJSON(`${detailUrl}?date=${latest.date}`); | |
| if (!detail || !detail.agents) return; | |
| for (const a of AGENTS.filter(x => x.kind === kind)) { | |
| const lookupKey = kind === "nba" ? a.id : a.key; | |
| const dd = detail.agents[lookupKey]; | |
| if (!dd) continue; | |
| a.dayDecision = dd; | |
| if (Array.isArray(dd.allocations)) { | |
| for (const al of dd.allocations) { | |
| if (al.category) categoryCounter[al.category] = (categoryCounter[al.category] || 0) + 1; | |
| } | |
| } | |
| a.bubble = buildTraderBubble(a); | |
| } | |
| logTrade(kind, "fleet", `day ${latest.date}: ${latest.total_allocations||0} bets Β· ${((latest.avg_cash_pct||0)*100).toFixed(0)}% cash`); | |
| } | |
| await wireDayDecisions(ENDPOINTS.nbaDays, ENDPOINTS.nbaDays, "nba"); | |
| await wireDayDecisions(ENDPOINTS.politicalDays, ENDPOINTS.politicalDays, "political"); | |
| renderCategories(categoryCounter); | |
| // Islands | |
| const islandResults = await Promise.all(ISLANDS.map(([lbl,u]) => fetchJSON(u).then(d => [lbl,d]))); | |
| for (const [lbl, data] of islandResults) { | |
| const a = AGENTS.find(x => x._isInfra && x.key===lbl); | |
| if (!a) continue; | |
| if (data) { | |
| evoOK++; | |
| const prev = a.data; | |
| a.data = data; | |
| a.state = (data.stagnation||0) > 100 ? "error" : "profit"; | |
| if (prev && prev.best_brier && data.best_brier < prev.best_brier - 0.00005) { | |
| a.activeUntil = performance.now() + 4000; | |
| logTrade("island", lbl, `Brier β ${data.best_brier.toFixed(5)}`); | |
| } | |
| } | |
| // Infra buildings at top: no persistent bubble. Hover card / click reveals info. | |
| a.bubble = ""; | |
| } | |
| // Compute current fleet-best across infra-with-data, then redraw all towers. | |
| const infras = AGENTS.filter(x => x._isInfra && x.data && x.data.best_brier); | |
| _fleetBestBrier = infras.length | |
| ? Math.min(...infras.map(x => x.data.best_brier)) | |
| : 1.0; | |
| for (const a of AGENTS.filter(x => x._isInfra)) updateInfraTower(a); | |
| renderEvolution(); | |
| renderLeaderboard(); | |
| renderThreeFloorLB(); | |
| // v2.24 Claude Code crew β read dispatch-latest.json for fleet-wide dispatch | |
| // count + per-agent flags (which of the 14 agents were dispatched this cycle). | |
| // Source of truth: THE BOSS writes data/ops/dispatch-latest.json every 4h at :00. | |
| let crewOK = 0; | |
| const dispatch = await fetchJSON(CREW_DISPATCH_URL); | |
| const prevCrewDispatch = AGENTS.filter(x => x.kind === "crew")[0]?._prevDispatchTs; | |
| const dispatchTs = dispatch?.ts || null; | |
| const dispatchedSet = new Set(); | |
| const recentSet = new Set(); | |
| if (dispatch && Array.isArray(dispatch.dispatched_agents)) { | |
| for (const ag of dispatch.dispatched_agents) dispatchedSet.add(String(ag).toUpperCase().replace(/\s+/g,"_")); | |
| } | |
| if (dispatch && Array.isArray(dispatch.recently_active_4h)) { | |
| for (const ag of dispatch.recently_active_4h) recentSet.add(String(ag).toUpperCase().replace(/\s+/g,"_")); | |
| } | |
| for (const a of AGENTS.filter(x => x.kind === "crew")) { | |
| if (!dispatch) { | |
| a.state = "error"; | |
| a.bubble = `${a.label}\n(no dispatch feed)`; | |
| continue; | |
| } | |
| crewOK++; | |
| // Dispatcher agent_id uses underscore form (INTERNAL_AFFAIRS) vs label ("INTERNAL AFFAIRS") | |
| const key = a.key.toUpperCase().replace(/\s+/g, "_"); | |
| const wasDispatched = dispatchedSet.has(key); | |
| const wasRecent = wasDispatched || recentSet.has(key); | |
| let ageMin = null; | |
| if (dispatchTs) { | |
| const t = Date.parse(dispatchTs); | |
| if (!isNaN(t)) ageMin = Math.floor((Date.now() - t) / 60000); | |
| } | |
| a.state = wasDispatched ? "profit" : (wasRecent ? "thinking" : "idle"); | |
| a._lastDispatch = wasDispatched ? (dispatchTs ? dispatchTs.slice(11,16) + "Z" : "now") : (wasRecent ? "<4h" : null); | |
| if (wasDispatched && dispatchTs !== a._prevDispatchTs) { | |
| a.activeUntil = performance.now() + 4000; | |
| logTrade("crew", a.label, `dispatched Β· ${dispatchTs.slice(11,16)}Z`); | |
| spawnPulseRing(a.x * TILE, a.y * TILE, 0xffb86c); | |
| } | |
| a._prevDispatchTs = dispatchTs; | |
| a.data = { dispatched: wasDispatched, recent: wasRecent, ts: dispatchTs, role: a.role, track: a.track }; | |
| const tag = wasDispatched ? " Β· ACTIVE" : (wasRecent ? " Β· recent" : ""); | |
| a.bubble = `${a.label}\n${a.role}${tag}${ageMin != null ? ` Β· ${ageMin}m ago` : ""}`; | |
| } | |
| const kCouncil = document.getElementById("k-councils"); | |
| if (kCouncil) { kCouncil.textContent = `${crewOK}/${CLAUDE_CREW.length}`; kCouncil.className = crewOK === CLAUDE_CREW.length ? "v-ok" : "v-warn"; } | |
| document.getElementById("k-nba").textContent = `${nbaOK}/${NBA_TRADERS.length}`; | |
| document.getElementById("k-nba").className = nbaOK === NBA_TRADERS.length ? "v-ok" : "v-warn"; | |
| document.getElementById("k-pol").textContent = `${polOK}/${POL_TRADERS.length}`; | |
| document.getElementById("k-pol").className = polOK === POL_TRADERS.length ? "v-ok" : "v-warn"; | |
| // v2.17: +ITF row, fleet target = ITF_TRADERS.length (7 live). Islands counted vs ISLANDS.length (11 survivors). | |
| const kItf = document.getElementById("k-itf"); | |
| if (kItf) { kItf.textContent = `${itfOK}/${ITF_TRADERS.length}`; kItf.className = itfOK === ITF_TRADERS.length ? "v-ok" : "v-warn"; } | |
| document.getElementById("k-evo").textContent = `${evoOK}/${ISLANDS.length}`; | |
| document.getElementById("k-evo").className = evoOK >= ISLANDS.length ? "v-ok" : "v-warn"; | |
| document.getElementById("k-pacts").textContent = pacts; | |
| document.getElementById("k-last").textContent = new Date().toISOString().slice(11,19); | |
| document.getElementById("hud-rep").textContent = `pacts ${pacts} Β· rep ${repEntries}`; | |
| document.getElementById("hud-tick").textContent = "Β· live"; | |
| // βββ Bloomberg ticker update βββ | |
| const setTx = (id, t, cls) => { | |
| const el = document.getElementById(id); if (!el) return; | |
| const prev = el.textContent; | |
| el.textContent = t; | |
| el.className = "val" + (cls ? " " + cls : ""); | |
| // v2.22 marquee-mirror: keep the duplicate segments in sync so the track | |
| // loop reads the same data on both halves (seamless scroll). | |
| for (const m of document.querySelectorAll(`[data-mirror="${id}"]`)) { | |
| m.textContent = t; | |
| m.className = "val" + (cls ? " " + cls : ""); | |
| } | |
| // Flash when value changes β draws the eye to fresh ticks | |
| if (prev !== t && prev !== "β" && prev !== "") { | |
| el.classList.add("flash"); | |
| setTimeout(() => el.classList.remove("flash"), 600); | |
| } | |
| }; | |
| const avg = (o) => { | |
| const vals = Object.values(o || {}); if (!vals.length) return null; | |
| return vals.reduce((s,a) => s + (a.bankroll || 100), 0) / vals.length - 100; | |
| }; | |
| const sumCalls = (o) => Object.values(o || {}).reduce((s,a) => s + (a.llm_calls || 0), 0); | |
| if (nba) { | |
| setTx("bt-nba-day", `D${nba.days_processed || 0}/${nba.days_total || 175}`); | |
| const p = avg(nba.agents); if (p != null) setTx("bt-nba-pnl", `${p>=0?"+":""}${p.toFixed(2)}%`, p>=0?"up":"down"); | |
| } | |
| if (pol) { | |
| setTx("bt-pol-day", `D${pol.days_processed || 0}/${pol.days_total || 175}`); | |
| const p = avg(pol.agents); if (p != null) setTx("bt-pol-pnl", `${p>=0?"+":""}${p.toFixed(2)}%`, p>=0?"up":"down"); | |
| } | |
| // v2.17 ITF ticker: total trades across fleet + sum open positions (tape-green when trades>0). | |
| if (itfLB && Array.isArray(itfLB.agents)) { | |
| const totTrades = itfLB.agents.reduce((s,a)=>s+(a.trades||0),0); | |
| const totOpen = itfLB.agents.reduce((s,a)=>s+(a.open_positions||0),0); | |
| setTx("bt-itf-trades", `${totTrades} trd`, totTrades > 0 ? "up" : ""); | |
| setTx("bt-itf-open", `${totOpen} open`, totOpen > 0 ? "up" : ""); | |
| } | |
| setTx("bt-pacts", String(pacts)); | |
| setTx("bt-calls", String(sumCalls(nba && nba.agents) + sumCalls(pol && pol.agents))); | |
| const best = AGENTS.filter(x => x._isInfra && x.data && x.data.best_brier) | |
| .sort((a,b) => a.data.best_brier - b.data.best_brier)[0]; | |
| if (best) setTx("bt-fleet", `${best.label} ${best.data.best_brier.toFixed(5)}`, "star"); | |
| // βββ v2.0: Live bet ticker β scroll last 20 allocations from both TFs βββ | |
| renderBetTicker(); | |
| // v2.12: minimap removed | |
| // Fade boot-log after first refresh | |
| const bl = document.getElementById("boot-log"); | |
| if (bl && !bl._faded) { | |
| bl._faded = true; | |
| setTimeout(() => { bl.style.opacity = "0"; setTimeout(() => bl.remove(), 700); }, 6000); | |
| } | |
| } | |
| // βββ v2.15: PokΓ©mon B/W dialogue box β typewriter bet log βββ | |
| // Pulls latest allocations from both TFs, queues as PokΓ©mon-style one-line events | |
| // ("mistral-ministral used ML_HOME! Super effective! +$432"). Typewriter-reveals each | |
| // char, then auto-advances after 3s. Player can read at their pace. | |
| const POKE_QUEUE = []; | |
| let _pokeIdx = 0, _pokeLastMs = 0, _pokeCurEvent = null, _pokeCharIdx = 0; | |
| const POKE_TYPE_MS = 22; // ms per character | |
| const POKE_HOLD_MS = 3200; // ms to hold after reveal before advancing | |
| function renderBetTicker() { | |
| // Rebuild queue from latest allocations (keep newest 40, oldest-first so typewriter tells a story) | |
| const bets = []; | |
| for (const a of AGENTS) { | |
| if (!a.dayDecision || !Array.isArray(a.dayDecision.allocations)) continue; | |
| for (const al of a.dayDecision.allocations) { | |
| if (!al.category) continue; | |
| const amt = al.stake_amount != null ? al.stake_amount | |
| : al.stake != null ? al.stake | |
| : al.amount != null ? al.amount : 0; | |
| bets.push({ | |
| agent: a.label, kind: a.kind, cat: al.category, | |
| amt: +amt || 0, | |
| won: al.won === true ? true : al.won === false ? false : null, | |
| profit: al.profit == null ? null : +al.profit, | |
| game: al.game || "", | |
| pct: (al.pct || 0) * 100, | |
| }); | |
| } | |
| } | |
| // Keep last 40 (chronological) β PokΓ©mon dialog plays them in order | |
| POKE_QUEUE.length = 0; | |
| POKE_QUEUE.push(...bets.slice(-40)); | |
| // v2.16 β fallback: when day-decisions is empty (TF restarted / warming up), synthesize | |
| // a live-signal queue from /api/status so the dialogue box never gets stuck on | |
| // "awaiting day-decisions" for hours. Each TF contributes council summary + fleet leader | |
| // + LLM-call count as PokΓ©mon-style system messages. | |
| if (!POKE_QUEUE.length) { | |
| const synth = []; | |
| const pushWarming = (tag, s) => { | |
| if (!s || !s.running) return; | |
| const leader = s.fleet_leader || "β"; | |
| const best = s.fleet_best_bankroll != null ? `$${(s.fleet_best_bankroll).toFixed(0)}` : "$100"; | |
| const calls = s.llm_calls || 0; | |
| const councils = s.council_plan_count || 0; | |
| const summary = (s.latest_council_summary || "").slice(0, 120); | |
| synth.push({ _synth: true, kind: tag, text: | |
| `<span class="trn ${tag === "political" ? "pol" : "nba"}">${tag.toUpperCase()} TF</span> council ${councils} Β· ${calls} llm-calls Β· leader <span class="cat">${escapeHtml(leader)}</span> ${best}` }); | |
| if (summary) synth.push({ _synth: true, kind: tag, text: | |
| `<span class="trn ${tag === "political" ? "pol" : "nba"}">${tag.toUpperCase()} PLAN</span> β ${escapeHtml(summary)}` }); | |
| synth.push({ _synth: true, kind: tag, text: | |
| `<span class="win">warming councilsβ¦</span> first bets commit after day 1 (~20 llm-calls/agent)` }); | |
| }; | |
| pushWarming("nba", LATEST_NBA); | |
| pushWarming("political", LATEST_POL); | |
| if (!synth.length) synth.push({ _synth: true, kind: "nba", text: | |
| `<span class="lose">both TFs offline</span> β watchdog will restart within 2 min` }); | |
| POKE_QUEUE.push(...synth); | |
| } | |
| if (_pokeIdx >= POKE_QUEUE.length) _pokeIdx = 0; | |
| } | |
| function _pokeFormat(b) { | |
| // v2.16 β synthetic warming-up events carry pre-formatted HTML. | |
| if (b._synth) return b.text || ""; | |
| // Agent uses CATEGORY! β PokΓ©mon move-style readout. | |
| const trn = (b.kind === "political" ? "pol" : "nba"); | |
| const a = b.agent.replace(/^(NBA|POL)Β·/, ""); | |
| const cat = (b.cat || "?").toUpperCase().replace(/_/g, " "); | |
| const verb = b.kind === "political" ? "positioned" : "bet"; | |
| const pct = b.pct > 0 ? `${b.pct.toFixed(0)}%` : ""; | |
| const stake = b.amt > 0 ? `$${b.amt.toFixed(0)}` : "pass"; | |
| let outcome; | |
| if (b.won === true) { | |
| const prof = b.profit != null ? ` +$${b.profit.toFixed(0)}` : ""; | |
| outcome = `<span class="win">it's super effective!${prof}</span>`; | |
| } else if (b.won === false) { | |
| const prof = b.profit != null ? ` ${b.profit.toFixed(0)}` : ""; | |
| outcome = `<span class="lose">it missed${prof}</span>`; | |
| } else outcome = "<span>awaiting resultβ¦</span>"; | |
| return `<span class="trn ${trn}">${escapeHtml(a)}</span> ${verb} ${stake} on <span class="cat">${escapeHtml(cat)}</span> ${pct} β ${outcome}`; | |
| } | |
| function tickPokeDialog(ms) { | |
| const body = document.getElementById("pd-body"); | |
| if (!body || !POKE_QUEUE.length) return; | |
| if (!_pokeCurEvent) { | |
| _pokeCurEvent = POKE_QUEUE[_pokeIdx]; | |
| _pokeCharIdx = 0; | |
| _pokeLastMs = ms; | |
| } | |
| // _pokeCurEvent captured; render the formatted HTML up to _pokeCharIdx plain-text chars. | |
| // Trick: format the FULL html, then slice the text-only portion for typewriter effect by | |
| // walking the chars ignoring tags (use a pre-formatted plain string for indexing). | |
| if (!_pokeCurEvent._html) { | |
| _pokeCurEvent._html = _pokeFormat(_pokeCurEvent); | |
| _pokeCurEvent._plain = _pokeCurEvent._html.replace(/<[^>]+>/g, ""); | |
| } | |
| const plainLen = _pokeCurEvent._plain.length; | |
| if (_pokeCharIdx < plainLen) { | |
| const dt = ms - _pokeLastMs; | |
| const nChars = Math.max(1, Math.floor(dt / POKE_TYPE_MS)); | |
| _pokeCharIdx = Math.min(plainLen, _pokeCharIdx + nChars); | |
| _pokeLastMs = ms; | |
| // Rebuild the visible HTML up to _pokeCharIdx plain chars | |
| body.innerHTML = _pokeBuildPartial(_pokeCurEvent._html, _pokeCharIdx) + | |
| (_pokeCharIdx < plainLen ? '<span class="cursor">β</span>' : '<span class="cursor">βΌ</span>'); | |
| } else { | |
| // Fully revealed β hold, then advance | |
| if (ms - _pokeLastMs > POKE_HOLD_MS) { | |
| _pokeIdx = (_pokeIdx + 1) % POKE_QUEUE.length; | |
| _pokeCurEvent = null; | |
| } | |
| } | |
| } | |
| function _pokeBuildPartial(html, nPlain) { | |
| // Emit HTML char-by-char, consuming tags without counting them against nPlain. | |
| let out = "", i = 0, shown = 0; | |
| while (i < html.length && shown < nPlain) { | |
| if (html[i] === "<") { | |
| const end = html.indexOf(">", i); | |
| out += html.slice(i, end + 1); | |
| i = end + 1; | |
| } else { | |
| out += html[i]; | |
| i++; | |
| shown++; | |
| } | |
| } | |
| // Close any open tags cleanly by emitting the rest that are within the current span scope | |
| // (for this case, spans are short-lived β easier to just append closing from html after this point up to nearest </span>). | |
| while (i < html.length && html[i] !== "<") i++; // advance to next tag | |
| // Append closing spans from remaining HTML (cheap approach: scan for closing tags) | |
| const remaining = html.slice(i); | |
| const unclosed = (out.match(/<span[^>]*>/g) || []).length - (out.match(/<\/span>/g) || []).length; | |
| for (let k = 0; k < unclosed; k++) out += "</span>"; | |
| return out; | |
| } | |
| // βββ v2.0: Mini-map (top-right, 120x80) βββββββββββββββββββββββββββββββββββ | |
| // v2.12: renderMinimap() removed (minimap HUD deleted) | |
| // ββ LIVE RESEARCH (papers.json static β refreshed by daily cron) ββββββββββ | |
| const TAG_COLOR = { | |
| "nomos-karpathy":"#ffd866", "nomos-digest":"#ff7ab6", | |
| "scan":"#ffa500", "calibration-scan":"#50fa7b", "axelrod-scan":"#ff79c6", | |
| "multiagent-trading-scan":"#8be9fd", "github-scan":"#bd93f9", | |
| }; | |
| async function loadPapers() { | |
| try { | |
| const r = await fetch("data/papers.json?t=" + Date.now(), {cache:"no-store"}); | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| const data = await r.json(); | |
| const papers = data.papers || []; | |
| // v2.8: render to in-world research board (DIEGETIC.research) instead of sidebar DOM | |
| const rboard = DIEGETIC.research; | |
| if (rboard) { | |
| _clearLines(rboard); | |
| if (!papers.length) { rboard.lines[0].text = "β no scans loaded β"; rboard.lines[0].style.fill = 0x5b6478; } | |
| else for (let i = 0; i < Math.min(papers.length, rboard.lines.length); i++) { | |
| const p = papers[i]; | |
| const tag = (p.tag || "?").slice(0, 6).toUpperCase(); | |
| const title = (p.title || "").slice(0, 34); | |
| const hex = TAG_COLOR[p.tag] || "#ffa500"; | |
| const line = rboard.lines[i]; | |
| line.text = `[${tag.padEnd(6," ")}] ${title}`; | |
| line.style.fill = parseInt(hex.replace("#",""), 16); | |
| } | |
| } | |
| } catch(e) { | |
| const rboard = DIEGETIC.research; | |
| if (rboard && rboard.lines[0]) { | |
| rboard.lines[0].text = `papers.json load failed`; | |
| rboard.lines[0].style.fill = 0xff5c5c; | |
| } | |
| console.warn("papers load error:", e); | |
| } | |
| } | |
| // ββ BOOT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| computeCamera(); | |
| camScale = camTargetScale; camX = camTargetX; camY = camTargetY; // snap on boot | |
| // Pull build sha (kills hardcoded v2.x) + live trader counts from TF APIs | |
| try { | |
| const [bRaw, nbaRaw, polRaw] = await Promise.all([ | |
| fetch("data/build.json?t=" + Date.now(), {cache:"no-store"}).then(r => r.ok ? r.json() : null).catch(() => null), | |
| fetchJSON(ENDPOINTS.nba), | |
| fetchJSON(ENDPOINTS.political), | |
| ]); | |
| const sha = (bRaw && bRaw.commit) ? bRaw.commit : "live"; | |
| const bt = document.getElementById("build-tag"); | |
| if (bt) bt.textContent = "build: " + sha; | |
| const nNba = nbaRaw && nbaRaw.axelrod_strategies ? Object.keys(nbaRaw.axelrod_strategies).length : 12; | |
| const nPol = polRaw && polRaw.axelrod_strategies ? Object.keys(polRaw.axelrod_strategies).length : 10; | |
| logTrade("council", "pixel-world", `boot Β· sha=${sha} Β· ${nNba} NBA traders Β· ${nPol} POL traders Β· live /api/status (no hardcoded enum)`); | |
| } catch (e) { | |
| logTrade("council", "pixel-world", "boot Β· sha=unknown Β· falling back to static trader list"); | |
| } | |
| await refresh(); | |
| await loadPapers(); | |
| setInterval(loadPapers, 5 * 60 * 1000); // reload papers every 5 min | |
| setInterval(refresh, 15000); // 15s poll β safe under HF rate-limit | |
| // Bloomberg UTC clock β tick every second independent of data refresh | |
| setInterval(() => { | |
| const t = new Date().toISOString().slice(11,19); | |
| const el = document.getElementById("bt-utc"); | |
| if (el) el.textContent = t; | |
| // v2.22: keep marquee duplicate in sync every second | |
| for (const m of document.querySelectorAll('[data-mirror="bt-utc"]')) m.textContent = t; | |
| }, 1000); | |
| // v2.22: inject latest trade events into the scrolling marquee as a rolling feed. | |
| // Pulls from logTrade's TICKER_EVENTS (already maintained). Rebuilds the inner | |
| // HTML of #tk-feed-primary + #tk-feed-mirror every refresh so new bets surface. | |
| function renderTickerFeed() { | |
| const ev = (typeof TICKER_EVENTS !== "undefined" && TICKER_EVENTS) ? TICKER_EVENTS.slice(0, 12) : []; | |
| const segs = ev.map(line => { | |
| const arrow = /β|-\$|LOSS|missed/i.test(line) ? '<span class="arrow down">βΌ</span>' | |
| : /β|\+\$|WIN|effective|Brier/i.test(line) ? '<span class="arrow up">β²</span>' | |
| : '<span class="arrow">β</span>'; | |
| return `<span class="seg">${arrow}<span class="val">${line.replace(/</g,"<")}</span></span>`; | |
| }).join(""); | |
| const a = document.getElementById("tk-feed-primary"); | |
| const b = document.getElementById("tk-feed-mirror"); | |
| if (a) a.innerHTML = segs; | |
| if (b) b.innerHTML = segs; | |
| } | |
| setInterval(renderTickerFeed, 4000); | |
| renderTickerFeed(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |