Spaces:
Sleeping
Sleeping
| """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()) | |