""" app.py โ€” Tiny Civilization: The Tinywick Hollow Gazette ๐Ÿ„ Persistent woodland civilisation ยท Qwen2.5-1.5B ยท ZeroGPU ยท Gradio 6 Build Small Hackathon 2026 โ€” Thousand Token Wood track """ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 0 โ–ธ IMPORTS # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• from __future__ import annotations import json, os, random, re, textwrap, traceback from datetime import datetime import gradio as gr from PIL import Image, ImageDraw, ImageFont import database try: import spaces _ZERO_GPU = True except ImportError: class _Stub: @staticmethod def GPU(fn=None, *, duration=60): return fn if callable(fn) else (lambda f: f) spaces = _Stub(); _ZERO_GPU = False # type: ignore import torch from transformers import pipeline as hf_pipeline # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 1 โ–ธ CONSTANTS # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• CREATURES = ["fox", "badger", "squirrel", "mole"] EVENT_TYPES = ["trade", "gossip", "feud", "invention", "discovery", "ceremony"] CREATURE_EMOJI = {"fox":"๐ŸฆŠ","badger":"๐Ÿฆก","squirrel":"๐Ÿฟ๏ธ","mole":"๐Ÿ€"} MODEL_PRIMARY = "Qwen/Qwen2.5-1.5B-Instruct" MODEL_FALLBACK = "Qwen/Qwen2.5-3B-Instruct" REL_DELTAS = {"trade":+5,"gossip":-4,"feud":-10,"invention":+7,"discovery":+6,"ceremony":+3} REL_TIERS = [ (0,25,"Sworn Enemies","โš”๏ธ"),(26,40,"Very Suspicious","๐Ÿฆ”"), (41,55,"Wary Acquaintances","๐Ÿค"),(56,70,"Friendly","๐ŸŒฐ"), (71,85,"Close Companions","๐ŸŒฟ"),(86,100,"Inseparable","๐Ÿ„"), ] WEIRD_OBJECTS = [ "half-eaten poem","suspicious mushroom","button that looks like the moon", "forgotten birthday","three secrets","a fake acorn", "map to somewhere that may not exist","extremely formal apology note", "small jar of preserved thunder","second-hand prophecy", ] LAWS = [ "no trading on Tuesdays","buttons = currency", "everyone must compliment the badger","mushrooms are sacred", "all debts expire at midnight","silence is legally binding", "hats must be worn ironically","the mole's word is final (underground only)", ] RUMOUR_TYPES = [ "has been secretly hoarding acorns", "was seen talking to a suspicious stranger at midnight", "invented something that does not work at all", "owes three unpayable debts","made a deal with the rain", "owns the moon (allegedly)", "has been writing a novel about everyone in the hollow", "is not who they say they are", "has found a door underground that was not there before", "once ate an entire philosophy", ] CLASSIFIEDS = [ "LOST: One certainty. If found, please do not return it. โ€” M. Mole", "FOR SALE: Slightly used certificate of excellence. Condition: forged. โ€” R. Fox", "NOTICE: The badger declares the east mushroom illegal pending investigation.", "WANTED: Someone to explain what happened last Tuesday.", "FOUND: An unexplained event near the old oak. Owner may claim it.", "REWARD: For the return of three acorns lent in confidence. You know who you are.", "LOST: One argument. I was winning it. โ€” B. Badger", "ANNOUNCEMENT: The squirrel's new invention works. (Third announcement this week.)", ] LOADING_MSGS = [ "๐ŸฆŠ Fox is forging today's certificates of news...", "๐Ÿฆก Badger is filing constitutional objections to yesterday's headline...", "๐Ÿฟ๏ธ Squirrel invented a faster printing press! It prints sideways!", "๐Ÿ€ Mole observes that the news already exists, underground...", "๐Ÿ“ฐ The Gazette's editor is being insufferably pompous...", "โœ’๏ธ Setting moveable type for today's edition...", "๐Ÿ–จ๏ธ Pressing ink onto fresh Tinywick newsprint...", "๐Ÿ“œ Fox has pre-forged three copies of the headline...", "๐Ÿ„ Badger has declared the comma unconstitutional...", "๐Ÿ” Fact-checking in progress. All facts remain suspicious.", ] # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 2 โ–ธ AGENT PROMPTS # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• AGENT_PROMPTS: dict[str, str] = { "fox": ( "You are Reginald Fox โ€” charming, dishonest, deeply fond of certificates. " "You speak formally and hint at secret arrangements. Everything is negotiable. " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), "badger": ( "You are Beatrice Badger โ€” gruff keeper of rules, deeply suspicious of everyone " "(especially the fox), secretly a poet. Mushrooms are a serious matter. " "Short declarative sentences. You are always right. " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), "squirrel": ( "You are Cornelius Squirrel โ€” anxious inventor who speaks fast and repeats himself! " "You invent things that almost-but-not-quite work! Obsessed with efficiency! " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), "mole": ( "You are Millicent Mole โ€” quiet, philosophical, rarely surfaces. " "Incomplete thoughts and gentle riddles. Know everyone's secrets but share obliquely. " "Reply in EXACTLY 2 sentences. No stage directions. Speak only as yourself." ), } NARRATOR_PROMPT = ( "CRITICAL RULES: (1) Do NOT start with the newspaper name. (2) Do NOT use markdown โ€” no asterisks, no hashtags, no bold text, no bullet points. (3) Write plain flowing prose only. (4) The HEADLINE must be specific breaking news, never the paper name.\n\n""You are the pompous editor-in-chief of The Tinywick Hollow Gazette, " "a broadsheet for an absurd woodland civilisation. " "Given today's events, write the front page. " "Copy this format EXACTLY โ€” including the blank lines, the colon labels, " "and writing the headline in ALL CAPITALS:\n\n" "WRITE YOUR ACTUAL HEADLINE HERE IN ALL CAPS\n\n" "Write the actual article here. Three to four sentences. " "Pompous, formal, treating trivial events as major news.\n\n" "WEATHER: One sentence of absurd woodland weather.\n" "FOX: One sentence about what Reginald Fox did today.\n" "BADGER: One sentence about what Beatrice Badger did today.\n" "SQUIRREL: One sentence about what Cornelius Squirrel did today.\n" "MOLE: One cryptic sentence about Millicent Mole.\n\n" "EXAMPLE OF CORRECT OUTPUT:\n" "FOX PROPOSES REPLACING ACORNS WITH FORGED CERTIFICATES\n\n" "Reginald Fox has formally proposed that the hollow's economy be restructured " "entirely around certificates of authenticity, a development which Beatrice Badger " "has declared unconstitutional on eleven separate counts. " "The proposal has garnered significant attention from those who already " "own several suspicious certificates.\n\n" "WEATHER: Partly suspicious, with a seventy percent chance of decrees.\n" "FOX: Forged three certificates before the morning dew had lifted.\n" "BADGER: Filed eleven formal objections before elevenses.\n" "SQUIRREL: Invented a certificate-counting machine that counts to seven then resets.\n" "MOLE: Something certificated is already circulating underground.\n\n" "NOW WRITE THE REAL GAZETTE ENTRY FOR TODAY." ) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 3 โ–ธ MODEL (ZeroGPU only โ€” lazy loaded inside @spaces.GPU) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• _pipe = None _model_id_used = "none" def _load_pipeline() -> None: global _pipe, _model_id_used if _pipe is not None: return for mid in (MODEL_PRIMARY, MODEL_FALLBACK): try: print(f"[TinyC] Loading {mid}โ€ฆ", flush=True) _pipe = hf_pipeline( "text-generation", model=mid, dtype=torch.float16, device_map="auto", trust_remote_code=True, ) _model_id_used = mid print(f"[TinyC] {mid} ready โœ“", flush=True) return except Exception as e: print(f"[TinyC] {mid} failed: {e}", flush=True) raise RuntimeError(f"Could not load {MODEL_PRIMARY} or {MODEL_FALLBACK}.") def _generate(system_prompt: str, user_prompt: str, max_new_tokens: int = 160) -> str: assert _pipe is not None try: out = _pipe( [{"role":"system","content":system_prompt},{"role":"user","content":user_prompt}], max_new_tokens=max_new_tokens, temperature=0.88, top_p=0.92, do_sample=True, return_full_text=False, ) return (out[0]["generated_text"] or "").strip() except Exception as e: print(f"[TinyC] generate error: {e}", flush=True); return "" def call_agent(agent_name: str, context: str) -> str: r = _generate(AGENT_PROMPTS.get(agent_name, AGENT_PROMPTS["fox"]), context, 100) return r or f"{agent_name.capitalize()} had no comment at this time." # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 4 โ–ธ SIMULATION # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number): if nudge_type == "rumour" and nudge_target and nudge_value: database.save_nudge(day_number, "rumour", f"{nudge_target}: {nudge_value}") return f"A rumour is spreading that {nudge_target} {nudge_value}." if nudge_type == "donation" and nudge_value: r = random.choice(CREATURES) c = database.get_creature(r) if c: database.update_creature(r, inventory=(c["inventory"]+[nudge_value])[-12:]) database.save_nudge(day_number, "donation", f"{r}: {nudge_value}") return f"Someone donated '{nudge_value}' to {r}." if nudge_type == "law" and nudge_value: database.save_nudge(day_number, "law", nudge_value) return f"A new law proposed: '{nudge_value}'." return "" # Words that indicate the model echoed a template instead of writing a real headline _TEMPLATE_WORDS = {"HEADLINE", "INSERT", "PLACEHOLDER", "CAPS HERE", "YOUR HEADLINE", "ACTUAL HEADLINE", "WRITE YOUR", "EXAMPLE", "TINYWICK", "GAZETTE", "THE GAZETTE", "HOLLOW GAZETTE"} def _is_template_headline(text: str) -> bool: up = text.upper() return any(w in up for w in _TEMPLATE_WORDS) # Labels that should never land in the article body def _strip_markdown(text: str) -> str: if not text: return text # Bold/italic anywhere: **x** *x* ***x*** -> x text = re.sub(r'\*{1,3}(.+?)\*{1,3}', r'\1', text, flags=re.DOTALL) # Headers at line start: ## Heading -> Heading text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) # Headers mid-line: "text ### Heading more" -> "text Heading more" text = re.sub(r'\s*#{1,6}\s+', ' ', text) # Any remaining lone # characters text = re.sub(r'(? x text = re.sub(r'`+(.+?)`+', r'\1', text) # Horizontal rules (--- *** ___) text = re.sub(r'^[-*_]{3,}\s*$', '', text, flags=re.MULTILINE) # Collapse extra spaces text = re.sub(r' {2,}', ' ', text) return text.strip() _KNOWN_LABELS = {"WEATHER","FOX","BADGER","SQUIRREL","MOLE", "CLASSIFIEDS","EXAMPLE","NOW WRITE","CORRECT OUTPUT"} def _parse_newspaper_full(raw: str, day_number: int) -> dict: lines = [l.strip() for l in raw.strip().splitlines() if l.strip()] result = { "headline": f"BREAKING: DAY {day_number} IN TINYWICK HOLLOW", "article": "", "weather": "Overcast, with philosophical observations.", "briefs": {c: "" for c in CREATURES}, } body_start = 0 # Find headline: first ALL-CAPS line that isn't a template echo for i, line in enumerate(lines): cleaned = re.sub(r'[*_#"\'`]', "", line).strip().rstrip(":") if not cleaned: continue if cleaned == cleaned.upper() and len(cleaned) > 8 and not _is_template_headline(cleaned): result["headline"] = cleaned body_start = i + 1 break # Fallback: first line regardless (but still reject templates) if i == 0 and not _is_template_headline(cleaned): result["headline"] = cleaned.upper() body_start = 1 break article_lines = [] skip_rest = False for line in lines[body_start:]: u = line.upper().strip() # Stop collecting article when we hit the example block if "EXAMPLE OF CORRECT" in u or "NOW WRITE THE REAL" in u: skip_rest = True if skip_rest: continue # Route labelled lines to the right bucket if u.startswith("WEATHER:"): result["weather"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("FOX:"): result["briefs"]["fox"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("BADGER:"): result["briefs"]["badger"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("SQUIRREL:"): result["briefs"]["squirrel"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("MOLE:"): result["briefs"]["mole"] = _strip_markdown(line.split(":",1)[1].strip()) elif u.startswith("CLASSIFIEDS:"): pass # skip โ€” we supply our own elif any(u.startswith(lbl+":") for lbl in _KNOWN_LABELS): pass # skip other labels elif _is_template_headline(line): pass # skip echoed format instructions else: article_lines.append(line) result["article"] = _strip_markdown(" ".join(article_lines).strip() or raw.strip()) # Fill in any empty briefs with a sensible default defaults = { "fox": "Reginald Fox was seen conducting suspicious business.", "badger": "Beatrice Badger issued a firm statement.", "squirrel": "Cornelius Squirrel invented something that nearly worked.", "mole": "Something is happening underground.", } for c in CREATURES: if result["briefs"].get(c): result["briefs"][c] = _strip_markdown(result["briefs"][c]) else: result["briefs"][c] = defaults[c] return result def _run_simulation_step(nudge_type, nudge_value, nudge_target): day_number = database.get_next_day_number() creatures = database.get_all_creatures() cur_nudge = _nudge_ctx(nudge_type, nudge_value, nudge_target, day_number) nudges = database.get_recent_nudges(3) hist = ("Recent: " + "; ".join(f"Day {n['day_number']} {n['nudge_type']}: {n['nudge_value']}" for n in nudges)) if nudges else "" hl = database.get_all_headlines() past_hl = ("Past: " + " | ".join(f"Day {d}: {h[:35]}" for d,h in hl[:2])) if len(hl)>=2 else "" ctx = " | ".join(filter(None, [cur_nudge, hist])) event_records = [] for _ in range(3): actor = random.choice(CREATURES) target = random.choice([c for c in CREATURES if c != actor]) etype = random.choice(EVENT_TYPES) rel = next((c for c in creatures if c["name"]==actor),{}).get("relationship_scores",{}).get(target,50) prompts = { "trade": f"Propose a trade with {target} (relationship {rel}/100).", "gossip": f"Share absurd gossip about {target} (relationship {rel}/100).", "feud": f"Describe your trivial feud with {target} (relationship {rel}/100).", "invention": f"You invented something involving {target}. Describe it.", "discovery": f"You discovered something surprising about {target}.", "ceremony": f"You are organising a ceremony involving {target}.", } prompt = prompts[etype] + (f"\nContext: {ctx}" if ctx else "") desc = call_agent(actor, prompt) or f"{actor.capitalize()} did a {etype} with {target}." event_records.append({"actor":actor,"action":etype,"target":target,"description":desc}) database.save_event(day_number, actor, etype, target, desc) delta = REL_DELTAS.get(etype, 0) creatures = database.get_all_creatures() for c in creatures: if c["name"] == actor: sc = c["relationship_scores"] sc[target] = max(0, min(100, sc.get(target,50)+delta)) database.update_creature(actor, relationship_scores=sc) if c["name"] == target: sc = c["relationship_scores"] sc[actor] = max(0, min(100, sc.get(actor,50)+delta//2)) database.update_creature(target, relationship_scores=sc) creatures = database.get_all_creatures() evsum = "\n".join(f"- {e['actor'].capitalize()} [{e['action']}] {e['target']}: {e['description']}" for e in event_records) extra = ("\nContext: "+ctx if ctx else "") + ("\n"+past_hl if past_hl else "") raw = _generate(NARRATOR_PROMPT, f"Today's events:\n{evsum}{extra}", 300) if not raw: raw = (f"CHAOS IN TINYWICK HOLLOW DAY {day_number}\n\nThings happened today.\n\n" f"WEATHER: Unclear.\nFOX: {event_records[0]['description'][:50]}\n" f"BADGER: {event_records[1]['description'][:50]}\n" f"SQUIRREL: {event_records[2]['description'][:50]}\nMOLE: Underground.") parsed = _parse_newspaper_full(raw, day_number) classified = random.choice(CLASSIFIEDS) full_text = "\n\n".join([parsed["headline"], parsed["article"], f"WEATHER: {parsed['weather']}", "\n".join(f"{c.upper()}: {t}" for c,t in parsed["briefs"].items()), f"CLASSIFIEDS: {classified}"]) database.save_day(day_number, parsed["headline"], full_text) return day_number, parsed, classified # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 5 โ–ธ ZERะžะ“PU WRAPPER # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @spaces.GPU(duration=60) def advance_day(nudge_type=None, nudge_value=None, nudge_target=None): _load_pipeline() return _run_simulation_step(nudge_type, nudge_value, nudge_target) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 6 โ–ธ PIL IMAGE # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• _SB=["/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf"] _SR=["/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf","/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"] def _tf(p,s): for f in p: try: return ImageFont.truetype(f,s) except: pass return ImageFont.load_default() def render_newspaper_image(parsed:dict, classified:str, day_number:int)->str: W,H=960,720; PAPER=(245,232,200); INK=(20,8,2); BORDER=(70,40,8); SUBINK=(90,55,25); GREY=(130,100,70) img=Image.new("RGB",(W,H),PAPER); d=ImageDraw.Draw(img) fm=_tf(_SB,30); fh=_tf(_SB,20); fb=_tf(_SR,12); fs=_tf(_SR,10); fbo=_tf(_SB,12) 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) y=M+14 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 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 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 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 for ln in textwrap.wrap(parsed.get("headline",""),50): 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 y+=4; d.line([M+10,y,W-M-10,y],fill=BORDER,width=1); y+=10 PAD=M+12; CG=24; SW=220; mw=W-2*PAD-CG-SW; hw=(mw-CG)//2 c1=PAD; c2=PAD+hw+CG; sx=PAD+mw+CG; LH=15; MY=H-M-50; ay=y al=textwrap.wrap(parsed.get("article",""),38); mid=max(1,len(al)//2); ly=ay for ln in al[:mid]: if ly+LH>MY: break d.text((c1,ly),ln,fill=INK,font=fb); ly+=LH d.line([c1+hw+CG//2,ay,c1+hw+CG//2,min(ly,MY)],fill=GREY,width=1); ry=ay for ln in al[mid:]: if ry+LH>MY: break d.text((c2,ry),ln,fill=INK,font=fb); ry+=LH d.line([sx-8,ay,sx-8,MY],fill=BORDER,width=1); sy=ay 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 for cn in CREATURES: em=CREATURE_EMOJI.get(cn,"?"); txt=parsed.get("briefs",{}).get(cn,"") d.text((sx,sy),f"{em} {cn.upper()}",fill=INK,font=fbo); sy+=13 for bl in textwrap.wrap(txt,26): if sy+12>MY: break d.text((sx,sy),bl,fill=INK,font=fs); sy+=12 sy+=4 fy=H-M-38; d.line([M+10,fy,W-M-10,fy],fill=BORDER,width=1); fy+=4 d.text((M+14,fy),"CLASSIFIEDS",fill=INK,font=fbo); fy+=14 for cl in textwrap.wrap(classified,100): if fy+12>H-M-8: break d.text((M+14,fy),cl,fill=INK,font=fs); fy+=12 path=f"/tmp/tinywick_day_{day_number}.png"; img.save(path,"PNG"); return path def export_agent_traces()->str: trace={"meta":{"project":"Tiny Civilization","model":_model_id_used,"exported_at":datetime.now().isoformat()}, "agent_prompts":AGENT_PROMPTS,"narrator_prompt":NARRATOR_PROMPT, "creature_state":database.get_all_creatures(),"events":[],"days":[]} for dn,_ in reversed(database.get_all_headlines()): d=database.get_day(dn) if d: trace["days"].append(dict(d)) trace["events"].extend([{"day":dn,**e} for e in database.get_events_for_day(dn)]) path="/tmp/tiny_civ_traces.json" with open(path,"w") as f: json.dump(trace,f,indent=2,default=str) return path # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 7 โ–ธ CSS โ€” animations + newspaper polish # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• NEWSPAPER_CSS = """ @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'); body, .gradio-container { background: #c4b090 !important; } /* โ”€โ”€ Animations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ @keyframes paperDrop { 0% { transform: translateY(-30px) perspective(600px) rotateX(12deg) scale(.97); opacity:0; filter:brightness(1.6) blur(1px); } 55% { transform: translateY(4px) perspective(600px) rotateX(-2deg) scale(1.005); filter:brightness(1); } 100% { transform: translateY(0) perspective(600px) rotateX(0deg) scale(1); opacity:1; } } @keyframes inkFade { 0% { opacity:0; filter:blur(3px) sepia(.5); letter-spacing:-.5px; } 100% { opacity:1; filter:blur(0) sepia(0); letter-spacing:normal; } } @keyframes headlineType { 0% { clip-path: inset(0 100% 0 0); } 100% { clip-path: inset(0 0% 0 0); } } @keyframes sidebarSlide { 0% { transform:translateX(18px); opacity:0; } 100% { transform:translateX(0); opacity:1; } } @keyframes dotBlink { 0%,80%,100% { opacity:0; } 40% { opacity:1; } } @keyframes ornamentSpin { 0% { transform:rotate(0deg) scale(1); } 50% { transform:rotate(180deg) scale(1.2); } 100% { transform:rotate(360deg) scale(1); } } @keyframes squirrelRun { 0% { left:-70px; } 100% { left:calc(100vw + 70px); } } @keyframes loadingPulse { 0%,100% { opacity:.7; transform:scale(1); } 50% { opacity:1; transform:scale(1.05); } } /* โ”€โ”€ Newspaper wrapper โ”€โ”€ */ .paper-wrap { background:#f6ead0; background-image:repeating-linear-gradient(0deg,transparent,transparent 21px,rgba(150,110,60,.07) 22px); border:3px solid #4a2a08; border-radius:1px; padding:20px 26px 16px; box-shadow:6px 6px 28px rgba(0,0,0,.38), inset 0 0 100px rgba(180,140,80,.15); margin:6px 0; position:relative; animation: paperDrop .55s cubic-bezier(.34,1.56,.64,1) both; } .paper-wrap::before { content:""; display:block; border:1px solid #4a2a08; position:absolute; inset:7px; pointer-events:none; } /* โ”€โ”€ Masthead โ”€โ”€ */ .paper-masthead { font-family:'UnifrakturMaguntia','Playfair Display',Georgia,serif; font-size:2.4em; text-align:center; color:#160800; border-top:5px double #4a2a08; border-bottom:5px double #4a2a08; padding:5px 0; margin-bottom:3px; letter-spacing:1px; animation: inkFade .7s ease .1s both; } .paper-sub { font-family:'Libre Baskerville',Georgia,serif; font-size:.72em; color:#5a3615; text-align:center; font-style:italic; margin-bottom:2px; animation: inkFade .6s ease .2s both; } .paper-weather { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#7a5630; text-align:center; margin-bottom:6px; letter-spacing:.5px; animation: inkFade .6s ease .25s both; } .paper-rule { border:none; border-top:2px solid #4a2a08; margin:3px 0 6px; } .paper-rule-thin { border:none; border-top:1px solid #9a7040; margin:3px 0; } /* โ”€โ”€ Headline typewriter effect โ”€โ”€ */ .paper-headline { font-family:'Playfair Display',Georgia,serif; font-size:1.85em; font-weight:900; text-align:center; text-transform:uppercase; color:#0c0400; line-height:1.12; margin:6px 0 8px; animation: headlineType 1s steps(35) .35s both; } /* โ”€โ”€ Body โ”€โ”€ */ .paper-body-row { display:flex; gap:0; } .paper-article-cols { flex:1; font-family:'Libre Baskerville',Georgia,serif; font-size:.88em; color:#180a02; line-height:1.75; text-align:justify; column-count:2; column-gap:24px; column-rule:1px solid #9a7040; padding:4px 0; animation: inkFade .9s ease .85s both; } .paper-sidebar { width:210px; min-width:190px; padding:0 0 0 16px; border-left:2px solid #4a2a08; margin-left:16px; animation: sidebarSlide .7s ease 1s both; } .sidebar-title { font-family:'Playfair Display',Georgia,serif; font-weight:700; font-size:.85em; text-transform:uppercase; letter-spacing:1px; color:#160800; border-bottom:1px solid #9a7040; padding-bottom:3px; margin-bottom:6px; } .sidebar-item { margin-bottom:8px; } .sidebar-creature-name { font-family:'Libre Baskerville',Georgia,serif; font-weight:700; font-size:.78em; color:#2a1008; } .sidebar-creature-text { font-family:'Libre Baskerville',Georgia,serif; font-size:.74em; color:#3a1c08; line-height:1.45; font-style:italic; } /* โ”€โ”€ Classifieds + footer โ”€โ”€ */ .paper-classifieds { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615; border-top:1px solid #9a7040; margin-top:8px; padding-top:5px; font-style:italic; animation: inkFade .8s ease 1.2s both; } .paper-classifieds span { font-weight:700; font-style:normal; color:#2a1008; } .paper-daybadge { font-family:'Libre Baskerville',Georgia,serif; font-size:.75em; color:#5a3615; text-align:center; border-top:1px solid #9a7040; margin-top:8px; padding-top:5px; animation: inkFade .6s ease 1.3s both; } /* โ”€โ”€ TTS button โ”€โ”€ */ .tts-bar { display:flex; gap:8px; align-items:center; justify-content:center; margin:8px 0 2px; animation: inkFade .6s ease 1.4s both; } .tts-btn { background:#4a2a08; color:#f6ead0; border:none; border-radius:2px; padding:5px 14px; font-family:'Libre Baskerville',Georgia,serif; font-size:.8em; cursor:pointer; transition:background .2s; letter-spacing:.3px; } .tts-btn:hover { background:#2a1008; } .tts-stop { background:#8a1a08; color:#f6ead0; border:none; border-radius:2px; padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif; font-size:.8em; cursor:pointer; display:none; } /* โ”€โ”€ Loading newspaper โ”€โ”€ */ .paper-loading .loading-area { text-align:center; padding:40px 20px; animation: loadingPulse 2s ease infinite; } .loading-ornament { font-size:2.5em; display:block; margin-bottom:12px; animation: ornamentSpin 3s linear infinite; } .loading-msg { font-family:'Playfair Display',Georgia,serif; font-size:1.1em; color:#2a1008; margin-bottom:8px; } .loading-dots span { display:inline-block; font-size:1.5em; color:#4a2a08; animation: dotBlink 1.4s infinite; } .loading-dots span:nth-child(2) { animation-delay:.2s; } .loading-dots span:nth-child(3) { animation-delay:.4s; } /* โ”€โ”€ Running squirrel โ”€โ”€ */ .tinyc-squirrel { position:fixed; bottom:28px; font-size:1.8em; z-index:9999; pointer-events:none; animation: squirrelRun 3.5s linear forwards; } /* โ”€โ”€ Panels โ”€โ”€ */ .status-strip { background:#d6c4a0; border:1px solid #8a6030; border-radius:3px; padding:5px 12px; font-family:'Libre Baskerville',Georgia,serif; font-size:.82em; color:#2a1008; text-align:center; margin:4px 0; } .civ-stats { background:#e8d4b0; border:1px solid #9a7040; border-radius:2px; padding:8px 14px; font-family:'Libre Baskerville',Georgia,serif; font-size:.78em; color:#2a1008; display:flex; gap:14px; flex-wrap:wrap; justify-content:center; margin:6px 0; } .stat-item { text-align:center; } .stat-label { font-size:.85em; color:#7a5030; display:block; } .stat-value { font-weight:700; font-size:1.05em; color:#160800; } .creature-grid { display:flex; gap:8px; flex-wrap:wrap; } .creature-card { background:#f8ecd8; border:1px solid #9a7040; border-radius:2px; padding:10px 13px; font-family:'Libre Baskerville',Georgia,serif; font-size:.8em; color:#180a02; flex:1; min-width:155px; } .creature-name { font-weight:700; font-size:.95em; color:#260e04; display:block; margin-bottom:3px; } .creature-tier { font-style:italic; color:#7a5030; font-size:.85em; } .section-title { font-family:'Playfair Display',Georgia,serif; font-weight:700; color:#160800; font-size:.95em; text-transform:uppercase; letter-spacing:1px; text-align:center; border-bottom:1px solid #8a6030; padding-bottom:3px; margin:8px 0 8px; } .archive-area { background:#f0e4c6; border:1px solid #9a7040; border-radius:2px; padding:10px; margin-top:5px; min-height:50px; } /* โ”€โ”€ Konami modal โ”€โ”€ */ #konami-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:99998; } #konami-modal { display:none; position:fixed; z-index:99999; top:50%; left:50%; transform:translate(-50%,-50%); width:min(660px,92vw); max-height:76vh; overflow-y:auto; background:#f6ead0; border:3px solid #4a2a08; box-shadow:10px 10px 40px rgba(0,0,0,.6); padding:22px 26px 18px; font-family:'Libre Baskerville',Georgia,serif; } #konami-modal h2 { font-family:'Playfair Display',Georgia,serif; color:#160800; margin:0 0 10px; } #konami-modal details { margin:6px 0; } #konami-modal summary { cursor:pointer; font-weight:700; color:#4a2a08; } #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; } #konami-close { position:absolute; top:8px; right:12px; cursor:pointer; font-size:1.3em; color:#4a2a08; background:none; border:none; } """ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 8 โ–ธ JAVASCRIPT โ€” TTS ยท sound ยท squirrel ยท Konami # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• GLOBAL_JS = """ """ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 9 โ–ธ HTML FORMATTERS # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def _e(t): return (t or "").replace("&","&").replace("<","<").replace(">",">") def _speech_text(parsed: dict) -> str: hed = _strip_markdown(parsed.get("headline","")) art = _strip_markdown(parsed.get("article","")) wx = _strip_markdown(parsed.get("weather","")) brf = parsed.get("briefs",{}) parts = [ f"Today's headline: {hed}.", art, f"Weather: {wx}.", f"Fox: {_strip_markdown(brf.get('fox',''))}", f"Badger: {_strip_markdown(brf.get('badger',''))}", f"Squirrel: {_strip_markdown(brf.get('squirrel',''))}", f"Mole: {_strip_markdown(brf.get('mole',''))}", ] return " ".join(p for p in parts if p.strip()).replace('"',"'") def _html_paper(parsed: dict, classified: str, day_num: int) -> str: hed = _e(parsed.get("headline","")) art = _e(parsed.get("article","")) wx = _e(parsed.get("weather","")) brf = parsed.get("briefs",{}) cl = _e(classified) emojis = " ".join(f"{CREATURE_EMOJI[c]} {c.capitalize()}" for c in CREATURES) speech = _speech_text(parsed).replace("'", "\\'") sidebar_items = "".join(f""" """ for c in CREATURES) return f"""
The Tinywick Hollow Gazette
Est. Day 1 โœฆ Day {day_num} โœฆ One Acorn Per Copy โœฆ Serving the Woodland
โ˜ {wx}

