"""Math Adventure -- a talking, tap-to-play math game for an advanced 5-6 year old. Built with Gradio (by Hugging Face). Problems are read aloud by a Hugging Face text-to-speech model (see tts.py). Difficulty adapts as the player gets answers right (see game_logic.py). Run locally: py app.py -> http://localhost:7860 Deploy: upload this folder to a Hugging Face Space (SDK: Gradio). """ import gradio as gr import game_logic as gl import tts NUM_CHOICES = 4 CSS = """ #title {text-align:center; font-size:2.4rem; margin:0.2em 0;} #scoreboard {text-align:center; font-size:1.4rem; font-weight:700;} #problem { text-align:center; font-size:5rem; line-height:1.3; min-height:1.6em; padding:0.3em 0.2em; word-break:break-word; } #feedback {text-align:center; font-size:1.8rem; min-height:1.4em; font-weight:700;} .answer-btn button { font-size:2.6rem !important; min-height:110px !important; border-radius:22px !important; } #replay button {font-size:1.3rem !important;} .gradio-container {max-width:760px !important; margin:auto !important;} """ def _scoreboard(state): stars = "⭐" * min(state["score"], 20) return ( f"Level {state['level']} 🌟 • Score: {state['score']} • " f"Streak: {state['streak']} 🔥\n\n{stars}" ) def _button_updates(problem): """Return a gr.update for each answer button, given the current problem.""" choices = problem["choices"] updates = [] for i in range(NUM_CHOICES): if i < len(choices): updates.append(gr.update(value=choices[i], visible=True)) else: updates.append(gr.update(visible=False)) return updates def _render(state, problem, feedback, spoken=None): """Bundle every UI output for one render pass.""" audio = tts.speak(spoken if spoken is not None else problem["spoken"]) return ( state, problem, gr.update(value=problem["display"]), # problem display *_button_updates(problem), # the answer buttons gr.update(value=_scoreboard(state)), # scoreboard gr.update(value=feedback), # feedback line audio, # autoplayed audio ) def start_game(): state = gl.new_state() problem = gl.generate_problem(state["level"]) return _render(state, problem, "Tap the right answer! 👇") def answer(idx, state, problem): # Guard against stale clicks on a hidden button. if idx >= len(problem["choices"]): return _render(state, problem, "") chosen = problem["choices"][idx] correct = chosen == problem["answer"] state = gl.update_state(state, correct) if correct: feedback = "✅ Great job! 🎉" spoken_prefix = "Great job!" else: feedback = f"❌ It was {problem['answer']}. You can do it — next one!" spoken_prefix = "Good try!" next_problem = gl.generate_problem(state["level"]) spoken = f"{spoken_prefix} {next_problem['spoken']}" return _render(state, next_problem, feedback, spoken=spoken) def replay(problem): return tts.speak(problem["spoken"]) with gr.Blocks(title="Math Adventure") as demo: gr.Markdown("# 🧮 Math Adventure", elem_id="title") game_state = gr.State() problem_state = gr.State() scoreboard = gr.Markdown(elem_id="scoreboard") problem_display = gr.Markdown(elem_id="problem") feedback = gr.Markdown(elem_id="feedback") with gr.Row(): btns = [gr.Button("", elem_classes="answer-btn") for _ in range(2)] with gr.Row(): btns += [gr.Button("", elem_classes="answer-btn") for _ in range(2)] with gr.Row(): replay_btn = gr.Button("🔊 Hear it again", elem_id="replay") # autoplay reads each new problem aloud. Kept visible because browsers do # not play a hidden audio element -- the player also gives a manual control. audio = gr.Audio(autoplay=True, visible=True, label="🔉 Listen", type="filepath", interactive=False) # Outputs updated on every render pass (order must match _render()). render_outputs = [game_state, problem_state, problem_display, *btns, scoreboard, feedback, audio] for i, b in enumerate(btns): b.click( fn=lambda gs, ps, idx=i: answer(idx, gs, ps), inputs=[game_state, problem_state], outputs=render_outputs, ) replay_btn.click(fn=replay, inputs=[problem_state], outputs=[audio]) demo.load(fn=start_game, inputs=None, outputs=render_outputs) if __name__ == "__main__": tts.warm_up() # pre-load the TTS model so the first round speaks promptly demo.launch(css=CSS, theme=gr.themes.Soft())