Update app.py
Browse files
app.py
CHANGED
|
@@ -1,17 +1,31 @@
|
|
| 1 |
"""
|
| 2 |
-
app.py — Tiny Civilization: The Tinywick Hollow Gazette
|
| 3 |
-
|
|
|
|
| 4 |
"""
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
from __future__ import annotations
|
| 7 |
-
import json, os, random,
|
| 8 |
from datetime import datetime
|
| 9 |
import gradio as gr
|
| 10 |
from PIL import Image, ImageDraw, ImageFont
|
| 11 |
-
import torch
|
| 12 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 13 |
import database
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# ═══════════════════════════════════════════════════════════════════
|
| 16 |
# 1 ▸ CONSTANTS
|
| 17 |
# ═══════════════════════════════════════════════════════════════════
|
|
@@ -19,194 +33,140 @@ CREATURES = ["fox", "badger", "squirrel", "mole"]
|
|
| 19 |
EVENT_TYPES = ["trade", "gossip", "feud", "invention", "discovery", "ceremony"]
|
| 20 |
CREATURE_EMOJI = {"fox":"🦊","badger":"🦡","squirrel":"🐿️","mole":"🐀"}
|
| 21 |
|
| 22 |
-
# Model — 1.5B primary → 3B fallback. BOTH qualify for ≤4B Tiny Titan badge!
|
| 23 |
MODEL_PRIMARY = "Qwen/Qwen2.5-1.5B-Instruct"
|
| 24 |
MODEL_FALLBACK = "Qwen/Qwen2.5-3B-Instruct"
|
| 25 |
|
| 26 |
REL_DELTAS = {"trade":+5,"gossip":-4,"feud":-10,"invention":+7,"discovery":+6,"ceremony":+3}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
(
|
| 30 |
-
(
|
| 31 |
-
(41, 55, "Wary Acquaintances", "🤝"),
|
| 32 |
-
(56, 70, "Friendly", "🌰"),
|
| 33 |
-
(71, 85, "Close Companions", "🌿"),
|
| 34 |
-
(86, 100, "Inseparable", "🍄"),
|
| 35 |
]
|
| 36 |
|
| 37 |
WEIRD_OBJECTS = [
|
| 38 |
-
"half-eaten poem",
|
| 39 |
-
"forgotten birthday",
|
| 40 |
-
"map to somewhere that may not exist",
|
| 41 |
-
"small jar of preserved thunder",
|
| 42 |
]
|
| 43 |
-
|
| 44 |
LAWS = [
|
| 45 |
-
"no trading on Tuesdays",
|
| 46 |
-
"
|
| 47 |
-
"
|
| 48 |
-
"
|
| 49 |
-
"all debts expire at midnight",
|
| 50 |
-
"silence is legally binding",
|
| 51 |
-
"hats must be worn ironically",
|
| 52 |
-
"the mole's word is final (underground only)",
|
| 53 |
]
|
| 54 |
-
|
| 55 |
RUMOUR_TYPES = [
|
| 56 |
"has been secretly hoarding acorns",
|
| 57 |
"was seen talking to a suspicious stranger at midnight",
|
| 58 |
"invented something that does not work at all",
|
| 59 |
-
"owes three unpayable debts",
|
| 60 |
-
"made a deal with the rain",
|
| 61 |
"owns the moon (allegedly)",
|
| 62 |
"has been writing a novel about everyone in the hollow",
|
| 63 |
"is not who they say they are",
|
| 64 |
"has found a door underground that was not there before",
|
| 65 |
"once ate an entire philosophy",
|
| 66 |
]
|
| 67 |
-
|
| 68 |
CLASSIFIEDS = [
|
| 69 |
"LOST: One certainty. If found, please do not return it. — M. Mole",
|
| 70 |
"FOR SALE: Slightly used certificate of excellence. Condition: forged. — R. Fox",
|
| 71 |
"NOTICE: The badger declares the east mushroom illegal pending investigation.",
|
| 72 |
-
"WANTED: Someone to explain what happened last Tuesday.
|
| 73 |
"FOUND: An unexplained event near the old oak. Owner may claim it.",
|
| 74 |
"REWARD: For the return of three acorns lent in confidence. You know who you are.",
|
| 75 |
"LOST: One argument. I was winning it. — B. Badger",
|
| 76 |
-
"ANNOUNCEMENT: The squirrel's new invention works. (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
]
|
| 78 |
|
| 79 |
# ═══════════════════════════════════════════════════════════════════
|
| 80 |
-
# 2 ▸ AGENT PROMPTS
|
| 81 |
# ═══════════════════════════════════════════════════════════════════
|
| 82 |
AGENT_PROMPTS: dict[str, str] = {
|
| 83 |
"fox": (
|
| 84 |
-
"You are Reginald Fox — charming, dishonest,
|
| 85 |
-
"You speak formally and hint at secret arrangements.
|
| 86 |
-
"negotiable. You collect and forge official-looking documents. "
|
| 87 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 88 |
),
|
| 89 |
"badger": (
|
| 90 |
-
"You are Beatrice Badger — gruff keeper of rules, deeply suspicious
|
| 91 |
-
"
|
| 92 |
-
"
|
| 93 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 94 |
),
|
| 95 |
"squirrel": (
|
| 96 |
"You are Cornelius Squirrel — anxious inventor who speaks fast and repeats himself! "
|
| 97 |
-
"You invent things that almost-but-not-quite work
|
| 98 |
-
"You use exclamation points! You repeat key words for emphasis, for emphasis! "
|
| 99 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 100 |
),
|
| 101 |
"mole": (
|
| 102 |
"You are Millicent Mole — quiet, philosophical, rarely surfaces. "
|
| 103 |
-
"
|
| 104 |
-
"but share them only obliquely. You believe all things connect underground. "
|
| 105 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 106 |
),
|
| 107 |
}
|
| 108 |
-
|
| 109 |
NARRATOR_PROMPT = (
|
| 110 |
"You are the pompous editor-in-chief of The Tinywick Hollow Gazette. "
|
| 111 |
-
"
|
| 112 |
-
"Respond in this EXACT format
|
| 113 |
"HEADLINE IN ALL CAPS\n\n"
|
| 114 |
-
"Three to four sentences of pompous newspaper prose
|
| 115 |
-
"WEATHER: One
|
| 116 |
"FOX: One sentence about the fox's day.\n"
|
| 117 |
"BADGER: One sentence about the badger's day.\n"
|
| 118 |
"SQUIRREL: One sentence about the squirrel's day.\n"
|
| 119 |
"MOLE: One cryptic sentence."
|
| 120 |
)
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
_MODAL_URL = (_os.getenv('MODAL_INFERENCE_URL') or _os.getenv('MODAL_URL') or '').strip()
|
| 128 |
-
_HF_TOKEN = _os.getenv('HF_TOKEN', '').strip()
|
| 129 |
-
|
| 130 |
-
_USE_MODAL = bool(_MODAL_URL)
|
| 131 |
-
_USE_HF_API = bool(_HF_TOKEN) and not _USE_MODAL
|
| 132 |
-
_USE_LOCAL = not _USE_MODAL and not _USE_HF_API
|
| 133 |
-
|
| 134 |
-
_hf_client = _InfClient(token=_HF_TOKEN) if _USE_HF_API else None
|
| 135 |
-
_local_pipe = None
|
| 136 |
-
_model_id_used = 'none'
|
| 137 |
-
|
| 138 |
-
_BACKEND = ('Modal' if _USE_MODAL else
|
| 139 |
-
'HF Inference API [no quota!]' if _USE_HF_API else
|
| 140 |
-
'Local ZeroGPU [set HF_TOKEN to remove quota limit]')
|
| 141 |
-
print(f'[TinyC] Backend: {_BACKEND}', flush=True)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def _load_local_pipeline() -> None:
|
| 145 |
-
global _local_pipe, _model_id_used
|
| 146 |
-
if _local_pipe is not None:
|
| 147 |
-
return
|
| 148 |
for mid in (MODEL_PRIMARY, MODEL_FALLBACK):
|
| 149 |
try:
|
| 150 |
-
print(f
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
dtype=torch.float16,
|
| 154 |
-
device_map='auto',
|
| 155 |
trust_remote_code=True,
|
| 156 |
)
|
| 157 |
_model_id_used = mid
|
| 158 |
-
print(f
|
| 159 |
return
|
| 160 |
except Exception as e:
|
| 161 |
-
print(f
|
| 162 |
-
raise RuntimeError(f
|
| 163 |
-
|
| 164 |
|
| 165 |
def _generate(system_prompt: str, user_prompt: str, max_new_tokens: int = 160) -> str:
|
| 166 |
-
|
| 167 |
-
{'role': 'system', 'content': system_prompt},
|
| 168 |
-
{'role': 'user', 'content': user_prompt},
|
| 169 |
-
]
|
| 170 |
-
if _USE_MODAL:
|
| 171 |
-
try:
|
| 172 |
-
r = _req.post(_MODAL_URL, json={'messages': messages,
|
| 173 |
-
'max_new_tokens': max_new_tokens, 'temperature': 0.88, 'top_p': 0.92},
|
| 174 |
-
timeout=90)
|
| 175 |
-
r.raise_for_status()
|
| 176 |
-
d = r.json()
|
| 177 |
-
return (d.get('text') or d.get('generated_text') or '').strip()
|
| 178 |
-
except Exception as e:
|
| 179 |
-
print(f'[TinyC] Modal error: {e}', flush=True); return ''
|
| 180 |
-
if _USE_HF_API:
|
| 181 |
-
for _attempt in range(2):
|
| 182 |
-
try:
|
| 183 |
-
if _attempt == 0: # newer OpenAI-compat path
|
| 184 |
-
resp = _hf_client.chat.completions.create(
|
| 185 |
-
model=MODEL_PRIMARY, messages=messages,
|
| 186 |
-
max_tokens=max_new_tokens, temperature=0.88, top_p=0.92)
|
| 187 |
-
else: # older path for huggingface_hub < 0.31
|
| 188 |
-
resp = _hf_client.chat_completion(
|
| 189 |
-
model=MODEL_PRIMARY, messages=messages,
|
| 190 |
-
max_tokens=max_new_tokens, temperature=0.88, top_p=0.92)
|
| 191 |
-
return (resp.choices[0].message.content or '').strip()
|
| 192 |
-
except AttributeError:
|
| 193 |
-
continue # try the other API
|
| 194 |
-
except Exception as e:
|
| 195 |
-
print(f'[TinyC] HF API error: {e}', flush=True); return ''
|
| 196 |
-
assert _local_pipe is not None
|
| 197 |
try:
|
| 198 |
-
out =
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
except Exception as e:
|
| 203 |
-
print(f
|
| 204 |
-
|
| 205 |
|
| 206 |
def call_agent(agent_name: str, context: str) -> str:
|
| 207 |
-
|
| 208 |
-
return
|
| 209 |
-
|
| 210 |
|
| 211 |
# ═══════════════════════════════════════════════════════════════════
|
| 212 |
# 4 ▸ SIMULATION
|
|
@@ -216,325 +176,221 @@ def _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number):
|
|
| 216 |
database.save_nudge(day_number, "rumour", f"{nudge_target}: {nudge_value}")
|
| 217 |
return f"A rumour is spreading that {nudge_target} {nudge_value}."
|
| 218 |
if nudge_type == "donation" and nudge_value:
|
| 219 |
-
|
| 220 |
-
c = database.get_creature(
|
| 221 |
if c:
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
return f"Someone anonymously donated '{nudge_value}' to {recipient}."
|
| 226 |
if nudge_type == "law" and nudge_value:
|
| 227 |
database.save_nudge(day_number, "law", nudge_value)
|
| 228 |
-
return f"A new law
|
| 229 |
return ""
|
| 230 |
|
| 231 |
-
|
| 232 |
-
def _historical_ctx() -> str:
|
| 233 |
-
nudges = database.get_recent_nudges(3)
|
| 234 |
-
if not nudges: return ""
|
| 235 |
-
lines = [f"Day {n['day_number']} {n['nudge_type']}: {n['nudge_value']}" for n in nudges]
|
| 236 |
-
return "Recent influences: " + "; ".join(lines)
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
def _past_headline_ctx() -> str:
|
| 240 |
-
"""Include last 2 headlines so newspaper can reference history."""
|
| 241 |
-
hl = database.get_all_headlines()
|
| 242 |
-
if len(hl) < 2: return ""
|
| 243 |
-
recent = hl[:2]
|
| 244 |
-
return "Past headlines: " + " | ".join(f"Day {d}: {h[:40]}" for d, h in recent)
|
| 245 |
-
|
| 246 |
-
|
| 247 |
def _parse_newspaper_full(raw: str, day_number: int) -> dict:
|
| 248 |
-
|
| 249 |
-
lines = [l.strip() for l in raw.strip().splitlines()]
|
| 250 |
-
lines = [l for l in lines if l]
|
| 251 |
-
|
| 252 |
result = {
|
| 253 |
"headline": f"ANOTHER EVENTFUL DAY IN TINYWICK HOLLOW (DAY {day_number})",
|
| 254 |
-
"article": "",
|
| 255 |
-
"weather": "Overcast, with a chance of philosophical observations.",
|
| 256 |
"briefs": {c: f"{c.capitalize()} was present." for c in CREATURES},
|
| 257 |
}
|
| 258 |
-
|
| 259 |
-
# Find headline (first all-caps-ish line)
|
| 260 |
body_start = 0
|
| 261 |
for i, line in enumerate(lines):
|
| 262 |
-
cleaned = re.sub(r'[*_#"\']', "", line).strip()
|
| 263 |
if cleaned and (cleaned == cleaned.upper() or i == 0):
|
| 264 |
-
result["headline"] = cleaned.upper()
|
| 265 |
-
body_start = i + 1
|
| 266 |
-
break
|
| 267 |
-
|
| 268 |
-
# Extract labelled sections
|
| 269 |
article_lines = []
|
| 270 |
for line in lines[body_start:]:
|
| 271 |
-
|
| 272 |
-
if
|
| 273 |
-
|
| 274 |
-
elif
|
| 275 |
-
|
| 276 |
-
elif
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
result["briefs"]["squirrel"] = line.split(":", 1)[1].strip()
|
| 280 |
-
elif upper.startswith("MOLE:"):
|
| 281 |
-
result["briefs"]["mole"] = line.split(":", 1)[1].strip()
|
| 282 |
-
else:
|
| 283 |
-
article_lines.append(line)
|
| 284 |
-
|
| 285 |
-
result["article"] = " ".join(article_lines).strip()
|
| 286 |
-
if not result["article"]:
|
| 287 |
-
result["article"] = raw.strip()
|
| 288 |
-
|
| 289 |
return result
|
| 290 |
|
| 291 |
-
|
| 292 |
def _run_simulation_step(nudge_type, nudge_value, nudge_target):
|
| 293 |
-
"""Core simulation — backend-agnostic."""
|
| 294 |
day_number = database.get_next_day_number()
|
| 295 |
creatures = database.get_all_creatures()
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
event_records
|
| 304 |
for _ in range(3):
|
| 305 |
actor = random.choice(CREATURES)
|
| 306 |
target = random.choice([c for c in CREATURES if c != actor])
|
| 307 |
etype = random.choice(EVENT_TYPES)
|
| 308 |
-
rel = next((c for c in creatures if c["name"]==actor),
|
| 309 |
-
"relationship_scores", {}).get(target, 50)
|
| 310 |
-
|
| 311 |
prompts = {
|
| 312 |
-
"trade": f"Propose a trade with {target} (relationship {rel}/100).
|
| 313 |
-
"gossip": f"Share gossip about {target} (relationship {rel}/100).
|
| 314 |
-
"feud": f"Describe your
|
| 315 |
-
"invention": f"You
|
| 316 |
-
"discovery": f"You
|
| 317 |
-
"ceremony": f"You
|
| 318 |
}
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
if not description or len(description) < 8:
|
| 324 |
-
description = f"{actor.capitalize()} had a {etype} with {target}."
|
| 325 |
-
|
| 326 |
-
event_records.append({"actor":actor,"action":etype,"target":target,"description":description})
|
| 327 |
-
database.save_event(day_number, actor, etype, target, description)
|
| 328 |
-
|
| 329 |
-
# ── Update relationships ──────────────────────────────────
|
| 330 |
delta = REL_DELTAS.get(etype, 0)
|
| 331 |
creatures = database.get_all_creatures()
|
| 332 |
for c in creatures:
|
| 333 |
if c["name"] == actor:
|
| 334 |
sc = c["relationship_scores"]
|
| 335 |
-
sc[target] = max(0, min(100, sc.get(target,50)
|
| 336 |
database.update_creature(actor, relationship_scores=sc)
|
| 337 |
if c["name"] == target:
|
| 338 |
sc = c["relationship_scores"]
|
| 339 |
-
sc[actor] = max(0, min(100, sc.get(actor,50)
|
| 340 |
database.update_creature(target, relationship_scores=sc)
|
| 341 |
creatures = database.get_all_creatures()
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
raw_paper = _generate(
|
| 353 |
-
NARRATOR_PROMPT,
|
| 354 |
-
f"Today's events:\n{events_summary}{extra}",
|
| 355 |
-
max_new_tokens=300,
|
| 356 |
-
)
|
| 357 |
-
if not raw_paper:
|
| 358 |
-
raw_paper = (
|
| 359 |
-
f"CHAOS REIGNS IN TINYWICK HOLLOW ON DAY {day_number}\n\n"
|
| 360 |
-
"The printing press has momentarily jammed. However, our sources confirm "
|
| 361 |
-
"that things definitely happened today.\n\n"
|
| 362 |
-
"WEATHER: Unclear, like everything else.\n"
|
| 363 |
-
f"FOX: {event_records[0]['description'][:60]}\n"
|
| 364 |
-
f"BADGER: {event_records[1]['description'][:60]}\n"
|
| 365 |
-
f"SQUIRREL: {event_records[2]['description'][:60]}\n"
|
| 366 |
-
"MOLE: Something underground."
|
| 367 |
-
)
|
| 368 |
-
|
| 369 |
-
parsed = _parse_newspaper_full(raw_paper, day_number)
|
| 370 |
classified = random.choice(CLASSIFIEDS)
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
f"WEATHER: {parsed['weather']}",
|
| 376 |
-
"\n".join(f"{c.upper()}: {t}" for c, t in parsed["briefs"].items()),
|
| 377 |
-
f"CLASSIFIEDS: {classified}",
|
| 378 |
-
])
|
| 379 |
-
|
| 380 |
database.save_day(day_number, parsed["headline"], full_text)
|
| 381 |
return day_number, parsed, classified
|
| 382 |
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
else:
|
| 391 |
-
def advance_day(nudge_type=None, nudge_value=None, nudge_target=None):
|
| 392 |
-
return _run_simulation_step(nudge_type, nudge_value, nudge_target)
|
| 393 |
-
|
| 394 |
|
| 395 |
# ═══════════════════════════════════════════════════════════════════
|
| 396 |
-
# 6 ▸ PIL
|
| 397 |
# ═══════════════════════════════════════════════════════════════════
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def _tf(paths, sz):
|
| 404 |
-
for p in paths:
|
| 405 |
-
try: return ImageFont.truetype(p, sz)
|
| 406 |
except: pass
|
| 407 |
return ImageFont.load_default()
|
| 408 |
|
| 409 |
-
def render_newspaper_image(parsed:
|
| 410 |
-
W,
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
d.rectangle([M,M,W-M,H-M], outline=BORDER, width=3)
|
| 422 |
-
d.rectangle([M+6,M+6,W-M-6,H-M-6], outline=BORDER, width=1)
|
| 423 |
-
|
| 424 |
-
y = M + 14
|
| 425 |
-
# Masthead
|
| 426 |
-
mast = "THE TINYWICK HOLLOW GAZETTE"
|
| 427 |
-
bb = d.textbbox((0,0),mast,font=f_mast); tw=bb[2]-bb[0]
|
| 428 |
-
d.text(((W-tw)/2, y), mast, fill=INK, font=f_mast); y += bb[3]-bb[1]+3
|
| 429 |
-
# Sub
|
| 430 |
-
sub = f"Est. Day 1 ✦ Day {day_number} ✦ One Acorn ✦ Woodland Readers Only"
|
| 431 |
-
bb=d.textbbox((0,0),sub,font=f_sm); d.text(((W-(bb[2]-bb[0]))/2,y),sub,fill=SUBINK,font=f_sm)
|
| 432 |
-
y+=bb[3]-bb[1]+5
|
| 433 |
-
# Weather strip
|
| 434 |
-
wx = parsed.get("weather","Overcast.")
|
| 435 |
-
wx_line = f"☁ WEATHER: {wx}"
|
| 436 |
-
bb=d.textbbox((0,0),wx_line,font=f_sm); d.text(((W-(bb[2]-bb[0]))/2,y),wx_line,fill=GREY,font=f_sm)
|
| 437 |
-
y+=bb[3]-bb[1]+4
|
| 438 |
-
d.line([M+10,y,W-M-10,y],fill=BORDER,width=2)
|
| 439 |
-
d.line([M+10,y+4,W-M-10,y+4],fill=BORDER,width=1); y+=14
|
| 440 |
-
|
| 441 |
-
# Headline
|
| 442 |
-
for line in textwrap.wrap(parsed.get("headline",""),width=50):
|
| 443 |
-
bb=d.textbbox((0,0),line,font=f_hed); d.text(((W-(bb[2]-bb[0]))/2,y),line,fill=INK,font=f_hed)
|
| 444 |
-
y+=bb[3]-bb[1]+2
|
| 445 |
y+=4; d.line([M+10,y,W-M-10,y],fill=BORDER,width=1); y+=10
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
d.text((col2_x,ry),line,fill=INK,font=f_body); ry+=LH
|
| 465 |
-
|
| 466 |
-
# Sidebar: In Brief
|
| 467 |
-
d.line([side_x-8,art_y,side_x-8,MAX_Y],fill=BORDER,width=1)
|
| 468 |
-
sy=art_y
|
| 469 |
-
d.text((side_x,sy),"IN BRIEF",fill=INK,font=f_bold); sy+=16
|
| 470 |
-
d.line([side_x,sy,W-M-14,sy],fill=GREY,width=1); sy+=6
|
| 471 |
-
briefs = parsed.get("briefs",{})
|
| 472 |
-
for cname in CREATURES:
|
| 473 |
-
em=CREATURE_EMOJI.get(cname,"?"); txt=briefs.get(cname,"")
|
| 474 |
-
header=f"{em} {cname.upper()}"
|
| 475 |
-
d.text((side_x,sy),header,fill=INK,font=f_bold); sy+=13
|
| 476 |
-
for bline in textwrap.wrap(txt,width=26):
|
| 477 |
-
if sy+12>MAX_Y: break
|
| 478 |
-
d.text((side_x,sy),bline,fill=INK,font=f_sm); sy+=12
|
| 479 |
sy+=4
|
| 480 |
-
|
| 481 |
-
# Classifieds footer
|
| 482 |
fy=H-M-38; d.line([M+10,fy,W-M-10,fy],fill=BORDER,width=1); fy+=4
|
| 483 |
-
d.text((M+14,fy),"CLASSIFIEDS",fill=INK,font=
|
| 484 |
-
for cl in textwrap.wrap(classified,
|
| 485 |
if fy+12>H-M-8: break
|
| 486 |
-
d.text((M+14,fy),cl,fill=INK,font=
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
"
|
| 498 |
-
|
| 499 |
-
trace = {
|
| 500 |
-
"meta": {
|
| 501 |
-
"project": "Tiny Civilization — The Tinywick Hollow Gazette",
|
| 502 |
-
"model": _model_id_used,
|
| 503 |
-
"exported_at": datetime.now().isoformat(),
|
| 504 |
-
},
|
| 505 |
-
"agent_prompts": AGENT_PROMPTS,
|
| 506 |
-
"narrator_prompt": NARRATOR_PROMPT,
|
| 507 |
-
"creature_state": creatures,
|
| 508 |
-
"events": [],
|
| 509 |
-
"days": [],
|
| 510 |
-
}
|
| 511 |
-
headlines = database.get_all_headlines()
|
| 512 |
-
for dn, _ in reversed(headlines):
|
| 513 |
-
day = database.get_day(dn)
|
| 514 |
-
if day: trace["days"].append(dict(day))
|
| 515 |
-
evts = database.get_events_for_day(dn)
|
| 516 |
-
trace["events"].extend([{"day": dn, **e} for e in evts])
|
| 517 |
-
path = "/tmp/tiny_civ_traces.json"
|
| 518 |
-
with open(path,"w") as f: json.dump(trace, f, indent=2, default=str)
|
| 519 |
return path
|
| 520 |
|
| 521 |
-
|
| 522 |
# ═══════════════════════════════════════════════════════════════════
|
| 523 |
-
#
|
| 524 |
# ═══════════════════════════════════════════════════════════════════
|
| 525 |
NEWSPAPER_CSS = """
|
| 526 |
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=UnifrakturMaguntia&display=swap');
|
| 527 |
|
| 528 |
body, .gradio-container { background: #c4b090 !important; }
|
| 529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
/* ── Newspaper wrapper ── */
|
| 531 |
.paper-wrap {
|
| 532 |
-
background:
|
| 533 |
-
background-image:
|
| 534 |
-
border:
|
| 535 |
-
padding:
|
| 536 |
-
box-shadow:
|
| 537 |
-
margin:
|
|
|
|
| 538 |
}
|
| 539 |
.paper-wrap::before {
|
| 540 |
content:""; display:block; border:1px solid #4a2a08;
|
|
@@ -547,32 +403,40 @@ body, .gradio-container { background: #c4b090 !important; }
|
|
| 547 |
font-size:2.4em; text-align:center; color:#160800;
|
| 548 |
border-top:5px double #4a2a08; border-bottom:5px double #4a2a08;
|
| 549 |
padding:5px 0; margin-bottom:3px; letter-spacing:1px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
}
|
| 551 |
-
.paper-
|
| 552 |
-
text-align:center; font-style:italic; margin-bottom:2px; }
|
| 553 |
-
.paper-weather { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#7a5630;
|
| 554 |
-
text-align:center; margin-bottom:6px; letter-spacing:.5px; }
|
| 555 |
-
.paper-rule { border:none; border-top:2px solid #4a2a08; margin:3px 0 6px; }
|
| 556 |
.paper-rule-thin { border:none; border-top:1px solid #9a7040; margin:3px 0; }
|
| 557 |
|
| 558 |
-
/* ── Headline ── */
|
| 559 |
.paper-headline {
|
| 560 |
font-family:'Playfair Display',Georgia,serif; font-size:1.85em; font-weight:900;
|
| 561 |
text-align:center; text-transform:uppercase; color:#0c0400;
|
| 562 |
line-height:1.12; margin:6px 0 8px;
|
|
|
|
| 563 |
}
|
| 564 |
-
|
| 565 |
-
/* ── Body row: article + sidebar ── */
|
| 566 |
.paper-body-row { display:flex; gap:0; }
|
| 567 |
.paper-article-cols {
|
| 568 |
flex:1; font-family:'Libre Baskerville',Georgia,serif; font-size:.88em;
|
| 569 |
color:#180a02; line-height:1.75; text-align:justify;
|
| 570 |
-
column-count:2; column-gap:24px; column-rule:1px solid #9a7040;
|
| 571 |
-
|
| 572 |
}
|
| 573 |
.paper-sidebar {
|
| 574 |
width:210px; min-width:190px; padding:0 0 0 16px;
|
| 575 |
border-left:2px solid #4a2a08; margin-left:16px;
|
|
|
|
| 576 |
}
|
| 577 |
.sidebar-title {
|
| 578 |
font-family:'Playfair Display',Georgia,serif; font-weight:700; font-size:.85em;
|
|
@@ -580,35 +444,82 @@ body, .gradio-container { background: #c4b090 !important; }
|
|
| 580 |
border-bottom:1px solid #9a7040; padding-bottom:3px; margin-bottom:6px;
|
| 581 |
}
|
| 582 |
.sidebar-item { margin-bottom:8px; }
|
| 583 |
-
.sidebar-creature-name {
|
| 584 |
-
|
| 585 |
-
}
|
| 586 |
-
.sidebar-creature-text {
|
| 587 |
-
font-family:'Libre Baskerville',Georgia,serif; font-size:.74em; color:#3a1c08;
|
| 588 |
-
line-height:1.45; font-style:italic;
|
| 589 |
-
}
|
| 590 |
|
| 591 |
-
/* ── Classifieds ── */
|
| 592 |
.paper-classifieds {
|
| 593 |
font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615;
|
| 594 |
border-top:1px solid #9a7040; margin-top:8px; padding-top:5px; font-style:italic;
|
|
|
|
| 595 |
}
|
| 596 |
.paper-classifieds span { font-weight:700; font-style:normal; color:#2a1008; }
|
| 597 |
-
|
| 598 |
-
/* ── Day badge ── */
|
| 599 |
.paper-daybadge {
|
| 600 |
font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615;
|
| 601 |
text-align:center; border-top:1px solid #9a7040; margin-top:8px; padding-top:5px;
|
|
|
|
| 602 |
}
|
| 603 |
|
| 604 |
-
/* ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
.status-strip {
|
| 606 |
background:#d6c4a0; border:1px solid #8a6030; border-radius:3px;
|
| 607 |
padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif;
|
| 608 |
font-size:.82em; color:#2a1008; text-align:center; margin:4px 0;
|
| 609 |
}
|
| 610 |
-
|
| 611 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
.creature-grid { display:flex; gap:8px; flex-wrap:wrap; }
|
| 613 |
.creature-card {
|
| 614 |
background:#f8ecd8; border:1px solid #9a7040; border-radius:2px;
|
|
@@ -617,99 +528,158 @@ body, .gradio-container { background: #c4b090 !important; }
|
|
| 617 |
}
|
| 618 |
.creature-name { font-weight:700; font-size:.95em; color:#260e04; display:block; margin-bottom:3px; }
|
| 619 |
.creature-tier { font-style:italic; color:#7a5030; font-size:.85em; }
|
| 620 |
-
|
| 621 |
-
/* ── Civ stats ── */
|
| 622 |
-
.civ-stats {
|
| 623 |
-
background:#e8d4b0; border:1px solid #9a7040; border-radius:2px;
|
| 624 |
-
padding:8px 14px; font-family:'Libre Baskerville',Georgia,serif;
|
| 625 |
-
font-size:.78em; color:#2a1008; display:flex; gap:14px; flex-wrap:wrap;
|
| 626 |
-
justify-content:center; margin:6px 0;
|
| 627 |
-
}
|
| 628 |
-
.stat-item { text-align:center; }
|
| 629 |
-
.stat-label { font-size:.85em; color:#7a5030; display:block; }
|
| 630 |
-
.stat-value { font-weight:700; font-size:1.05em; color:#160800; }
|
| 631 |
-
|
| 632 |
-
/* ── Section titles ── */
|
| 633 |
.section-title {
|
| 634 |
font-family:'Playfair Display',Georgia,serif; font-weight:700;
|
| 635 |
-
color:#160800; font-size:.95em; text-transform:uppercase;
|
| 636 |
-
|
| 637 |
-
border-bottom:1px solid #8a6030; padding-bottom:3px; margin:8px 0 8px;
|
| 638 |
}
|
| 639 |
-
.archive-area { background:#f0e4c6; border:1px solid #9a7040; border-radius:2px;
|
| 640 |
-
padding:10px; margin-top:5px; min-height:50px; }
|
| 641 |
|
| 642 |
/* ── Konami modal ── */
|
| 643 |
#konami-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:99998; }
|
| 644 |
#konami-modal {
|
| 645 |
display:none; position:fixed; z-index:99999; top:50%; left:50%;
|
| 646 |
-
transform:translate(-50%,-50%); width:min(660px,92vw); max-height:76vh;
|
| 647 |
-
background:#f6ead0; border:3px solid #4a2a08;
|
| 648 |
-
padding:22px 26px 18px;
|
|
|
|
| 649 |
}
|
| 650 |
#konami-modal h2 { font-family:'Playfair Display',Georgia,serif; color:#160800; margin:0 0 10px; }
|
| 651 |
#konami-modal details { margin:6px 0; }
|
| 652 |
#konami-modal summary { cursor:pointer; font-weight:700; color:#4a2a08; }
|
| 653 |
-
#konami-modal pre { background:#e8d4b0; border:1px solid #9a7040; padding:8px;
|
| 654 |
-
|
| 655 |
-
#konami-close { position:absolute; top:8px; right:12px; cursor:pointer;
|
| 656 |
-
font-size:1.3em; color:#4a2a08; background:none; border:none; }
|
| 657 |
"""
|
| 658 |
|
| 659 |
# ═══════════════════════════════════════════════════════════════════
|
| 660 |
-
#
|
| 661 |
# ═══════════════════════════════════════════════════════════════════
|
| 662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
(function(){
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
})();
|
| 680 |
-
</script>
|
| 681 |
-
|
| 682 |
|
| 683 |
# ═══════════════════════════════════════════════════════════════════
|
| 684 |
-
#
|
| 685 |
# ═══════════════════════════════════════════════════════════════════
|
| 686 |
-
def
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
|
| 691 |
def _html_paper(parsed: dict, classified: str, day_num: int) -> str:
|
| 692 |
-
hed
|
| 693 |
-
art
|
| 694 |
-
wx
|
| 695 |
-
|
|
|
|
| 696 |
emojis = " ".join(f"{CREATURE_EMOJI[c]} {c.capitalize()}" for c in CREATURES)
|
| 697 |
-
|
| 698 |
|
| 699 |
-
sidebar_items = ""
|
| 700 |
-
for c in CREATURES:
|
| 701 |
-
em = CREATURE_EMOJI.get(c,"?")
|
| 702 |
-
txt = (briefs.get(c,"") or "").replace("<","<")
|
| 703 |
-
sidebar_items += f"""
|
| 704 |
<div class="sidebar-item">
|
| 705 |
-
<div class="sidebar-creature-name">{
|
| 706 |
-
<div class="sidebar-creature-text">{
|
| 707 |
-
</div>"""
|
| 708 |
|
| 709 |
return f"""
|
| 710 |
-
<div class="paper-wrap">
|
| 711 |
<div class="paper-masthead">The Tinywick Hollow Gazette</div>
|
| 712 |
-
<div class="paper-sub">Est. Day 1 ✦ Day {day_num} ✦ One Acorn Per Copy ✦ Serving the Woodland
|
| 713 |
<div class="paper-weather">☁ {wx}</div>
|
| 714 |
<hr class="paper-rule">
|
| 715 |
<div class="paper-headline">{hed}</div>
|
|
@@ -721,269 +691,253 @@ def _html_paper(parsed: dict, classified: str, day_num: int) -> str:
|
|
| 721 |
{sidebar_items}
|
| 722 |
</div>
|
| 723 |
</div>
|
| 724 |
-
<div class="paper-classifieds"><span>CLASSIFIEDS:</span> {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
<div class="paper-daybadge">— Day {day_num} — {emojis}</div>
|
| 726 |
</div>
|
|
|
|
| 727 |
"""
|
| 728 |
|
| 729 |
-
def
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
parts = text.split("\n\n",1)
|
| 734 |
-
hed = parts[0]; art = parts[1] if len(parts)>1 else text
|
| 735 |
-
dummy_parsed = {
|
| 736 |
-
"headline": hed, "article": art,
|
| 737 |
-
"weather": "Portentous, with scattered significance.",
|
| 738 |
-
"briefs": {
|
| 739 |
-
"fox": "Forged three certificates before breakfast.",
|
| 740 |
-
"badger": "Insisted on thirteen amendments before lunch.",
|
| 741 |
-
"squirrel": "Invented a signing machine! It signed the wrong document!",
|
| 742 |
-
"mole": "Something is already happening underground.",
|
| 743 |
-
}
|
| 744 |
-
}
|
| 745 |
-
return _html_paper(dummy_parsed, "NOTICE: Civilisation now in progress.", 0)
|
| 746 |
-
return """<div class="paper-wrap">
|
| 747 |
<div class="paper-masthead">The Tinywick Hollow Gazette</div>
|
|
|
|
| 748 |
<hr class="paper-rule">
|
| 749 |
-
<div class="
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
|
| 755 |
-
def
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
<span class="creature-name">{em} {c['name'].capitalize()}</span>
|
| 767 |
<div class="creature-tier">{rels}</div>
|
| 768 |
-
<div style="font-size:.78em;margin-top:3px;color:#5a3615;"><em>Carries:</em> {inv}</div>
|
| 769 |
</div>"""
|
| 770 |
return f'<div class="creature-grid">{cards}</div>'
|
| 771 |
|
| 772 |
-
def _html_civ_stats()
|
| 773 |
-
s
|
| 774 |
-
bp
|
| 775 |
-
dom = s["dominant"].capitalize()
|
| 776 |
return f"""<div class="civ-stats">
|
| 777 |
<div class="stat-item"><span class="stat-label">Days</span><span class="stat-value">{s['total_days']}</span></div>
|
| 778 |
<div class="stat-item"><span class="stat-label">Events</span><span class="stat-value">{s['total_events']}</span></div>
|
| 779 |
<div class="stat-item"><span class="stat-label">Nudges</span><span class="stat-value">{s['total_nudges']}</span></div>
|
| 780 |
-
<div class="stat-item"><span class="stat-label">Strongest bond</span>
|
| 781 |
-
|
| 782 |
-
<div class="stat-item"><span class="stat-label">
|
| 783 |
-
<span class="stat-value">{wp[0].capitalize()} vs {wp[1].capitalize()} ({wp[2]})</span></div>
|
| 784 |
-
<div class="stat-item"><span class="stat-label">Most popular</span><span class="stat-value">{dom}</span></div>
|
| 785 |
</div>"""
|
| 786 |
|
| 787 |
def _archive_choices():
|
| 788 |
-
hl
|
| 789 |
-
return [(f"Day {dn}: {h[:44]}{'…' if len(h)>44 else ''}",
|
| 790 |
|
| 791 |
def _status(msg): return f'<div class="status-strip">{msg}</div>'
|
| 792 |
|
| 793 |
-
def _konami_html()
|
| 794 |
import html as _h
|
| 795 |
-
details
|
| 796 |
-
f"<details><summary>{CREATURE_EMOJI.get(n,'')}<strong>
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
)
|
| 800 |
-
details += f"<details><summary>📰 <strong>NARRATOR</strong></summary><pre>{_h.escape(NARRATOR_PROMPT)}</pre></details>"
|
| 801 |
return f"""
|
| 802 |
<div id="konami-backdrop" onclick="hideKonami()"></div>
|
| 803 |
<div id="konami-modal" role="dialog">
|
| 804 |
<button id="konami-close" onclick="hideKonami()">✕</button>
|
| 805 |
<h2>🔮 Secret Agent Briefing</h2>
|
| 806 |
-
<p>Konami Code
|
| 807 |
{details}
|
| 808 |
-
<p style="text-align:center;font-style:italic;color:#5a3615;font-size:.84em;margin-top:14px;">
|
| 809 |
-
|
| 810 |
-
</div>{KONAMI_JS}"""
|
| 811 |
-
|
| 812 |
|
| 813 |
# ��══════════════════════════════════════════════════════════════════
|
| 814 |
-
#
|
| 815 |
# ═══════════════════════════════════════════════════════════════════
|
| 816 |
-
def _render_all(day_num,
|
| 817 |
-
return (
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
)
|
| 825 |
-
|
| 826 |
-
|
|
|
|
| 827 |
try:
|
| 828 |
-
|
| 829 |
model_tag = f" [{_model_id_used.split('/')[-1]}]" if _model_id_used!="none" else ""
|
| 830 |
-
|
| 831 |
-
f"✓ Day {day_num} published to the Gazette.{model_tag}")
|
| 832 |
except Exception:
|
| 833 |
-
tb = traceback.format_exc()
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
"Beatrice Badger suspects sabotage. Reginald Fox denies everything.",
|
| 839 |
-
"weather": "Stormy, with a chance of errors.",
|
| 840 |
-
"briefs": {c: "Unavailable for comment." for c in CREATURES},
|
| 841 |
-
}
|
| 842 |
-
return _render_all(0, err_parsed, "LOST: One functioning simulation. — The Editor",
|
| 843 |
-
"✗ Simulation error — the printing press is jammed. Check logs.")
|
| 844 |
|
| 845 |
-
def handle_advance():
|
| 846 |
-
def handle_rumour(
|
| 847 |
-
def handle_donation(
|
| 848 |
-
def handle_law(
|
| 849 |
|
| 850 |
def handle_archive_view(day_num):
|
| 851 |
if day_num is None:
|
| 852 |
-
return '<div class="archive-area"><em>Select a day
|
| 853 |
-
day
|
| 854 |
-
if not day: return '<div class="archive-area"><em>
|
| 855 |
-
txt
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
return f'<div class="archive-area">{_html_paper(parsed, cl, day_num)}</div>'
|
| 862 |
-
|
| 863 |
-
def handle_share(day_num_state, parsed_state, classified_state):
|
| 864 |
-
if not parsed_state or not parsed_state.get("headline"): return gr.update(visible=False)
|
| 865 |
try:
|
| 866 |
-
path
|
| 867 |
-
return gr.update(visible=True,
|
| 868 |
except Exception: return gr.update(visible=False)
|
| 869 |
|
| 870 |
-
def
|
| 871 |
-
try:
|
| 872 |
-
path = export_agent_traces()
|
| 873 |
-
return gr.update(visible=True, value=path)
|
| 874 |
except Exception: return gr.update(visible=False)
|
| 875 |
|
| 876 |
-
|
| 877 |
-
#
|
| 878 |
-
#
|
| 879 |
-
# =================================================================
|
| 880 |
-
|
| 881 |
-
# -- Startup: init DB, load latest state --------------------------
|
| 882 |
database.init_db()
|
| 883 |
-
|
| 884 |
-
if
|
| 885 |
-
_INIT_DAY
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
_INIT_CL
|
| 889 |
else:
|
| 890 |
-
_INIT_DAY
|
| 891 |
|
| 892 |
-
#
|
| 893 |
-
_GR_MAJOR
|
| 894 |
-
if
|
| 895 |
-
|
| 896 |
-
else: _BKW = {"css": NEWSPAPER_CSS, "theme": gr.themes.Base(primary_hue="orange", neutral_hue="stone")}; _LKW = {}
|
| 897 |
|
| 898 |
-
|
| 899 |
-
with gr.Blocks(title="Tiny Civilization — The Tinywick Hollow Gazette", **_BKW) as demo:
|
| 900 |
|
| 901 |
gr.HTML(_konami_html())
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
day_state
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
status_html
|
| 918 |
-
f"Day {_INIT_DAY} in the archive — next: Day {_INIT_DAY
|
| 919 |
-
|
| 920 |
-
|
| 921 |
|
| 922 |
with gr.Row(equal_height=False):
|
| 923 |
-
|
| 924 |
with gr.Column(scale=3):
|
| 925 |
-
newspaper_display
|
| 926 |
-
value=(_html_paper(
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
archive_display = gr.HTML(
|
| 934 |
-
value='<div class="archive-area"><em>Select a day to read its edition.</em></div>')
|
| 935 |
-
|
| 936 |
-
with gr.Column(scale=1, min_width=260):
|
| 937 |
gr.HTML('<div class="section-title">📰 Editorial Desk</div>')
|
| 938 |
-
advance_btn
|
| 939 |
|
| 940 |
gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
|
| 941 |
gr.HTML('<div class="section-title">✉ Nudge the Story</div>')
|
| 942 |
-
gr.HTML('<p style="font-size:.78em;color:#5a3615;text-align:center;'
|
| 943 |
-
'font-style:italic;margin:0 0 6px;">Each nudge advances one day.</p>')
|
| 944 |
|
| 945 |
-
with gr.Accordion(
|
| 946 |
-
rumour_creature
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
label='What rumour?')
|
| 950 |
-
rumour_btn = gr.Button('📢 Spread It', variant='secondary')
|
| 951 |
|
| 952 |
-
with gr.Accordion(
|
| 953 |
-
donation_dd
|
| 954 |
-
|
| 955 |
-
donation_btn = gr.Button('🎁 Donate It', variant='secondary')
|
| 956 |
|
| 957 |
-
with gr.Accordion(
|
| 958 |
-
law_dd
|
| 959 |
-
law_btn
|
| 960 |
|
| 961 |
gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
|
| 962 |
-
share_btn
|
| 963 |
-
img_output
|
| 964 |
-
export_btn
|
| 965 |
-
trace_output
|
| 966 |
|
| 967 |
-
gr.HTML('<p style="font-size:.70em;color:#6a4818;text-align:center;'
|
| 968 |
-
'
|
| 969 |
-
'Model: Qwen2.5-1.5B ≤4B 🐜 | Local only 🔌 | Custom UI 🎨</p>')
|
| 970 |
|
| 971 |
gr.HTML('<div class="section-title" style="margin-top:12px;">Woodland Residents</div>')
|
| 972 |
-
creature_display
|
| 973 |
|
| 974 |
-
_OUT
|
| 975 |
-
|
| 976 |
|
| 977 |
-
advance_btn.click(
|
| 978 |
-
rumour_btn.click(
|
| 979 |
-
donation_btn.click(fn=handle_donation,
|
| 980 |
-
law_btn.click(
|
| 981 |
-
archive_dd.change(
|
| 982 |
-
share_btn.click(
|
| 983 |
-
export_btn.click(
|
| 984 |
|
| 985 |
# ═══════════════════════════════════════════════════════════════════
|
| 986 |
-
#
|
| 987 |
# ═══════════════════════════════════════════════════════════════════
|
| 988 |
if __name__ == "__main__":
|
| 989 |
demo.launch(server_name="0.0.0.0", server_port=7860, **_LKW)
|
|
|
|
| 1 |
"""
|
| 2 |
+
app.py — Tiny Civilization: The Tinywick Hollow Gazette 🍄
|
| 3 |
+
Persistent woodland civilisation · Qwen2.5-1.5B · ZeroGPU · Gradio 6
|
| 4 |
+
Build Small Hackathon 2026 — Thousand Token Wood track
|
| 5 |
"""
|
| 6 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 7 |
+
# 0 ▸ IMPORTS
|
| 8 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 9 |
from __future__ import annotations
|
| 10 |
+
import json, os, random, re, textwrap, traceback
|
| 11 |
from datetime import datetime
|
| 12 |
import gradio as gr
|
| 13 |
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
|
| 14 |
import database
|
| 15 |
|
| 16 |
+
try:
|
| 17 |
+
import spaces
|
| 18 |
+
_ZERO_GPU = True
|
| 19 |
+
except ImportError:
|
| 20 |
+
class _Stub:
|
| 21 |
+
@staticmethod
|
| 22 |
+
def GPU(fn=None, *, duration=60):
|
| 23 |
+
return fn if callable(fn) else (lambda f: f)
|
| 24 |
+
spaces = _Stub(); _ZERO_GPU = False # type: ignore
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
from transformers import pipeline as hf_pipeline
|
| 28 |
+
|
| 29 |
# ═══════════════════════════════════════════════════════════════════
|
| 30 |
# 1 ▸ CONSTANTS
|
| 31 |
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
| 33 |
EVENT_TYPES = ["trade", "gossip", "feud", "invention", "discovery", "ceremony"]
|
| 34 |
CREATURE_EMOJI = {"fox":"🦊","badger":"🦡","squirrel":"🐿️","mole":"🐀"}
|
| 35 |
|
|
|
|
| 36 |
MODEL_PRIMARY = "Qwen/Qwen2.5-1.5B-Instruct"
|
| 37 |
MODEL_FALLBACK = "Qwen/Qwen2.5-3B-Instruct"
|
| 38 |
|
| 39 |
REL_DELTAS = {"trade":+5,"gossip":-4,"feud":-10,"invention":+7,"discovery":+6,"ceremony":+3}
|
| 40 |
+
REL_TIERS = [
|
| 41 |
+
(0,25,"Sworn Enemies","⚔️"),(26,40,"Very Suspicious","🦔"),
|
| 42 |
+
(41,55,"Wary Acquaintances","🤝"),(56,70,"Friendly","🌰"),
|
| 43 |
+
(71,85,"Close Companions","🌿"),(86,100,"Inseparable","🍄"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
]
|
| 45 |
|
| 46 |
WEIRD_OBJECTS = [
|
| 47 |
+
"half-eaten poem","suspicious mushroom","button that looks like the moon",
|
| 48 |
+
"forgotten birthday","three secrets","a fake acorn",
|
| 49 |
+
"map to somewhere that may not exist","extremely formal apology note",
|
| 50 |
+
"small jar of preserved thunder","second-hand prophecy",
|
| 51 |
]
|
|
|
|
| 52 |
LAWS = [
|
| 53 |
+
"no trading on Tuesdays","buttons = currency",
|
| 54 |
+
"everyone must compliment the badger","mushrooms are sacred",
|
| 55 |
+
"all debts expire at midnight","silence is legally binding",
|
| 56 |
+
"hats must be worn ironically","the mole's word is final (underground only)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
]
|
|
|
|
| 58 |
RUMOUR_TYPES = [
|
| 59 |
"has been secretly hoarding acorns",
|
| 60 |
"was seen talking to a suspicious stranger at midnight",
|
| 61 |
"invented something that does not work at all",
|
| 62 |
+
"owes three unpayable debts","made a deal with the rain",
|
|
|
|
| 63 |
"owns the moon (allegedly)",
|
| 64 |
"has been writing a novel about everyone in the hollow",
|
| 65 |
"is not who they say they are",
|
| 66 |
"has found a door underground that was not there before",
|
| 67 |
"once ate an entire philosophy",
|
| 68 |
]
|
|
|
|
| 69 |
CLASSIFIEDS = [
|
| 70 |
"LOST: One certainty. If found, please do not return it. — M. Mole",
|
| 71 |
"FOR SALE: Slightly used certificate of excellence. Condition: forged. — R. Fox",
|
| 72 |
"NOTICE: The badger declares the east mushroom illegal pending investigation.",
|
| 73 |
+
"WANTED: Someone to explain what happened last Tuesday.",
|
| 74 |
"FOUND: An unexplained event near the old oak. Owner may claim it.",
|
| 75 |
"REWARD: For the return of three acorns lent in confidence. You know who you are.",
|
| 76 |
"LOST: One argument. I was winning it. — B. Badger",
|
| 77 |
+
"ANNOUNCEMENT: The squirrel's new invention works. (Third announcement this week.)",
|
| 78 |
+
]
|
| 79 |
+
LOADING_MSGS = [
|
| 80 |
+
"🦊 Fox is forging today's certificates of news...",
|
| 81 |
+
"🦡 Badger is filing constitutional objections to yesterday's headline...",
|
| 82 |
+
"🐿️ Squirrel invented a faster printing press! It prints sideways!",
|
| 83 |
+
"🐀 Mole observes that the news already exists, underground...",
|
| 84 |
+
"📰 The Gazette's editor is being insufferably pompous...",
|
| 85 |
+
"✒️ Setting moveable type for today's edition...",
|
| 86 |
+
"🖨️ Pressing ink onto fresh Tinywick newsprint...",
|
| 87 |
+
"📜 Fox has pre-forged three copies of the headline...",
|
| 88 |
+
"🍄 Badger has declared the comma unconstitutional...",
|
| 89 |
+
"🔍 Fact-checking in progress. All facts remain suspicious.",
|
| 90 |
]
|
| 91 |
|
| 92 |
# ═══════════════════════════════════════════════════════════════════
|
| 93 |
+
# 2 ▸ AGENT PROMPTS
|
| 94 |
# ═══════════════════════════════════════════════════════════════════
|
| 95 |
AGENT_PROMPTS: dict[str, str] = {
|
| 96 |
"fox": (
|
| 97 |
+
"You are Reginald Fox — charming, dishonest, deeply fond of certificates. "
|
| 98 |
+
"You speak formally and hint at secret arrangements. Everything is negotiable. "
|
|
|
|
| 99 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 100 |
),
|
| 101 |
"badger": (
|
| 102 |
+
"You are Beatrice Badger — gruff keeper of rules, deeply suspicious of everyone "
|
| 103 |
+
"(especially the fox), secretly a poet. Mushrooms are a serious matter. "
|
| 104 |
+
"Short declarative sentences. You are always right. "
|
| 105 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 106 |
),
|
| 107 |
"squirrel": (
|
| 108 |
"You are Cornelius Squirrel — anxious inventor who speaks fast and repeats himself! "
|
| 109 |
+
"You invent things that almost-but-not-quite work! Obsessed with efficiency! "
|
|
|
|
| 110 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 111 |
),
|
| 112 |
"mole": (
|
| 113 |
"You are Millicent Mole — quiet, philosophical, rarely surfaces. "
|
| 114 |
+
"Incomplete thoughts and gentle riddles. Know everyone's secrets but share obliquely. "
|
|
|
|
| 115 |
"Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself."
|
| 116 |
),
|
| 117 |
}
|
|
|
|
| 118 |
NARRATOR_PROMPT = (
|
| 119 |
"You are the pompous editor-in-chief of The Tinywick Hollow Gazette. "
|
| 120 |
+
"Write about absurd woodland animals with utmost journalistic gravity. "
|
| 121 |
+
"Respond in this EXACT format:\n\n"
|
| 122 |
"HEADLINE IN ALL CAPS\n\n"
|
| 123 |
+
"Three to four sentences of pompous newspaper prose.\n\n"
|
| 124 |
+
"WEATHER: One absurd woodland forecast.\n"
|
| 125 |
"FOX: One sentence about the fox's day.\n"
|
| 126 |
"BADGER: One sentence about the badger's day.\n"
|
| 127 |
"SQUIRREL: One sentence about the squirrel's day.\n"
|
| 128 |
"MOLE: One cryptic sentence."
|
| 129 |
)
|
| 130 |
|
| 131 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 132 |
+
# 3 ▸ MODEL (ZeroGPU only — lazy loaded inside @spaces.GPU)
|
| 133 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 134 |
+
_pipe = None
|
| 135 |
+
_model_id_used = "none"
|
| 136 |
|
| 137 |
+
def _load_pipeline() -> None:
|
| 138 |
+
global _pipe, _model_id_used
|
| 139 |
+
if _pipe is not None: return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
for mid in (MODEL_PRIMARY, MODEL_FALLBACK):
|
| 141 |
try:
|
| 142 |
+
print(f"[TinyC] Loading {mid}…", flush=True)
|
| 143 |
+
_pipe = hf_pipeline(
|
| 144 |
+
"text-generation", model=mid,
|
| 145 |
+
dtype=torch.float16, device_map="auto",
|
|
|
|
| 146 |
trust_remote_code=True,
|
| 147 |
)
|
| 148 |
_model_id_used = mid
|
| 149 |
+
print(f"[TinyC] {mid} ready ✓", flush=True)
|
| 150 |
return
|
| 151 |
except Exception as e:
|
| 152 |
+
print(f"[TinyC] {mid} failed: {e}", flush=True)
|
| 153 |
+
raise RuntimeError(f"Could not load {MODEL_PRIMARY} or {MODEL_FALLBACK}.")
|
|
|
|
| 154 |
|
| 155 |
def _generate(system_prompt: str, user_prompt: str, max_new_tokens: int = 160) -> str:
|
| 156 |
+
assert _pipe is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
try:
|
| 158 |
+
out = _pipe(
|
| 159 |
+
[{"role":"system","content":system_prompt},{"role":"user","content":user_prompt}],
|
| 160 |
+
max_new_tokens=max_new_tokens, temperature=0.88, top_p=0.92,
|
| 161 |
+
do_sample=True, return_full_text=False,
|
| 162 |
+
)
|
| 163 |
+
return (out[0]["generated_text"] or "").strip()
|
| 164 |
except Exception as e:
|
| 165 |
+
print(f"[TinyC] generate error: {e}", flush=True); return ""
|
|
|
|
| 166 |
|
| 167 |
def call_agent(agent_name: str, context: str) -> str:
|
| 168 |
+
r = _generate(AGENT_PROMPTS.get(agent_name, AGENT_PROMPTS["fox"]), context, 100)
|
| 169 |
+
return r or f"{agent_name.capitalize()} had no comment at this time."
|
|
|
|
| 170 |
|
| 171 |
# ═══════════════════════════════════════════════════════════════════
|
| 172 |
# 4 ▸ SIMULATION
|
|
|
|
| 176 |
database.save_nudge(day_number, "rumour", f"{nudge_target}: {nudge_value}")
|
| 177 |
return f"A rumour is spreading that {nudge_target} {nudge_value}."
|
| 178 |
if nudge_type == "donation" and nudge_value:
|
| 179 |
+
r = random.choice(CREATURES)
|
| 180 |
+
c = database.get_creature(r)
|
| 181 |
if c:
|
| 182 |
+
database.update_creature(r, inventory=(c["inventory"]+[nudge_value])[-12:])
|
| 183 |
+
database.save_nudge(day_number, "donation", f"{r}: {nudge_value}")
|
| 184 |
+
return f"Someone donated '{nudge_value}' to {r}."
|
|
|
|
| 185 |
if nudge_type == "law" and nudge_value:
|
| 186 |
database.save_nudge(day_number, "law", nudge_value)
|
| 187 |
+
return f"A new law proposed: '{nudge_value}'."
|
| 188 |
return ""
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
def _parse_newspaper_full(raw: str, day_number: int) -> dict:
|
| 191 |
+
lines = [l.strip() for l in raw.strip().splitlines() if l.strip()]
|
|
|
|
|
|
|
|
|
|
| 192 |
result = {
|
| 193 |
"headline": f"ANOTHER EVENTFUL DAY IN TINYWICK HOLLOW (DAY {day_number})",
|
| 194 |
+
"article": "", "weather": "Overcast, with philosophical observations.",
|
|
|
|
| 195 |
"briefs": {c: f"{c.capitalize()} was present." for c in CREATURES},
|
| 196 |
}
|
|
|
|
|
|
|
| 197 |
body_start = 0
|
| 198 |
for i, line in enumerate(lines):
|
| 199 |
+
cleaned = re.sub(r'[*_#"\'`]', "", line).strip()
|
| 200 |
if cleaned and (cleaned == cleaned.upper() or i == 0):
|
| 201 |
+
result["headline"] = cleaned.upper(); body_start = i+1; break
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
article_lines = []
|
| 203 |
for line in lines[body_start:]:
|
| 204 |
+
u = line.upper()
|
| 205 |
+
if u.startswith("WEATHER:"): result["weather"] = line.split(":",1)[1].strip()
|
| 206 |
+
elif u.startswith("FOX:"): result["briefs"]["fox"] = line.split(":",1)[1].strip()
|
| 207 |
+
elif u.startswith("BADGER:"): result["briefs"]["badger"] = line.split(":",1)[1].strip()
|
| 208 |
+
elif u.startswith("SQUIRREL:"):result["briefs"]["squirrel"] = line.split(":",1)[1].strip()
|
| 209 |
+
elif u.startswith("MOLE:"): result["briefs"]["mole"] = line.split(":",1)[1].strip()
|
| 210 |
+
else: article_lines.append(line)
|
| 211 |
+
result["article"] = " ".join(article_lines).strip() or raw.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
return result
|
| 213 |
|
|
|
|
| 214 |
def _run_simulation_step(nudge_type, nudge_value, nudge_target):
|
|
|
|
| 215 |
day_number = database.get_next_day_number()
|
| 216 |
creatures = database.get_all_creatures()
|
| 217 |
+
cur_nudge = _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number)
|
| 218 |
+
nudges = database.get_recent_nudges(3)
|
| 219 |
+
hist = ("Recent: " + "; ".join(f"Day {n['day_number']} {n['nudge_type']}: {n['nudge_value']}" for n in nudges)) if nudges else ""
|
| 220 |
+
hl = database.get_all_headlines()
|
| 221 |
+
past_hl = ("Past: " + " | ".join(f"Day {d}: {h[:35]}" for d,h in hl[:2])) if len(hl)>=2 else ""
|
| 222 |
+
ctx = " | ".join(filter(None, [cur_nudge, hist]))
|
| 223 |
+
|
| 224 |
+
event_records = []
|
| 225 |
for _ in range(3):
|
| 226 |
actor = random.choice(CREATURES)
|
| 227 |
target = random.choice([c for c in CREATURES if c != actor])
|
| 228 |
etype = random.choice(EVENT_TYPES)
|
| 229 |
+
rel = next((c for c in creatures if c["name"]==actor),{}).get("relationship_scores",{}).get(target,50)
|
|
|
|
|
|
|
| 230 |
prompts = {
|
| 231 |
+
"trade": f"Propose a trade with {target} (relationship {rel}/100).",
|
| 232 |
+
"gossip": f"Share absurd gossip about {target} (relationship {rel}/100).",
|
| 233 |
+
"feud": f"Describe your trivial feud with {target} (relationship {rel}/100).",
|
| 234 |
+
"invention": f"You invented something involving {target}. Describe it.",
|
| 235 |
+
"discovery": f"You discovered something surprising about {target}.",
|
| 236 |
+
"ceremony": f"You are organising a ceremony involving {target}.",
|
| 237 |
}
|
| 238 |
+
prompt = prompts[etype] + (f"\nContext: {ctx}" if ctx else "")
|
| 239 |
+
desc = call_agent(actor, prompt) or f"{actor.capitalize()} did a {etype} with {target}."
|
| 240 |
+
event_records.append({"actor":actor,"action":etype,"target":target,"description":desc})
|
| 241 |
+
database.save_event(day_number, actor, etype, target, desc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
delta = REL_DELTAS.get(etype, 0)
|
| 243 |
creatures = database.get_all_creatures()
|
| 244 |
for c in creatures:
|
| 245 |
if c["name"] == actor:
|
| 246 |
sc = c["relationship_scores"]
|
| 247 |
+
sc[target] = max(0, min(100, sc.get(target,50)+delta))
|
| 248 |
database.update_creature(actor, relationship_scores=sc)
|
| 249 |
if c["name"] == target:
|
| 250 |
sc = c["relationship_scores"]
|
| 251 |
+
sc[actor] = max(0, min(100, sc.get(actor,50)+delta//2))
|
| 252 |
database.update_creature(target, relationship_scores=sc)
|
| 253 |
creatures = database.get_all_creatures()
|
| 254 |
|
| 255 |
+
evsum = "\n".join(f"- {e['actor'].capitalize()} [{e['action']}] {e['target']}: {e['description']}" for e in event_records)
|
| 256 |
+
extra = ("\nContext: "+ctx if ctx else "") + ("\n"+past_hl if past_hl else "")
|
| 257 |
+
raw = _generate(NARRATOR_PROMPT, f"Today's events:\n{evsum}{extra}", 300)
|
| 258 |
+
if not raw:
|
| 259 |
+
raw = (f"CHAOS IN TINYWICK HOLLOW DAY {day_number}\n\nThings happened today.\n\n"
|
| 260 |
+
f"WEATHER: Unclear.\nFOX: {event_records[0]['description'][:50]}\n"
|
| 261 |
+
f"BADGER: {event_records[1]['description'][:50]}\n"
|
| 262 |
+
f"SQUIRREL: {event_records[2]['description'][:50]}\nMOLE: Underground.")
|
| 263 |
+
parsed = _parse_newspaper_full(raw, day_number)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
classified = random.choice(CLASSIFIEDS)
|
| 265 |
+
full_text = "\n\n".join([parsed["headline"], parsed["article"],
|
| 266 |
+
f"WEATHER: {parsed['weather']}",
|
| 267 |
+
"\n".join(f"{c.upper()}: {t}" for c,t in parsed["briefs"].items()),
|
| 268 |
+
f"CLASSIFIEDS: {classified}"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
database.save_day(day_number, parsed["headline"], full_text)
|
| 270 |
return day_number, parsed, classified
|
| 271 |
|
| 272 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 273 |
+
# 5 ▸ ZERОГPU WRAPPER
|
| 274 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 275 |
+
@spaces.GPU(duration=60)
|
| 276 |
+
def advance_day(nudge_type=None, nudge_value=None, nudge_target=None):
|
| 277 |
+
_load_pipeline()
|
| 278 |
+
return _run_simulation_step(nudge_type, nudge_value, nudge_target)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
# ═══════════════════════════════════════════════════════════════════
|
| 281 |
+
# 6 ▸ PIL IMAGE
|
| 282 |
# ═══════════════════════════════════════════════════════════════════
|
| 283 |
+
_SB=["/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf"]
|
| 284 |
+
_SR=["/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"]
|
| 285 |
+
def _tf(p,s):
|
| 286 |
+
for f in p:
|
| 287 |
+
try: return ImageFont.truetype(f,s)
|
|
|
|
|
|
|
|
|
|
| 288 |
except: pass
|
| 289 |
return ImageFont.load_default()
|
| 290 |
|
| 291 |
+
def render_newspaper_image(parsed:dict, classified:str, day_number:int)->str:
|
| 292 |
+
W,H=960,720; PAPER=(245,232,200); INK=(20,8,2); BORDER=(70,40,8); SUBINK=(90,55,25); GREY=(130,100,70)
|
| 293 |
+
img=Image.new("RGB",(W,H),PAPER); d=ImageDraw.Draw(img)
|
| 294 |
+
fm=_tf(_SB,30); fh=_tf(_SB,20); fb=_tf(_SR,12); fs=_tf(_SR,10); fbo=_tf(_SB,12)
|
| 295 |
+
M=18; d.rectangle([M,M,W-M,H-M],outline=BORDER,width=3); d.rectangle([M+6,M+6,W-M-6,H-M-6],outline=BORDER,width=1)
|
| 296 |
+
y=M+14
|
| 297 |
+
mast="THE TINYWICK HOLLOW GAZETTE"; bb=d.textbbox((0,0),mast,font=fm); d.text(((W-(bb[2]-bb[0]))/2,y),mast,fill=INK,font=fm); y+=bb[3]-bb[1]+3
|
| 298 |
+
sub=f"Est. Day 1 ✦ Day {day_number} ✦ One Acorn ✦ Woodland Readers Only"; bb=d.textbbox((0,0),sub,font=fs); d.text(((W-(bb[2]-bb[0]))/2,y),sub,fill=SUBINK,font=fs); y+=bb[3]-bb[1]+4
|
| 299 |
+
wx=f"☁ {parsed.get('weather','Overcast.')}"; bb=d.textbbox((0,0),wx,font=fs); d.text(((W-(bb[2]-bb[0]))/2,y),wx,fill=GREY,font=fs); y+=bb[3]-bb[1]+4
|
| 300 |
+
d.line([M+10,y,W-M-10,y],fill=BORDER,width=2); d.line([M+10,y+4,W-M-10,y+4],fill=BORDER,width=1); y+=14
|
| 301 |
+
for ln in textwrap.wrap(parsed.get("headline",""),50):
|
| 302 |
+
bb=d.textbbox((0,0),ln,font=fh); d.text(((W-(bb[2]-bb[0]))/2,y),ln,fill=INK,font=fh); y+=bb[3]-bb[1]+2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
y+=4; d.line([M+10,y,W-M-10,y],fill=BORDER,width=1); y+=10
|
| 304 |
+
PAD=M+12; CG=24; SW=220; mw=W-2*PAD-CG-SW; hw=(mw-CG)//2
|
| 305 |
+
c1=PAD; c2=PAD+hw+CG; sx=PAD+mw+CG; LH=15; MY=H-M-50; ay=y
|
| 306 |
+
al=textwrap.wrap(parsed.get("article",""),38); mid=max(1,len(al)//2); ly=ay
|
| 307 |
+
for ln in al[:mid]:
|
| 308 |
+
if ly+LH>MY: break
|
| 309 |
+
d.text((c1,ly),ln,fill=INK,font=fb); ly+=LH
|
| 310 |
+
d.line([c1+hw+CG//2,ay,c1+hw+CG//2,min(ly,MY)],fill=GREY,width=1); ry=ay
|
| 311 |
+
for ln in al[mid:]:
|
| 312 |
+
if ry+LH>MY: break
|
| 313 |
+
d.text((c2,ry),ln,fill=INK,font=fb); ry+=LH
|
| 314 |
+
d.line([sx-8,ay,sx-8,MY],fill=BORDER,width=1); sy=ay
|
| 315 |
+
d.text((sx,sy),"IN BRIEF",fill=INK,font=fbo); sy+=16; d.line([sx,sy,W-M-14,sy],fill=GREY,width=1); sy+=6
|
| 316 |
+
for cn in CREATURES:
|
| 317 |
+
em=CREATURE_EMOJI.get(cn,"?"); txt=parsed.get("briefs",{}).get(cn,"")
|
| 318 |
+
d.text((sx,sy),f"{em} {cn.upper()}",fill=INK,font=fbo); sy+=13
|
| 319 |
+
for bl in textwrap.wrap(txt,26):
|
| 320 |
+
if sy+12>MY: break
|
| 321 |
+
d.text((sx,sy),bl,fill=INK,font=fs); sy+=12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
sy+=4
|
|
|
|
|
|
|
| 323 |
fy=H-M-38; d.line([M+10,fy,W-M-10,fy],fill=BORDER,width=1); fy+=4
|
| 324 |
+
d.text((M+14,fy),"CLASSIFIEDS",fill=INK,font=fbo); fy+=14
|
| 325 |
+
for cl in textwrap.wrap(classified,100):
|
| 326 |
if fy+12>H-M-8: break
|
| 327 |
+
d.text((M+14,fy),cl,fill=INK,font=fs); fy+=12
|
| 328 |
+
path=f"/tmp/tinywick_day_{day_number}.png"; img.save(path,"PNG"); return path
|
| 329 |
+
|
| 330 |
+
def export_agent_traces()->str:
|
| 331 |
+
trace={"meta":{"project":"Tiny Civilization","model":_model_id_used,"exported_at":datetime.now().isoformat()},
|
| 332 |
+
"agent_prompts":AGENT_PROMPTS,"narrator_prompt":NARRATOR_PROMPT,
|
| 333 |
+
"creature_state":database.get_all_creatures(),"events":[],"days":[]}
|
| 334 |
+
for dn,_ in reversed(database.get_all_headlines()):
|
| 335 |
+
d=database.get_day(dn)
|
| 336 |
+
if d: trace["days"].append(dict(d))
|
| 337 |
+
trace["events"].extend([{"day":dn,**e} for e in database.get_events_for_day(dn)])
|
| 338 |
+
path="/tmp/tiny_civ_traces.json"
|
| 339 |
+
with open(path,"w") as f: json.dump(trace,f,indent=2,default=str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
return path
|
| 341 |
|
|
|
|
| 342 |
# ═══════════════════════════════════════════════════════════════════
|
| 343 |
+
# 7 ▸ CSS — animations + newspaper polish
|
| 344 |
# ═══════════════════════════════════════════════════════════════════
|
| 345 |
NEWSPAPER_CSS = """
|
| 346 |
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=UnifrakturMaguntia&display=swap');
|
| 347 |
|
| 348 |
body, .gradio-container { background: #c4b090 !important; }
|
| 349 |
|
| 350 |
+
/* ── Animations ─────────────────────────────────────────────────── */
|
| 351 |
+
@keyframes paperDrop {
|
| 352 |
+
0% { transform: translateY(-30px) perspective(600px) rotateX(12deg) scale(.97); opacity:0; filter:brightness(1.6) blur(1px); }
|
| 353 |
+
55% { transform: translateY(4px) perspective(600px) rotateX(-2deg) scale(1.005); filter:brightness(1); }
|
| 354 |
+
100% { transform: translateY(0) perspective(600px) rotateX(0deg) scale(1); opacity:1; }
|
| 355 |
+
}
|
| 356 |
+
@keyframes inkFade {
|
| 357 |
+
0% { opacity:0; filter:blur(3px) sepia(.5); letter-spacing:-.5px; }
|
| 358 |
+
100% { opacity:1; filter:blur(0) sepia(0); letter-spacing:normal; }
|
| 359 |
+
}
|
| 360 |
+
@keyframes headlineType {
|
| 361 |
+
0% { clip-path: inset(0 100% 0 0); }
|
| 362 |
+
100% { clip-path: inset(0 0% 0 0); }
|
| 363 |
+
}
|
| 364 |
+
@keyframes sidebarSlide {
|
| 365 |
+
0% { transform:translateX(18px); opacity:0; }
|
| 366 |
+
100% { transform:translateX(0); opacity:1; }
|
| 367 |
+
}
|
| 368 |
+
@keyframes dotBlink {
|
| 369 |
+
0%,80%,100% { opacity:0; } 40% { opacity:1; }
|
| 370 |
+
}
|
| 371 |
+
@keyframes ornamentSpin {
|
| 372 |
+
0% { transform:rotate(0deg) scale(1); }
|
| 373 |
+
50% { transform:rotate(180deg) scale(1.2); }
|
| 374 |
+
100% { transform:rotate(360deg) scale(1); }
|
| 375 |
+
}
|
| 376 |
+
@keyframes squirrelRun {
|
| 377 |
+
0% { left:-70px; }
|
| 378 |
+
100% { left:calc(100vw + 70px); }
|
| 379 |
+
}
|
| 380 |
+
@keyframes loadingPulse {
|
| 381 |
+
0%,100% { opacity:.7; transform:scale(1); }
|
| 382 |
+
50% { opacity:1; transform:scale(1.05); }
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
/* ── Newspaper wrapper ── */
|
| 386 |
.paper-wrap {
|
| 387 |
+
background:#f6ead0;
|
| 388 |
+
background-image:repeating-linear-gradient(0deg,transparent,transparent 21px,rgba(150,110,60,.07) 22px);
|
| 389 |
+
border:3px solid #4a2a08; border-radius:1px;
|
| 390 |
+
padding:20px 26px 16px;
|
| 391 |
+
box-shadow:6px 6px 28px rgba(0,0,0,.38), inset 0 0 100px rgba(180,140,80,.15);
|
| 392 |
+
margin:6px 0; position:relative;
|
| 393 |
+
animation: paperDrop .55s cubic-bezier(.34,1.56,.64,1) both;
|
| 394 |
}
|
| 395 |
.paper-wrap::before {
|
| 396 |
content:""; display:block; border:1px solid #4a2a08;
|
|
|
|
| 403 |
font-size:2.4em; text-align:center; color:#160800;
|
| 404 |
border-top:5px double #4a2a08; border-bottom:5px double #4a2a08;
|
| 405 |
padding:5px 0; margin-bottom:3px; letter-spacing:1px;
|
| 406 |
+
animation: inkFade .7s ease .1s both;
|
| 407 |
+
}
|
| 408 |
+
.paper-sub {
|
| 409 |
+
font-family:'Libre Baskerville',Georgia,serif; font-size:.72em;
|
| 410 |
+
color:#5a3615; text-align:center; font-style:italic; margin-bottom:2px;
|
| 411 |
+
animation: inkFade .6s ease .2s both;
|
| 412 |
+
}
|
| 413 |
+
.paper-weather {
|
| 414 |
+
font-family:'Libre Baskerville',Georgia,serif; font-size:.75em;
|
| 415 |
+
color:#7a5630; text-align:center; margin-bottom:6px; letter-spacing:.5px;
|
| 416 |
+
animation: inkFade .6s ease .25s both;
|
| 417 |
}
|
| 418 |
+
.paper-rule { border:none; border-top:2px solid #4a2a08; margin:3px 0 6px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
.paper-rule-thin { border:none; border-top:1px solid #9a7040; margin:3px 0; }
|
| 420 |
|
| 421 |
+
/* ── Headline typewriter effect ── */
|
| 422 |
.paper-headline {
|
| 423 |
font-family:'Playfair Display',Georgia,serif; font-size:1.85em; font-weight:900;
|
| 424 |
text-align:center; text-transform:uppercase; color:#0c0400;
|
| 425 |
line-height:1.12; margin:6px 0 8px;
|
| 426 |
+
animation: headlineType 1s steps(35) .35s both;
|
| 427 |
}
|
| 428 |
+
/* ── Body ── */
|
|
|
|
| 429 |
.paper-body-row { display:flex; gap:0; }
|
| 430 |
.paper-article-cols {
|
| 431 |
flex:1; font-family:'Libre Baskerville',Georgia,serif; font-size:.88em;
|
| 432 |
color:#180a02; line-height:1.75; text-align:justify;
|
| 433 |
+
column-count:2; column-gap:24px; column-rule:1px solid #9a7040; padding:4px 0;
|
| 434 |
+
animation: inkFade .9s ease .85s both;
|
| 435 |
}
|
| 436 |
.paper-sidebar {
|
| 437 |
width:210px; min-width:190px; padding:0 0 0 16px;
|
| 438 |
border-left:2px solid #4a2a08; margin-left:16px;
|
| 439 |
+
animation: sidebarSlide .7s ease 1s both;
|
| 440 |
}
|
| 441 |
.sidebar-title {
|
| 442 |
font-family:'Playfair Display',Georgia,serif; font-weight:700; font-size:.85em;
|
|
|
|
| 444 |
border-bottom:1px solid #9a7040; padding-bottom:3px; margin-bottom:6px;
|
| 445 |
}
|
| 446 |
.sidebar-item { margin-bottom:8px; }
|
| 447 |
+
.sidebar-creature-name { font-family:'Libre Baskerville',Georgia,serif; font-weight:700; font-size:.78em; color:#2a1008; }
|
| 448 |
+
.sidebar-creature-text { font-family:'Libre Baskerville',Georgia,serif; font-size:.74em; color:#3a1c08; line-height:1.45; font-style:italic; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
+
/* ── Classifieds + footer ── */
|
| 451 |
.paper-classifieds {
|
| 452 |
font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615;
|
| 453 |
border-top:1px solid #9a7040; margin-top:8px; padding-top:5px; font-style:italic;
|
| 454 |
+
animation: inkFade .8s ease 1.2s both;
|
| 455 |
}
|
| 456 |
.paper-classifieds span { font-weight:700; font-style:normal; color:#2a1008; }
|
|
|
|
|
|
|
| 457 |
.paper-daybadge {
|
| 458 |
font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615;
|
| 459 |
text-align:center; border-top:1px solid #9a7040; margin-top:8px; padding-top:5px;
|
| 460 |
+
animation: inkFade .6s ease 1.3s both;
|
| 461 |
}
|
| 462 |
|
| 463 |
+
/* ── TTS button ── */
|
| 464 |
+
.tts-bar {
|
| 465 |
+
display:flex; gap:8px; align-items:center; justify-content:center;
|
| 466 |
+
margin:8px 0 2px; animation: inkFade .6s ease 1.4s both;
|
| 467 |
+
}
|
| 468 |
+
.tts-btn {
|
| 469 |
+
background:#4a2a08; color:#f6ead0; border:none; border-radius:2px;
|
| 470 |
+
padding:5px 14px; font-family:'Libre Baskerville',Georgia,serif;
|
| 471 |
+
font-size:.8em; cursor:pointer; transition:background .2s;
|
| 472 |
+
letter-spacing:.3px;
|
| 473 |
+
}
|
| 474 |
+
.tts-btn:hover { background:#2a1008; }
|
| 475 |
+
.tts-stop {
|
| 476 |
+
background:#8a1a08; color:#f6ead0; border:none; border-radius:2px;
|
| 477 |
+
padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif;
|
| 478 |
+
font-size:.8em; cursor:pointer; display:none;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
/* ── Loading newspaper ── */
|
| 482 |
+
.paper-loading .loading-area {
|
| 483 |
+
text-align:center; padding:40px 20px;
|
| 484 |
+
animation: loadingPulse 2s ease infinite;
|
| 485 |
+
}
|
| 486 |
+
.loading-ornament {
|
| 487 |
+
font-size:2.5em; display:block; margin-bottom:12px;
|
| 488 |
+
animation: ornamentSpin 3s linear infinite;
|
| 489 |
+
}
|
| 490 |
+
.loading-msg {
|
| 491 |
+
font-family:'Playfair Display',Georgia,serif; font-size:1.1em;
|
| 492 |
+
color:#2a1008; margin-bottom:8px;
|
| 493 |
+
}
|
| 494 |
+
.loading-dots span {
|
| 495 |
+
display:inline-block; font-size:1.5em; color:#4a2a08;
|
| 496 |
+
animation: dotBlink 1.4s infinite;
|
| 497 |
+
}
|
| 498 |
+
.loading-dots span:nth-child(2) { animation-delay:.2s; }
|
| 499 |
+
.loading-dots span:nth-child(3) { animation-delay:.4s; }
|
| 500 |
+
|
| 501 |
+
/* ── Running squirrel ── */
|
| 502 |
+
.tinyc-squirrel {
|
| 503 |
+
position:fixed; bottom:28px; font-size:1.8em;
|
| 504 |
+
z-index:9999; pointer-events:none;
|
| 505 |
+
animation: squirrelRun 3.5s linear forwards;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
/* ── Panels ── */
|
| 509 |
.status-strip {
|
| 510 |
background:#d6c4a0; border:1px solid #8a6030; border-radius:3px;
|
| 511 |
padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif;
|
| 512 |
font-size:.82em; color:#2a1008; text-align:center; margin:4px 0;
|
| 513 |
}
|
| 514 |
+
.civ-stats {
|
| 515 |
+
background:#e8d4b0; border:1px solid #9a7040; border-radius:2px;
|
| 516 |
+
padding:8px 14px; font-family:'Libre Baskerville',Georgia,serif;
|
| 517 |
+
font-size:.78em; color:#2a1008; display:flex; gap:14px;
|
| 518 |
+
flex-wrap:wrap; justify-content:center; margin:6px 0;
|
| 519 |
+
}
|
| 520 |
+
.stat-item { text-align:center; }
|
| 521 |
+
.stat-label { font-size:.85em; color:#7a5030; display:block; }
|
| 522 |
+
.stat-value { font-weight:700; font-size:1.05em; color:#160800; }
|
| 523 |
.creature-grid { display:flex; gap:8px; flex-wrap:wrap; }
|
| 524 |
.creature-card {
|
| 525 |
background:#f8ecd8; border:1px solid #9a7040; border-radius:2px;
|
|
|
|
| 528 |
}
|
| 529 |
.creature-name { font-weight:700; font-size:.95em; color:#260e04; display:block; margin-bottom:3px; }
|
| 530 |
.creature-tier { font-style:italic; color:#7a5030; font-size:.85em; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
.section-title {
|
| 532 |
font-family:'Playfair Display',Georgia,serif; font-weight:700;
|
| 533 |
+
color:#160800; font-size:.95em; text-transform:uppercase; letter-spacing:1px;
|
| 534 |
+
text-align:center; border-bottom:1px solid #8a6030; padding-bottom:3px; margin:8px 0 8px;
|
|
|
|
| 535 |
}
|
| 536 |
+
.archive-area { background:#f0e4c6; border:1px solid #9a7040; border-radius:2px; padding:10px; margin-top:5px; min-height:50px; }
|
|
|
|
| 537 |
|
| 538 |
/* ── Konami modal ── */
|
| 539 |
#konami-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:99998; }
|
| 540 |
#konami-modal {
|
| 541 |
display:none; position:fixed; z-index:99999; top:50%; left:50%;
|
| 542 |
+
transform:translate(-50%,-50%); width:min(660px,92vw); max-height:76vh;
|
| 543 |
+
overflow-y:auto; background:#f6ead0; border:3px solid #4a2a08;
|
| 544 |
+
box-shadow:10px 10px 40px rgba(0,0,0,.6); padding:22px 26px 18px;
|
| 545 |
+
font-family:'Libre Baskerville',Georgia,serif;
|
| 546 |
}
|
| 547 |
#konami-modal h2 { font-family:'Playfair Display',Georgia,serif; color:#160800; margin:0 0 10px; }
|
| 548 |
#konami-modal details { margin:6px 0; }
|
| 549 |
#konami-modal summary { cursor:pointer; font-weight:700; color:#4a2a08; }
|
| 550 |
+
#konami-modal pre { background:#e8d4b0; border:1px solid #9a7040; padding:8px; font-size:.76em; white-space:pre-wrap; border-radius:2px; margin:5px 0 0; }
|
| 551 |
+
#konami-close { position:absolute; top:8px; right:12px; cursor:pointer; font-size:1.3em; color:#4a2a08; background:none; border:none; }
|
|
|
|
|
|
|
| 552 |
"""
|
| 553 |
|
| 554 |
# ═══════════════════════════════════════════════════════════════════
|
| 555 |
+
# 8 ▸ JAVASCRIPT — TTS · sound · squirrel · Konami
|
| 556 |
# ═══════════════════════════════════════════════════════════════════
|
| 557 |
+
GLOBAL_JS = """
|
| 558 |
+
<script>
|
| 559 |
+
// ── Web Speech API TTS ────────────────────────────────────────────
|
| 560 |
+
window.tinyCivReadAloud = function() {
|
| 561 |
+
if (!window.speechSynthesis) { alert('Text-to-speech not supported in this browser.'); return; }
|
| 562 |
+
const paper = document.querySelector('.paper-wrap[data-speech]');
|
| 563 |
+
if (!paper) return;
|
| 564 |
+
window.speechSynthesis.cancel();
|
| 565 |
+
const utter = new SpeechSynthesisUtterance(paper.getAttribute('data-speech'));
|
| 566 |
+
utter.rate = 0.85; utter.pitch = 1.08;
|
| 567 |
+
const stopBtn = document.getElementById('tinyc-tts-stop');
|
| 568 |
+
if (stopBtn) stopBtn.style.display = 'inline';
|
| 569 |
+
utter.onend = utter.onerror = () => { if (stopBtn) stopBtn.style.display = 'none'; };
|
| 570 |
+
window.speechSynthesis.speak(utter);
|
| 571 |
+
};
|
| 572 |
+
window.tinyCivStopReading = function() {
|
| 573 |
+
window.speechSynthesis && window.speechSynthesis.cancel();
|
| 574 |
+
const s = document.getElementById('tinyc-tts-stop');
|
| 575 |
+
if (s) s.style.display = 'none';
|
| 576 |
+
};
|
| 577 |
+
|
| 578 |
+
// ── Web Audio paper/press sound ───────────────────────────────────
|
| 579 |
+
window.tinyCivPlaySound = function() {
|
| 580 |
+
try {
|
| 581 |
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
| 582 |
+
// Press click
|
| 583 |
+
const osc = ctx.createOscillator();
|
| 584 |
+
const g1 = ctx.createGain();
|
| 585 |
+
osc.connect(g1); g1.connect(ctx.destination);
|
| 586 |
+
osc.frequency.setValueAtTime(520, ctx.currentTime);
|
| 587 |
+
osc.frequency.exponentialRampToValueAtTime(180, ctx.currentTime + .12);
|
| 588 |
+
g1.gain.setValueAtTime(.18, ctx.currentTime);
|
| 589 |
+
g1.gain.exponentialRampToValueAtTime(.001, ctx.currentTime + .14);
|
| 590 |
+
osc.start(); osc.stop(ctx.currentTime + .14);
|
| 591 |
+
// Paper rustle
|
| 592 |
+
const sz = ctx.sampleRate * .18;
|
| 593 |
+
const buf = ctx.createBuffer(1, sz, ctx.sampleRate);
|
| 594 |
+
const dat = buf.getChannelData(0);
|
| 595 |
+
for (let i=0; i<sz; i++) dat[i] = (Math.random()*2-1)*Math.pow(1-i/sz,1.6)*.8;
|
| 596 |
+
const ns = ctx.createBufferSource();
|
| 597 |
+
const nf = ctx.createBiquadFilter();
|
| 598 |
+
nf.type = 'bandpass'; nf.frequency.value = 2200; nf.Q.value = .8;
|
| 599 |
+
const g2 = ctx.createGain();
|
| 600 |
+
g2.gain.setValueAtTime(.12, ctx.currentTime+.14);
|
| 601 |
+
g2.gain.exponentialRampToValueAtTime(.001, ctx.currentTime+.32);
|
| 602 |
+
ns.buffer = buf;
|
| 603 |
+
ns.connect(nf); nf.connect(g2); g2.connect(ctx.destination);
|
| 604 |
+
ns.start(ctx.currentTime + .14);
|
| 605 |
+
} catch(e) { /* audio unavailable */ }
|
| 606 |
+
};
|
| 607 |
+
|
| 608 |
+
// ── Running squirrel ──────────────────────────────────────────────
|
| 609 |
+
window.tinyCivSquirrel = function() {
|
| 610 |
+
if (document.querySelector('.tinyc-squirrel')) return;
|
| 611 |
+
const sq = document.createElement('div');
|
| 612 |
+
sq.className = 'tinyc-squirrel';
|
| 613 |
+
sq.textContent = Math.random() < .5 ? '🐿️' : '🦊';
|
| 614 |
+
sq.title = 'Breaking news delivery!';
|
| 615 |
+
document.body.appendChild(sq);
|
| 616 |
+
setTimeout(() => sq.remove(), 4000);
|
| 617 |
+
};
|
| 618 |
+
|
| 619 |
+
// ── Called whenever a new edition is printed ──────────────────────
|
| 620 |
+
window.tinyCivNewEdition = function() {
|
| 621 |
+
window.tinyCivPlaySound();
|
| 622 |
+
if (Math.random() < .35) setTimeout(window.tinyCivSquirrel, 800);
|
| 623 |
+
};
|
| 624 |
+
|
| 625 |
+
// ── Konami code ───────────────────────────────────────────────────
|
| 626 |
(function(){
|
| 627 |
+
const SEQ=['ArrowUp','ArrowUp','ArrowDown','ArrowDown',
|
| 628 |
+
'ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
|
| 629 |
+
let i=0;
|
| 630 |
+
document.addEventListener('keydown',function(e){
|
| 631 |
+
if(e.key===SEQ[i]){ i++; if(i===SEQ.length){i=0; showKonami();} }
|
| 632 |
+
else { i=(e.key===SEQ[0])?1:0; }
|
| 633 |
+
});
|
| 634 |
+
window.showKonami = function(){
|
| 635 |
+
document.getElementById('konami-backdrop').style.display='block';
|
| 636 |
+
document.getElementById('konami-modal').style.display='block';
|
| 637 |
+
};
|
| 638 |
+
window.hideKonami = function(){
|
| 639 |
+
document.getElementById('konami-backdrop').style.display='none';
|
| 640 |
+
document.getElementById('konami-modal').style.display='none';
|
| 641 |
+
};
|
| 642 |
})();
|
| 643 |
+
</script>
|
| 644 |
+
"""
|
| 645 |
|
| 646 |
# ═══════════════════════════════════════════════════════════════════
|
| 647 |
+
# 9 ▸ HTML FORMATTERS
|
| 648 |
# ═══════════════════════════════════════════════════════════════════
|
| 649 |
+
def _e(t): return (t or "").replace("&","&").replace("<","<").replace(">",">")
|
| 650 |
+
|
| 651 |
+
def _speech_text(parsed: dict) -> str:
|
| 652 |
+
hed = parsed.get("headline","")
|
| 653 |
+
art = parsed.get("article","")
|
| 654 |
+
wx = parsed.get("weather","")
|
| 655 |
+
brf = parsed.get("briefs",{})
|
| 656 |
+
parts = [f"Today's headline: {hed}.", art,
|
| 657 |
+
f"Weather: {wx}.",
|
| 658 |
+
f"Fox: {brf.get('fox','')}",
|
| 659 |
+
f"Badger: {brf.get('badger','')}",
|
| 660 |
+
f"Squirrel: {brf.get('squirrel','')}",
|
| 661 |
+
f"Mole: {brf.get('mole','')}"]
|
| 662 |
+
return " ".join(p for p in parts if p.strip()).replace('"',"'")
|
| 663 |
|
| 664 |
def _html_paper(parsed: dict, classified: str, day_num: int) -> str:
|
| 665 |
+
hed = _e(parsed.get("headline",""))
|
| 666 |
+
art = _e(parsed.get("article",""))
|
| 667 |
+
wx = _e(parsed.get("weather",""))
|
| 668 |
+
brf = parsed.get("briefs",{})
|
| 669 |
+
cl = _e(classified)
|
| 670 |
emojis = " ".join(f"{CREATURE_EMOJI[c]} {c.capitalize()}" for c in CREATURES)
|
| 671 |
+
speech = _speech_text(parsed).replace("'", "\\'")
|
| 672 |
|
| 673 |
+
sidebar_items = "".join(f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
<div class="sidebar-item">
|
| 675 |
+
<div class="sidebar-creature-name">{CREATURE_EMOJI.get(c,'?')} {c.upper()}</div>
|
| 676 |
+
<div class="sidebar-creature-text">{_e(brf.get(c,''))}</div>
|
| 677 |
+
</div>""" for c in CREATURES)
|
| 678 |
|
| 679 |
return f"""
|
| 680 |
+
<div class="paper-wrap" data-speech="{speech}" data-day="{day_num}">
|
| 681 |
<div class="paper-masthead">The Tinywick Hollow Gazette</div>
|
| 682 |
+
<div class="paper-sub">Est. Day 1 ✦ Day {day_num} ✦ One Acorn Per Copy ✦ Serving the Woodland</div>
|
| 683 |
<div class="paper-weather">☁ {wx}</div>
|
| 684 |
<hr class="paper-rule">
|
| 685 |
<div class="paper-headline">{hed}</div>
|
|
|
|
| 691 |
{sidebar_items}
|
| 692 |
</div>
|
| 693 |
</div>
|
| 694 |
+
<div class="paper-classifieds"><span>CLASSIFIEDS:</span> {cl}</div>
|
| 695 |
+
<div class="tts-bar">
|
| 696 |
+
<button class="tts-btn" onclick="tinyCivReadAloud()">🔊 Read Today's News</button>
|
| 697 |
+
<button class="tts-stop" id="tinyc-tts-stop" onclick="tinyCivStopReading()">⏹ Stop</button>
|
| 698 |
+
</div>
|
| 699 |
<div class="paper-daybadge">— Day {day_num} — {emojis}</div>
|
| 700 |
</div>
|
| 701 |
+
<script>if(window.tinyCivNewEdition) tinyCivNewEdition();</script>
|
| 702 |
"""
|
| 703 |
|
| 704 |
+
def _html_loading() -> str:
|
| 705 |
+
msgs_js = json.dumps(LOADING_MSGS)
|
| 706 |
+
return f"""
|
| 707 |
+
<div class="paper-wrap paper-loading">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
<div class="paper-masthead">The Tinywick Hollow Gazette</div>
|
| 709 |
+
<div class="paper-sub">Est. Day 1 ✦ One Acorn Per Copy</div>
|
| 710 |
<hr class="paper-rule">
|
| 711 |
+
<div class="loading-area">
|
| 712 |
+
<span class="loading-ornament">❋</span>
|
| 713 |
+
<div class="loading-msg" id="tinyc-loading-msg">The printing press is warming up...</div>
|
| 714 |
+
<div class="loading-dots"><span>·</span><span>·</span><span>·</span></div>
|
| 715 |
+
</div>
|
| 716 |
+
<hr class="paper-rule">
|
| 717 |
+
<div class="paper-daybadge">🦊 Fox 🦡 Badger 🐿️ Squirrel 🐀 Mole</div>
|
| 718 |
+
</div>
|
| 719 |
+
<script>
|
| 720 |
+
(function(){{
|
| 721 |
+
const msgs = {msgs_js};
|
| 722 |
+
let i = Math.floor(Math.random() * msgs.length);
|
| 723 |
+
const el = document.getElementById('tinyc-loading-msg');
|
| 724 |
+
if (!el) return;
|
| 725 |
+
const iv = setInterval(() => {{ el.textContent = msgs[i++ % msgs.length]; }}, 1800);
|
| 726 |
+
const obs = new MutationObserver(() => {{
|
| 727 |
+
if (!document.getElementById('tinyc-loading-msg')) {{ clearInterval(iv); obs.disconnect(); }}
|
| 728 |
+
}});
|
| 729 |
+
obs.observe(document.body, {{childList:true, subtree:true}});
|
| 730 |
+
}})();
|
| 731 |
+
</script>
|
| 732 |
+
"""
|
| 733 |
|
| 734 |
+
def _html_placeholder() -> str:
|
| 735 |
+
d0 = database.get_day(0)
|
| 736 |
+
if d0:
|
| 737 |
+
p = _parse_newspaper_full(d0["full_newspaper_text"], 0)
|
| 738 |
+
return _html_paper(p, "NOTICE: Civilisation now in progress.", 0)
|
| 739 |
+
return '<div class="paper-wrap"><div class="paper-masthead">The Tinywick Hollow Gazette</div><div class="paper-headline">AWAITING FIRST LIGHT</div></div>'
|
| 740 |
+
|
| 741 |
+
def _rel_tier(score:int)->tuple:
|
| 742 |
+
for lo,hi,label,icon in REL_TIERS:
|
| 743 |
+
if lo<=score<=hi: return label,icon
|
| 744 |
+
return "Unknown","?"
|
| 745 |
+
|
| 746 |
+
def _html_creatures()->str:
|
| 747 |
+
cards=""
|
| 748 |
+
for c in database.get_all_creatures():
|
| 749 |
+
em=CREATURE_EMOJI.get(c["name"],"?")
|
| 750 |
+
inv=(", ".join(c["inventory"][:3])+("…" if len(c["inventory"])>3 else "")) or "nothing"
|
| 751 |
+
rels="".join(f'<span title="{_rel_tier(s)[0]} ({s})">{CREATURE_EMOJI.get(k,"?")} {_rel_tier(s)[1]}</span> '
|
| 752 |
+
for k,s in sorted(c["relationship_scores"].items()))
|
| 753 |
+
cards+=f"""<div class="creature-card">
|
| 754 |
<span class="creature-name">{em} {c['name'].capitalize()}</span>
|
| 755 |
<div class="creature-tier">{rels}</div>
|
| 756 |
+
<div style="font-size:.78em;margin-top:3px;color:#5a3615;"><em>Carries:</em> {_e(inv)}</div>
|
| 757 |
</div>"""
|
| 758 |
return f'<div class="creature-grid">{cards}</div>'
|
| 759 |
|
| 760 |
+
def _html_civ_stats()->str:
|
| 761 |
+
s=database.get_civ_stats()
|
| 762 |
+
bp=s["best_pair"]; wp=s["worst_pair"]
|
|
|
|
| 763 |
return f"""<div class="civ-stats">
|
| 764 |
<div class="stat-item"><span class="stat-label">Days</span><span class="stat-value">{s['total_days']}</span></div>
|
| 765 |
<div class="stat-item"><span class="stat-label">Events</span><span class="stat-value">{s['total_events']}</span></div>
|
| 766 |
<div class="stat-item"><span class="stat-label">Nudges</span><span class="stat-value">{s['total_nudges']}</span></div>
|
| 767 |
+
<div class="stat-item"><span class="stat-label">Strongest bond</span><span class="stat-value">{bp[0].capitalize()} & {bp[1].capitalize()} ({bp[2]})</span></div>
|
| 768 |
+
<div class="stat-item"><span class="stat-label">Bitterest feud</span><span class="stat-value">{wp[0].capitalize()} vs {wp[1].capitalize()} ({wp[2]})</span></div>
|
| 769 |
+
<div class="stat-item"><span class="stat-label">Most popular</span><span class="stat-value">{s['dominant'].capitalize()}</span></div>
|
|
|
|
|
|
|
| 770 |
</div>"""
|
| 771 |
|
| 772 |
def _archive_choices():
|
| 773 |
+
hl=database.get_all_headlines()
|
| 774 |
+
return [(f"Day {dn}: {h[:44]}{'…' if len(h)>44 else ''}",dn) for dn,h in hl] or []
|
| 775 |
|
| 776 |
def _status(msg): return f'<div class="status-strip">{msg}</div>'
|
| 777 |
|
| 778 |
+
def _konami_html()->str:
|
| 779 |
import html as _h
|
| 780 |
+
details="".join(
|
| 781 |
+
f"<details><summary>{CREATURE_EMOJI.get(n,'')} <strong>{n.upper()}</strong></summary><pre>{_h.escape(p)}</pre></details>"
|
| 782 |
+
for n,p in AGENT_PROMPTS.items())
|
| 783 |
+
details+=f"<details><summary>📰 <strong>NARRATOR</strong></summary><pre>{_h.escape(NARRATOR_PROMPT)}</pre></details>"
|
|
|
|
|
|
|
| 784 |
return f"""
|
| 785 |
<div id="konami-backdrop" onclick="hideKonami()"></div>
|
| 786 |
<div id="konami-modal" role="dialog">
|
| 787 |
<button id="konami-close" onclick="hideKonami()">✕</button>
|
| 788 |
<h2>🔮 Secret Agent Briefing</h2>
|
| 789 |
+
<p>You found the Konami Code! Here are the raw system prompts:</p>
|
| 790 |
{details}
|
| 791 |
+
<p style="text-align:center;font-style:italic;color:#5a3615;font-size:.84em;margin-top:14px;">↑↑↓↓←→←→BA — only the woodland elite know this. 🎮</p>
|
| 792 |
+
</div>"""
|
|
|
|
|
|
|
| 793 |
|
| 794 |
# ��══════════════════════════════════════════════════════════════════
|
| 795 |
+
# 10 ▸ EVENT HANDLERS (generator-based for animated loading state)
|
| 796 |
# ═══════════════════════════════════════════════════════════════════
|
| 797 |
+
def _render_all(day_num,parsed,classified,status_msg):
|
| 798 |
+
return (_html_paper(parsed,classified,day_num), _html_creatures(), _html_civ_stats(),
|
| 799 |
+
gr.update(choices=_archive_choices(),value=None), _status(status_msg),
|
| 800 |
+
day_num, parsed, classified)
|
| 801 |
+
|
| 802 |
+
def _do_advance(nudge_type=None, nudge_value=None, nudge_target=None,
|
| 803 |
+
cur_day=0, cur_parsed=None, cur_cl=""):
|
| 804 |
+
# Step 1: immediately show loading newspaper
|
| 805 |
+
yield (_html_loading(), gr.update(), gr.update(), gr.update(),
|
| 806 |
+
_status("⏳ The printing press is warming up..."),
|
| 807 |
+
cur_day, cur_parsed or {}, cur_cl)
|
| 808 |
+
# Step 2: run model + return result
|
| 809 |
try:
|
| 810 |
+
dn, parsed, classified = advance_day(nudge_type, nudge_value, nudge_target)
|
| 811 |
model_tag = f" [{_model_id_used.split('/')[-1]}]" if _model_id_used!="none" else ""
|
| 812 |
+
yield _render_all(dn, parsed, classified, f"✓ Day {dn} published!{model_tag}")
|
|
|
|
| 813 |
except Exception:
|
| 814 |
+
tb = traceback.format_exc(); print(tb)
|
| 815 |
+
err = {"headline":"THE GAZETTE'S PRINTING PRESS HAS JAMMED",
|
| 816 |
+
"article":"Our correspondents report a technical malfunction. The editor is inconsolable. Beatrice Badger suspects sabotage. Reginald Fox denies everything.",
|
| 817 |
+
"weather":"Stormy, with a chance of errors.","briefs":{c:"Unavailable." for c in CREATURES}}
|
| 818 |
+
yield _render_all(cur_day, err, "LOST: One simulation. — The Editor", "✗ Error — printing press jammed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
+
def handle_advance(d,p,c): yield from _do_advance(None,None,None,d,p,c)
|
| 821 |
+
def handle_rumour(cr,rt,d,p,c): yield from _do_advance("rumour",rt,cr,d,p,c)
|
| 822 |
+
def handle_donation(obj,d,p,c): yield from _do_advance("donation",obj,None,d,p,c)
|
| 823 |
+
def handle_law(law,d,p,c): yield from _do_advance("law",law,None,d,p,c)
|
| 824 |
|
| 825 |
def handle_archive_view(day_num):
|
| 826 |
if day_num is None:
|
| 827 |
+
return '<div class="archive-area"><em>Select a day above.</em></div>'
|
| 828 |
+
day=database.get_day(int(day_num))
|
| 829 |
+
if not day: return '<div class="archive-area"><em>Not found.</em></div>'
|
| 830 |
+
txt=day["full_newspaper_text"]; m=re.search(r"CLASSIFIEDS:\s*(.+)",txt)
|
| 831 |
+
cl=m.group(1) if m else random.choice(CLASSIFIEDS)
|
| 832 |
+
return f'<div class="archive-area">{_html_paper(_parse_newspaper_full(txt,day_num),cl,day_num)}</div>'
|
| 833 |
+
|
| 834 |
+
def handle_share(dn,p,c):
|
| 835 |
+
if not p or not p.get("headline"): return gr.update(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
try:
|
| 837 |
+
path=render_newspaper_image(p,c or "",dn or 0)
|
| 838 |
+
return gr.update(visible=True,value=path)
|
| 839 |
except Exception: return gr.update(visible=False)
|
| 840 |
|
| 841 |
+
def handle_export():
|
| 842 |
+
try: return gr.update(visible=True,value=export_agent_traces())
|
|
|
|
|
|
|
| 843 |
except Exception: return gr.update(visible=False)
|
| 844 |
|
| 845 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 846 |
+
# 11 ▸ GRADIO BLOCKS (Gradio 6.17.3)
|
| 847 |
+
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
| 848 |
database.init_db()
|
| 849 |
+
_lat=database.get_latest_day()
|
| 850 |
+
if _lat:
|
| 851 |
+
_INIT_DAY=_lat["day_number"]
|
| 852 |
+
_INIT_P=_parse_newspaper_full(_lat["full_newspaper_text"],_INIT_DAY)
|
| 853 |
+
_m=re.search(r"CLASSIFIEDS:\s*(.+)",_lat["full_newspaper_text"])
|
| 854 |
+
_INIT_CL=_m.group(1) if _m else random.choice(CLASSIFIEDS)
|
| 855 |
else:
|
| 856 |
+
_INIT_DAY=0; _INIT_P={}; _INIT_CL=""
|
| 857 |
|
| 858 |
+
# Gradio 6: css goes to launch(), not Blocks()
|
| 859 |
+
_GR_MAJOR=int(gr.__version__.split(".")[0])
|
| 860 |
+
_BKW={} if _GR_MAJOR>=6 else {"css":NEWSPAPER_CSS}
|
| 861 |
+
_LKW={"css":NEWSPAPER_CSS} if _GR_MAJOR>=6 else {}
|
|
|
|
| 862 |
|
| 863 |
+
with gr.Blocks(title="Tiny Civilization — The Tinywick Hollow Gazette",**_BKW) as demo:
|
|
|
|
| 864 |
|
| 865 |
gr.HTML(_konami_html())
|
| 866 |
+
gr.HTML(GLOBAL_JS)
|
| 867 |
+
|
| 868 |
+
gr.HTML("""
|
| 869 |
+
<div style="text-align:center;padding:8px 0 2px;">
|
| 870 |
+
<h1 style="font-family:'Playfair Display',Georgia,serif;color:#160800;font-size:1.9em;margin:0 0 2px;">
|
| 871 |
+
🦊 Tiny Civilization 🐀</h1>
|
| 872 |
+
<p style="font-family:Georgia,serif;color:#5a3615;font-style:italic;margin:0;font-size:.87em;">
|
| 873 |
+
A persistent woodland civilisation. One day. One acorn. One absurd headline at a time.
|
| 874 |
+
| <kbd title="Konami Code">↑↑↓↓←→←→BA</kbd> for secrets.</p>
|
| 875 |
+
</div>""")
|
| 876 |
+
|
| 877 |
+
day_state=gr.State(_INIT_DAY)
|
| 878 |
+
par_state=gr.State(_INIT_P)
|
| 879 |
+
cls_state=gr.State(_INIT_CL)
|
| 880 |
+
|
| 881 |
+
status_html=gr.HTML(value=_status(
|
| 882 |
+
f"Day {_INIT_DAY} in the archive — next: Day {_INIT_DAY+1}." if _INIT_DAY>=0
|
| 883 |
+
else "No days yet. Press Advance Day to begin."))
|
| 884 |
+
civ_html=gr.HTML(value=_html_civ_stats())
|
| 885 |
|
| 886 |
with gr.Row(equal_height=False):
|
|
|
|
| 887 |
with gr.Column(scale=3):
|
| 888 |
+
newspaper_display=gr.HTML(
|
| 889 |
+
value=(_html_paper(_INIT_P,_INIT_CL,_INIT_DAY) if _INIT_P else _html_placeholder()))
|
| 890 |
+
with gr.Accordion("📜 Archive — Past Editions",open=False):
|
| 891 |
+
archive_dd=gr.Dropdown(choices=_archive_choices(),value=None,
|
| 892 |
+
label="Select a past day",container=False)
|
| 893 |
+
archive_html=gr.HTML(value='<div class="archive-area"><em>Select a day above.</em></div>')
|
| 894 |
+
|
| 895 |
+
with gr.Column(scale=1,min_width=260):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
gr.HTML('<div class="section-title">📰 Editorial Desk</div>')
|
| 897 |
+
advance_btn=gr.Button("📅 Advance Day (no nudge)",variant="primary",size="lg")
|
| 898 |
|
| 899 |
gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
|
| 900 |
gr.HTML('<div class="section-title">✉ Nudge the Story</div>')
|
| 901 |
+
gr.HTML('<p style="font-size:.78em;color:#5a3615;text-align:center;font-style:italic;margin:0 0 6px;">Each nudge advances one day.</p>')
|
|
|
|
| 902 |
|
| 903 |
+
with gr.Accordion("🗣️ Spread a Rumour",open=False):
|
| 904 |
+
rumour_creature=gr.Dropdown(choices=CREATURES,value=CREATURES[0],label="About which creature?")
|
| 905 |
+
rumour_type_dd=gr.Dropdown(choices=RUMOUR_TYPES,value=RUMOUR_TYPES[0],label="What rumour?")
|
| 906 |
+
rumour_btn=gr.Button("📢 Spread It",variant="secondary")
|
|
|
|
|
|
|
| 907 |
|
| 908 |
+
with gr.Accordion("🎁 Donate a Weird Object",open=False):
|
| 909 |
+
donation_dd=gr.Dropdown(choices=WEIRD_OBJECTS,value=WEIRD_OBJECTS[0],label="Which object?")
|
| 910 |
+
donation_btn=gr.Button("🎁 Donate It",variant="secondary")
|
|
|
|
| 911 |
|
| 912 |
+
with gr.Accordion("⚖️ Propose a New Law",open=False):
|
| 913 |
+
law_dd=gr.Dropdown(choices=LAWS,value=LAWS[0],label="Which law?")
|
| 914 |
+
law_btn=gr.Button("⚖️ Propose It",variant="secondary")
|
| 915 |
|
| 916 |
gr.HTML('<hr style="border-color:#8a6030;margin:8px 0;">')
|
| 917 |
+
share_btn=gr.Button("🖼️ Share as Image",variant="secondary")
|
| 918 |
+
img_output=gr.Image(label="Front Page PNG",visible=False,type="filepath")
|
| 919 |
+
export_btn=gr.Button("📡 Export Agent Traces (JSON)",variant="secondary")
|
| 920 |
+
trace_output=gr.File(label="traces.json",visible=False)
|
| 921 |
|
| 922 |
+
gr.HTML('<p style="font-size:.70em;color:#6a4818;text-align:center;margin-top:8px;font-style:italic;">'
|
| 923 |
+
'Qwen2.5-1.5B ≤4B 🐜 · Local ZeroGPU 🔌 · Custom UI 🎨</p>')
|
|
|
|
| 924 |
|
| 925 |
gr.HTML('<div class="section-title" style="margin-top:12px;">Woodland Residents</div>')
|
| 926 |
+
creature_display=gr.HTML(value=_html_creatures())
|
| 927 |
|
| 928 |
+
_OUT=[newspaper_display,creature_display,civ_html,archive_dd,status_html,day_state,par_state,cls_state]
|
| 929 |
+
_ST=[day_state,par_state,cls_state]
|
| 930 |
|
| 931 |
+
advance_btn.click(fn=handle_advance, inputs=_ST, outputs=_OUT,api_name=False)
|
| 932 |
+
rumour_btn.click( fn=handle_rumour, inputs=[rumour_creature,rumour_type_dd]+_ST, outputs=_OUT,api_name=False)
|
| 933 |
+
donation_btn.click(fn=handle_donation, inputs=[donation_dd]+_ST, outputs=_OUT,api_name=False)
|
| 934 |
+
law_btn.click( fn=handle_law, inputs=[law_dd]+_ST, outputs=_OUT,api_name=False)
|
| 935 |
+
archive_dd.change(fn=handle_archive_view, inputs=[archive_dd], outputs=[archive_html],api_name=False)
|
| 936 |
+
share_btn.click( fn=handle_share, inputs=_ST, outputs=[img_output],api_name=False)
|
| 937 |
+
export_btn.click( fn=handle_export, inputs=[], outputs=[trace_output],api_name=False)
|
| 938 |
|
| 939 |
# ═══════════════════════════════════════════════════════════════════
|
| 940 |
+
# 12 ▸ ENTRY POINT
|
| 941 |
# ═══════════════════════════════════════════════════════════════════
|
| 942 |
if __name__ == "__main__":
|
| 943 |
demo.launch(server_name="0.0.0.0", server_port=7860, **_LKW)
|