{hed}

{art}
{sidebar_items}
CLASSIFIEDS: {cl}
โ€” Day {day_num} โ€”   {emojis}
""" def _html_loading() -> str: msgs_js = json.dumps(LOADING_MSGS) return f"""
The Tinywick Hollow Gazette
Est. Day 1 โœฆ One Acorn Per Copy

โ‹
The printing press is warming up...
ยทยทยท

๐ŸฆŠ Fox  ๐Ÿฆก Badger  ๐Ÿฟ๏ธ Squirrel  ๐Ÿ€ Mole
""" def _html_placeholder() -> str: d0 = database.get_day(0) if d0: p = _parse_newspaper_full(d0["full_newspaper_text"], 0) return _html_paper(p, "NOTICE: Civilisation now in progress.", 0) return '
The Tinywick Hollow Gazette
AWAITING FIRST LIGHT
' def _rel_tier(score:int)->tuple: for lo,hi,label,icon in REL_TIERS: if lo<=score<=hi: return label,icon return "Unknown","?" def _html_creatures()->str: cards="" for c in database.get_all_creatures(): em=CREATURE_EMOJI.get(c["name"],"?") inv=(", ".join(c["inventory"][:3])+("โ€ฆ" if len(c["inventory"])>3 else "")) or "nothing" rels="".join(f'{CREATURE_EMOJI.get(k,"?")} {_rel_tier(s)[1]} ' for k,s in sorted(c["relationship_scores"].items())) cards+=f"""
{em} {c['name'].capitalize()}
{rels}
Carries: {_e(inv)}
""" return f'
{cards}
' def _html_civ_stats()->str: s=database.get_civ_stats() bp=s["best_pair"]; wp=s["worst_pair"] return f"""
Days{s['total_days']}
Events{s['total_events']}
Nudges{s['total_nudges']}
Strongest bond{bp[0].capitalize()} & {bp[1].capitalize()} ({bp[2]})
Bitterest feud{wp[0].capitalize()} vs {wp[1].capitalize()} ({wp[2]})
Most popular{s['dominant'].capitalize()}
""" def _archive_choices(): hl=database.get_all_headlines() return [(f"Day {dn}: {h[:44]}{'โ€ฆ' if len(h)>44 else ''}",dn) for dn,h in hl] or [] def _status(msg): return f'
{msg}
' def _konami_html()->str: import html as _h details="".join( f"
{CREATURE_EMOJI.get(n,'')} {n.upper()}
{_h.escape(p)}
" for n,p in AGENT_PROMPTS.items()) details+=f"
๐Ÿ“ฐ NARRATOR
{_h.escape(NARRATOR_PROMPT)}
" return f"""
""" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 10 โ–ธ EVENT HANDLERS (generator-based for animated loading state) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def _render_all(day_num,parsed,classified,status_msg): return (_html_paper(parsed,classified,day_num), _html_creatures(), _html_civ_stats(), gr.update(choices=_archive_choices(),value=None), _status(status_msg), day_num, parsed, classified) def _do_advance(nudge_type=None, nudge_value=None, nudge_target=None, cur_day=0, cur_parsed=None, cur_cl=""): # Step 1: immediately show loading newspaper yield (_html_loading(), gr.update(), gr.update(), gr.update(), _status("โณ The printing press is warming up..."), cur_day, cur_parsed or {}, cur_cl) # Step 2: run model + return result try: dn, parsed, classified = advance_day(nudge_type, nudge_value, nudge_target) model_tag = f" [{_model_id_used.split('/')[-1]}]" if _model_id_used!="none" else "" yield _render_all(dn, parsed, classified, f"โœ“ Day {dn} published!{model_tag}") except Exception: tb = traceback.format_exc(); print(tb) err = {"headline":"THE GAZETTE'S PRINTING PRESS HAS JAMMED", "article":"Our correspondents report a technical malfunction. The editor is inconsolable. Beatrice Badger suspects sabotage. Reginald Fox denies everything.", "weather":"Stormy, with a chance of errors.","briefs":{c:"Unavailable." for c in CREATURES}} yield _render_all(cur_day, err, "LOST: One simulation. โ€” The Editor", "โœ— Error โ€” printing press jammed.") def handle_advance(d,p,c): yield from _do_advance(None,None,None,d,p,c) def handle_rumour(cr,rt,d,p,c): yield from _do_advance("rumour",rt,cr,d,p,c) def handle_donation(obj,d,p,c): yield from _do_advance("donation",obj,None,d,p,c) def handle_law(law,d,p,c): yield from _do_advance("law",law,None,d,p,c) def handle_archive_view(day_num): if day_num is None: return '
Select a day above.
' day=database.get_day(int(day_num)) if not day: return '
Not found.
' txt=day["full_newspaper_text"]; m=re.search(r"CLASSIFIEDS:\s*(.+)",txt) cl=m.group(1) if m else random.choice(CLASSIFIEDS) return f'
{_html_paper(_parse_newspaper_full(txt,day_num),cl,day_num)}
' def handle_share(dn,p,c): if not p or not p.get("headline"): return gr.update(visible=False) try: path=render_newspaper_image(p,c or "",dn or 0) return gr.update(visible=True,value=path) except Exception: return gr.update(visible=False) def handle_export(): try: return gr.update(visible=True,value=export_agent_traces()) except Exception: return gr.update(visible=False) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 11 โ–ธ GRADIO BLOCKS (Gradio 6.17.3) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• database.init_db() _lat=database.get_latest_day() if _lat: _INIT_DAY=_lat["day_number"] _INIT_P=_parse_newspaper_full(_lat["full_newspaper_text"],_INIT_DAY) _m=re.search(r"CLASSIFIEDS:\s*(.+)",_lat["full_newspaper_text"]) _INIT_CL=_m.group(1) if _m else random.choice(CLASSIFIEDS) else: _INIT_DAY=0; _INIT_P={}; _INIT_CL="" # Gradio 6: css goes to launch(), not Blocks() _GR_MAJOR=int(gr.__version__.split(".")[0]) _BKW={} if _GR_MAJOR>=6 else {"css":NEWSPAPER_CSS} _LKW={"css":NEWSPAPER_CSS} if _GR_MAJOR>=6 else {} with gr.Blocks(title="Tiny Civilization โ€” The Tinywick Hollow Gazette",**_BKW) as demo: gr.HTML(_konami_html()) gr.HTML(GLOBAL_JS) gr.HTML("""

