MathMai / app.py
Heterogeneity2025's picture
Upload 2 files
5789671 verified
Raw
History Blame Contribute Delete
4.78 kB
"""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())