import base64 import pathlib import gradio as gr from judge import judge from levels import HOME_ID, LEVEL_ORDER, LEVELS STATIC = pathlib.Path(__file__).parent / "static" # The pen-scratch SFX, inlined as a data URI so it needs no static-file route # (which is brittle inside HF Spaces' iframe). The client decodes it once into # a Web Audio buffer; see sfx.scratch in game.js. _SCRATCH = base64.b64encode((STATIC / "pencil-scratch.mp3").read_bytes()).decode() # The background-music track is far too big to inline like the scratch above # (a multi-MB loop would bloat every page load), so it streams from the static # dir via the file route instead. game.js resolves the URL against the Gradio # root so it still holds up inside the HF Spaces iframe. gr.set_static_paths(paths=[STATIC]) # A static file streamed via the Gradio file route. game.js resolves the URL # against the Gradio root so it still holds up inside the HF Spaces iframe. def _file_route(name: str) -> str: return "gradio_api/file=" + str(STATIC / name) # Each board may name its own background loop (Level.music, a static/ basename); # resolve those to file-route URLs here so the Level data stays path-agnostic. # Boards without one play the default loop (Cipher2). game.js crossfades between # tracks on transition; per-board music_gain keeps them at a matched loudness. def _level_client(lid: str) -> dict: cv = LEVELS[lid].client_value() cv["music"] = _file_route(cv["music"]) if cv["music"] else "" return cv # Everything the board needs to render and switch levels client-side: the play # order, the boot board, each level's client-facing slice, the pen-scratch # sample (data URI), and the default looping background-music URL (file route). GAME = { "levels": [_level_client(lid) for lid in LEVEL_ORDER], "order": LEVEL_ORDER, "home": HOME_ID, "scratchAudio": "data:audio/mpeg;base64," + _SCRATCH, "music": _file_route("Cipher2.mp3"), } HEAD = """ """ # Hide all Gradio chrome: the game component IS the page. # # This block (unlike static/style.css, which Gradio injects NESTED under the # component id) is global, so the bonus board's glitch mode lives here: its # @keyframes and the page-chrome darkening must reach `html` and `.gradio-container`, # which a component-scoped rule can't. game.js toggles `.sq-glitch-page` on . BLOCKS_CSS = """ footer { display: none !important; } html, body { height: 100%; overflow: hidden; overscroll-behavior: none; } .gradio-container { padding: 0 !important; max-width: 100% !important; background: #faf8f2 !important; } .gradio-container .main, .gradio-container .wrap { background: #faf8f2 !important; } /* Zero Gradio's own wrapper padding so the game sits flush at the top — else a ~26px gap pushes the board down and clips the bottom hint off mobile screens. */ .gradio-container .main, .html-container { padding: 0 !important; } /* Smoothly fade Gradio's paper chrome to dark in glitch mode (matches the eased filter flip on .sq-root) so the centred column's side-bars don't glare beside the inverted board (the board's paper inverts to ~#05070d). */ .gradio-container, .gradio-container .main, .gradio-container .wrap, .gradio-container .contain, html, body { transition: background 0.85s ease-in-out; } html.sq-glitch-page, html.sq-glitch-page body, html.sq-glitch-page .gradio-container, html.sq-glitch-page .gradio-container .main, html.sq-glitch-page .gradio-container .wrap, html.sq-glitch-page .gradio-container .contain { background: #05070d !important; } /* drifting scanlines over the negative, for that decoded-signal hum */ @keyframes sq-scanline { from { background-position: 0 0; } to { background-position: 0 120px; } } /* the victory title's rainbow sweep (the gradient is 200% wide; slide it one full gradient-width for a seamless loop). Lives here, un-scoped, because the component-nested css_template can't host @keyframes. */ @keyframes sq-rainbow { from { background-position: 0% 50%; } to { background-position: 200% 50%; } } """ with gr.Blocks(css=BLOCKS_CSS, title="Semantique") as demo: game = gr.HTML( value=GAME, html_template=(STATIC / "game.html").read_text(), css_template=(STATIC / "style.css").read_text(), js_on_load=(STATIC / "game.js").read_text(), head=HEAD, server_functions=[judge], container=False, padding=False, ) if __name__ == "__main__": demo.launch()