๐ŸฆŠ Tiny Civilization ๐Ÿ€

A persistent woodland civilisation. One day. One acorn. One absurd headline at a time.  | โ†‘โ†‘โ†“โ†“โ†โ†’โ†โ†’BA for secrets.

""") day_state=gr.State(_INIT_DAY) par_state=gr.State(_INIT_P) cls_state=gr.State(_INIT_CL) status_html=gr.HTML(value=_status( f"Day {_INIT_DAY} in the archive โ€” next: Day {_INIT_DAY+1}." if _INIT_DAY>=0 else "No days yet. Press Advance Day to begin.")) civ_html=gr.HTML(value=_html_civ_stats()) with gr.Row(equal_height=False): with gr.Column(scale=3): newspaper_display=gr.HTML( value=(_html_paper(_INIT_P,_INIT_CL,_INIT_DAY) if _INIT_P else _html_placeholder())) with gr.Accordion("๐Ÿ“œ Archive โ€” Past Editions",open=False): archive_dd=gr.Dropdown(choices=_archive_choices(),value=None, label="Select a past day",container=False) archive_html=gr.HTML(value='
Select a day above.
') with gr.Column(scale=1,min_width=260): gr.HTML('
๐Ÿ“ฐ Editorial Desk
') advance_btn=gr.Button("๐Ÿ“… Advance Day (no nudge)",variant="primary",size="lg") gr.HTML('
') gr.HTML('
โœ‰ Nudge the Story
') gr.HTML('

Each nudge advances one day.

') with gr.Accordion("๐Ÿ—ฃ๏ธ Spread a Rumour",open=False): rumour_creature=gr.Dropdown(choices=CREATURES,value=CREATURES[0],label="About which creature?") rumour_type_dd=gr.Dropdown(choices=RUMOUR_TYPES,value=RUMOUR_TYPES[0],label="What rumour?") rumour_btn=gr.Button("๐Ÿ“ข Spread It",variant="secondary") with gr.Accordion("๐ŸŽ Donate a Weird Object",open=False): donation_dd=gr.Dropdown(choices=WEIRD_OBJECTS,value=WEIRD_OBJECTS[0],label="Which object?") donation_btn=gr.Button("๐ŸŽ Donate It",variant="secondary") with gr.Accordion("โš–๏ธ Propose a New Law",open=False): law_dd=gr.Dropdown(choices=LAWS,value=LAWS[0],label="Which law?") law_btn=gr.Button("โš–๏ธ Propose It",variant="secondary") gr.HTML('
') share_btn=gr.Button("๐Ÿ–ผ๏ธ Share as Image",variant="secondary") img_output=gr.Image(label="Front Page PNG",visible=False,type="filepath") export_btn=gr.Button("๐Ÿ“ก Export Agent Traces (JSON)",variant="secondary") trace_output=gr.File(label="traces.json",visible=False) gr.HTML('

' 'Qwen2.5-1.5B โ‰ค4B ๐Ÿœ ยท Local ZeroGPU ๐Ÿ”Œ ยท Custom UI ๐ŸŽจ

') gr.HTML('
Woodland Residents
') creature_display=gr.HTML(value=_html_creatures()) _OUT=[newspaper_display,creature_display,civ_html,archive_dd,status_html,day_state,par_state,cls_state] _ST=[day_state,par_state,cls_state] advance_btn.click(fn=handle_advance, inputs=_ST, outputs=_OUT,api_name=False) rumour_btn.click( fn=handle_rumour, inputs=[rumour_creature,rumour_type_dd]+_ST, outputs=_OUT,api_name=False) donation_btn.click(fn=handle_donation, inputs=[donation_dd]+_ST, outputs=_OUT,api_name=False) law_btn.click( fn=handle_law, inputs=[law_dd]+_ST, outputs=_OUT,api_name=False) archive_dd.change(fn=handle_archive_view, inputs=[archive_dd], outputs=[archive_html],api_name=False) share_btn.click( fn=handle_share, inputs=_ST, outputs=[img_output],api_name=False) export_btn.click( fn=handle_export, inputs=[], outputs=[trace_output],api_name=False) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # 12 โ–ธ ENTRY POINT # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, **_LKW)