devmandan Claude Fable 5 commited on
Commit
73352c3
·
1 Parent(s): ff7dcc8

Replays keep the theater

Browse files

Every wait-line, entrance-ritual beat, telegraph, and demand/q20 line shown
during a turn is now captured into the qa entries and the accusation, carried
into the dataset record, and packed into the replay payload. The tape player
plays each turn's theater as ONE self-animating step — lines fade in on a
0.85s stagger inside a single NEXT click, so the comic timing survives without
tripling the click count (earlier steps sit still). Payload stays lean
(capped lines per turn; typical link ~1.4k chars).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md6C7wMyiQuBeiDR1M9x25

Files changed (2) hide show
  1. app.py +30 -6
  2. tests/drive_mock.py +5 -1
app.py CHANGED
@@ -745,6 +745,9 @@ body { background: var(--paper); color: var(--ink); font-family: var(--sans);
745
  .step { margin: 14px 0; animation: fade .3s ease-out; }
746
  @keyframes fade { 0%{opacity:0; transform:translateY(4px)} 100%{opacity:1} }
747
  .step.scene { color: var(--ink-3); font-style: italic; }
 
 
 
748
  .step.q { font-weight: 600; padding-left: 16px; padding-right: 48px; }
749
  .step.q .r { font-weight: 400; color: var(--ink-3); font-style: italic; display: block; }
750
  .step.a { color: var(--ink-2); text-align: right; padding-right: 16px; padding-left: 48px; }
@@ -822,12 +825,16 @@ function build(){
822
  Object.keys(D.claims).map(k =>
823
  '<div class="d-card"><b>'+k+'</b><span>'+esc(D.claims[k])+'</span></div>').join('');
824
  steps.push({c:'scene', h: esc(D.det) + ' takes the case. The tape begins.'});
 
 
825
  D.qa.forEach((e,i) => {
 
826
  let q = '<span>Q'+(i+1)+' — '+esc(e.q)+'</span>';
827
  if (e.r) q = '<span class="r">'+esc(e.r)+'</span>'+q;
828
  steps.push({c:'q', h:q});
829
  if (e.a) steps.push({c:'a', h:esc(e.a)});
830
  });
 
831
  steps.push({c:'scene', h:'He has heard enough.'});
832
  if (D.acc.why) steps.push({c:'mono-log', h:esc(D.acc.why)});
833
  steps.push({c:'stamp', h:'THE LIE: '+esc(snip(D.claims[D.acc.lie]))+' · '+D.acc.pct+'% · '+esc(D.acc.tier)});
@@ -840,7 +847,9 @@ function build(){
840
  }
841
  function render(){
842
  const t = document.getElementById('tape');
843
- t.innerHTML = steps.slice(0, pos).map(s => '<div class="step '+s.c+'">'+s.h+'</div>').join('');
 
 
844
  document.getElementById('counter').textContent = pos + ' / ' + steps.length;
845
  document.getElementById('back').disabled = pos === 0;
846
  document.getElementById('next').disabled = pos >= steps.length;
@@ -1494,7 +1503,8 @@ def build_record(st):
1494
  "labels": {"lie": st["lie_label"], "truths": truths},
1495
  "conversation": [{"turn": e["turn"], "remark": e.get("remark", ""),
1496
  "question": e["q"], "answer": e.get("a"),
1497
- "vague": bool(e.get("vague"))} for e in st["qa"]],
 
1498
  "behavior": {"vague_answers": sum(1 for e in st["qa"] if e.get("vague")),
1499
  "final_vague_streak": _vague_stats(st["qa"])[1]},
1500
  "hidden_analysis": st["hidden_analysis"],
@@ -1795,11 +1805,13 @@ def detective_turn(state, chat):
1795
 
1796
  last_vague, vstreak, _ = _vague_stats(st["qa"])
1797
  wait_start = time.time()
 
1798
  shown = ritual.pop(0) if ritual else _next_wait_line(st, escalated=False,
1799
  vague=last_vague)
1800
  if st["one_word_count"] >= 2 and not st["one_word_called"]:
1801
  st["one_word_called"] = True
1802
  shown = random.choice(char.get("one_word", [ONE_WORD_CALLOUT]))
 
1803
  outs.chat = _chat_from_state(st, trailing=f"*{shown}*")
1804
  yield outs.tuple()
1805
  line_shown_at = time.time()
@@ -1819,6 +1831,7 @@ def detective_turn(state, chat):
1819
  else:
1820
  shown = _next_wait_line(st, escalated=(now - wait_start) > 15,
1821
  vague=last_vague)
 
1822
  outs.chat = _chat_from_state(st, trailing=f"*{shown}*")
1823
  next_line_at = now + random.uniform(4, 6)
1824
  line_shown_at = now
@@ -1843,6 +1856,7 @@ def detective_turn(state, chat):
1843
  time.sleep(MIN_WAIT_LINE_SECS - held)
1844
  while ritual:
1845
  shown = ritual.pop(0)
 
1846
  outs.chat = _chat_from_state(st, trailing=f"*{shown}*")
1847
  yield outs.tuple()
1848
  time.sleep(RITUAL_LINE_SECS)
@@ -1878,7 +1892,8 @@ def detective_turn(state, chat):
1878
  fb = 3
1879
  turn_no = len(st["qa"]) + 1
1880
  st["qa"].append({"turn": turn_no, "q": question, "a": None,
1881
- "remark": remark, "hunch": fb == 1})
 
1882
  # typewriter the question in (~35 chars/s, chunked for Gradio)
1883
  body_final = _chat_from_state(st)
1884
  full = body_final[-1]["content"]
@@ -1899,10 +1914,17 @@ def detective_turn(state, chat):
1899
 
1900
  # ── THE ACCUSATION ──
1901
  pct = _conf_pct(conf, st["game_id"])
 
 
 
 
 
 
1902
  st["accusation"] = {"lie": lie, "why": why or "The answers did not hold together.",
1903
  "confidence": conf, "pct": pct, "tier": _conf_tier(pct),
1904
  "turn": max(n_answered, 1), "forced": n_answered >= MAX_QUESTIONS,
1905
- "demanded": st["demand"], "fallback_level": fb}
 
1906
  st["phase"] = "verdict_staged"
1907
  demanded, at_q20 = st["demand"], n_answered >= MAX_QUESTIONS
1908
  st["demand"] = False
@@ -2014,10 +2036,12 @@ def _replay_url(st):
2014
  "case": st["session"]["case_no"],
2015
  "claims": st["claims"],
2016
  "lie": st["lie_label"],
2017
- "qa": [{"q": e["q"], "a": e.get("a") or "", "r": e.get("remark", "")}
 
2018
  for e in st["qa"]],
2019
  "acc": {"lie": acc.get("lie"), "why": acc.get("why", ""),
2020
- "pct": acc.get("pct"), "tier": acc.get("tier", "")},
 
2021
  "win": st["last_result"] == "win",
2022
  }
2023
  raw = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode()
 
745
  .step { margin: 14px 0; animation: fade .3s ease-out; }
746
  @keyframes fade { 0%{opacity:0; transform:translateY(4px)} 100%{opacity:1} }
747
  .step.scene { color: var(--ink-3); font-style: italic; }
748
+ .step.waits { color: var(--ink-3); font-style: italic; }
749
+ .step.waits .wl { opacity: 0; animation: fade .45s ease-out forwards; margin: 7px 0; }
750
+ .step.waits.old .wl { opacity: 1; animation: none; }
751
  .step.q { font-weight: 600; padding-left: 16px; padding-right: 48px; }
752
  .step.q .r { font-weight: 400; color: var(--ink-3); font-style: italic; display: block; }
753
  .step.a { color: var(--ink-2); text-align: right; padding-right: 16px; padding-left: 48px; }
 
825
  Object.keys(D.claims).map(k =>
826
  '<div class="d-card"><b>'+k+'</b><span>'+esc(D.claims[k])+'</span></div>').join('');
827
  steps.push({c:'scene', h: esc(D.det) + ' takes the case. The tape begins.'});
828
+ const waitStep = w => ({c:'waits', h: w.map((l,j) =>
829
+ '<div class="wl" style="animation-delay:'+(j*0.85)+'s">'+esc(l)+'</div>').join('')});
830
  D.qa.forEach((e,i) => {
831
+ if (e.w && e.w.length) steps.push(waitStep(e.w));
832
  let q = '<span>Q'+(i+1)+' — '+esc(e.q)+'</span>';
833
  if (e.r) q = '<span class="r">'+esc(e.r)+'</span>'+q;
834
  steps.push({c:'q', h:q});
835
  if (e.a) steps.push({c:'a', h:esc(e.a)});
836
  });
837
+ if (D.acc.w && D.acc.w.length) steps.push(waitStep(D.acc.w));
838
  steps.push({c:'scene', h:'He has heard enough.'});
839
  if (D.acc.why) steps.push({c:'mono-log', h:esc(D.acc.why)});
840
  steps.push({c:'stamp', h:'THE LIE: '+esc(snip(D.claims[D.acc.lie]))+' · '+D.acc.pct+'% · '+esc(D.acc.tier)});
 
847
  }
848
  function render(){
849
  const t = document.getElementById('tape');
850
+ // only the newest waits step animates its lines; earlier ones sit still
851
+ t.innerHTML = steps.slice(0, pos).map((s,i) =>
852
+ '<div class="step '+s.c+(i < pos-1 ? ' old' : '')+'">'+s.h+'</div>').join('');
853
  document.getElementById('counter').textContent = pos + ' / ' + steps.length;
854
  document.getElementById('back').disabled = pos === 0;
855
  document.getElementById('next').disabled = pos >= steps.length;
 
1503
  "labels": {"lie": st["lie_label"], "truths": truths},
1504
  "conversation": [{"turn": e["turn"], "remark": e.get("remark", ""),
1505
  "question": e["q"], "answer": e.get("a"),
1506
+ "vague": bool(e.get("vague")),
1507
+ "waits": e.get("waits", [])} for e in st["qa"]],
1508
  "behavior": {"vague_answers": sum(1 for e in st["qa"] if e.get("vague")),
1509
  "final_vague_streak": _vague_stats(st["qa"])[1]},
1510
  "hidden_analysis": st["hidden_analysis"],
 
1805
 
1806
  last_vague, vstreak, _ = _vague_stats(st["qa"])
1807
  wait_start = time.time()
1808
+ turn_waits = [] # every theater line shown this turn — replays keep the show
1809
  shown = ritual.pop(0) if ritual else _next_wait_line(st, escalated=False,
1810
  vague=last_vague)
1811
  if st["one_word_count"] >= 2 and not st["one_word_called"]:
1812
  st["one_word_called"] = True
1813
  shown = random.choice(char.get("one_word", [ONE_WORD_CALLOUT]))
1814
+ turn_waits.append(shown)
1815
  outs.chat = _chat_from_state(st, trailing=f"*{shown}*")
1816
  yield outs.tuple()
1817
  line_shown_at = time.time()
 
1831
  else:
1832
  shown = _next_wait_line(st, escalated=(now - wait_start) > 15,
1833
  vague=last_vague)
1834
+ turn_waits.append(shown)
1835
  outs.chat = _chat_from_state(st, trailing=f"*{shown}*")
1836
  next_line_at = now + random.uniform(4, 6)
1837
  line_shown_at = now
 
1856
  time.sleep(MIN_WAIT_LINE_SECS - held)
1857
  while ritual:
1858
  shown = ritual.pop(0)
1859
+ turn_waits.append(shown)
1860
  outs.chat = _chat_from_state(st, trailing=f"*{shown}*")
1861
  yield outs.tuple()
1862
  time.sleep(RITUAL_LINE_SECS)
 
1892
  fb = 3
1893
  turn_no = len(st["qa"]) + 1
1894
  st["qa"].append({"turn": turn_no, "q": question, "a": None,
1895
+ "remark": remark, "hunch": fb == 1,
1896
+ "waits": turn_waits[:6]})
1897
  # typewriter the question in (~35 chars/s, chunked for Gradio)
1898
  body_final = _chat_from_state(st)
1899
  full = body_final[-1]["content"]
 
1914
 
1915
  # ── THE ACCUSATION ──
1916
  pct = _conf_pct(conf, st["game_id"])
1917
+ if n_answered >= MAX_QUESTIONS:
1918
+ turn_waits.append(char["q20"])
1919
+ else:
1920
+ turn_waits.append(char["telegraph"])
1921
+ if st["demand"]:
1922
+ turn_waits.append(char["demand_resp"])
1923
  st["accusation"] = {"lie": lie, "why": why or "The answers did not hold together.",
1924
  "confidence": conf, "pct": pct, "tier": _conf_tier(pct),
1925
  "turn": max(n_answered, 1), "forced": n_answered >= MAX_QUESTIONS,
1926
+ "demanded": st["demand"], "fallback_level": fb,
1927
+ "waits": turn_waits[:8]}
1928
  st["phase"] = "verdict_staged"
1929
  demanded, at_q20 = st["demand"], n_answered >= MAX_QUESTIONS
1930
  st["demand"] = False
 
2036
  "case": st["session"]["case_no"],
2037
  "claims": st["claims"],
2038
  "lie": st["lie_label"],
2039
+ "qa": [{"q": e["q"], "a": e.get("a") or "", "r": e.get("remark", ""),
2040
+ "w": e.get("waits", [])}
2041
  for e in st["qa"]],
2042
  "acc": {"lie": acc.get("lie"), "why": acc.get("why", ""),
2043
+ "pct": acc.get("pct"), "tier": acc.get("tier", ""),
2044
+ "w": acc.get("waits", [])},
2045
  "win": st["last_result"] == "win",
2046
  }
2047
  raw = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode()
tests/drive_mock.py CHANGED
@@ -88,7 +88,11 @@ def main():
88
  replay = json.loads(_zlib.decompress(_b64.urlsafe_b64decode(payload)))
89
  check("replay payload roundtrips", replay["lie"] == "C" and len(replay["qa"]) >= 5
90
  and replay["det"] == app.CHARACTERS["pip"]["name"], str(replay)[:120])
91
- check("replay url reasonable length", len(m.group(0)) < 4000, str(len(m.group(0))))
 
 
 
 
92
  check("replay excludes hidden analysis", "suspicion" not in json.dumps(replay))
93
  check("phase done", st["phase"] == "done")
94
  check("streak counted", st["session"]["streak"] == 1)
 
88
  replay = json.loads(_zlib.decompress(_b64.urlsafe_b64decode(payload)))
89
  check("replay payload roundtrips", replay["lie"] == "C" and len(replay["qa"]) >= 5
90
  and replay["det"] == app.CHARACTERS["pip"]["name"], str(replay)[:120])
91
+ check("replay url reasonable length", len(m.group(0)) < 6000, str(len(m.group(0))))
92
+ check("replay keeps the theater", any(e.get("w") for e in replay["qa"])
93
+ and len(replay["acc"].get("w", [])) >= 1,
94
+ str([e.get("w") for e in replay["qa"]])[:120])
95
+ check("record keeps the theater", any(e.get("waits") for e in rec["conversation"]))
96
  check("replay excludes hidden analysis", "suspicion" not in json.dumps(replay))
97
  check("phase done", st["phase"] == "done")
98
  check("streak counted", st["session"]["streak"] == 1)