""" 🦁 NumZoo β€” Math practice with cute AI-generated animal rewards! """ import random import gradio as gr from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message from image_generator import generate_reward_image # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- REWARD_EVERY = 3 ANIMAL_EMOJIS = ["🐢", "🐱", "🐰", "🦊", "🐼", "🐨", "🦁", "🐯", "🐸", "🐧", "πŸ¦‹", "πŸ¦„"] PLACE_EMOJIS = ["🌊", "πŸ”οΈ", "🌸", "🌈", "πŸŒ™", "⭐", "🌴", "🏑", "🌺", "πŸ„"] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _status_text(state: dict) -> str: if not state: return "" return f"πŸ‘€ {state['name']} | ⭐ {state['score']} | πŸ“š {LEVEL_NAMES.get(state['level'], '')}" def _safe_question(state: dict) -> str: return f"## {state.get('question', '')} = ?" # --------------------------------------------------------------------------- # Step 1 β€” Name entry # --------------------------------------------------------------------------- def enter_name(player_name: str, state: dict): try: name = player_name.strip() or "Player" state = {"name": name} return ( state, gr.update(visible=False), # welcome gr.update(visible=True), # emoji picker gr.update(visible=False), # game random.sample(ANIMAL_EMOJIS, 1), random.sample(PLACE_EMOJIS, 1), ) except Exception as e: print(f"enter_name error: {e}") return state, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), [], [] # --------------------------------------------------------------------------- # Step 2 β€” Emoji selection β†’ start quiz # --------------------------------------------------------------------------- def start_game(animal_sel: list, place_sel: list, state: dict): try: if not animal_sel or not place_sel: return (state, gr.update(visible=True), gr.update(visible=False), "⚠️ Pick at least one animal and one place!", "", "") state.update({ "selected_animals": animal_sel[:3], "selected_places": place_sel[:3], "level": 1, "score": 0, "streak": 0, "correct_since_reward": 0, "level_correct": 0, }) question, answer = generate_question(state["level"]) state["question"] = question state["answer"] = answer return (state, gr.update(visible=False), gr.update(visible=True), "", _status_text(state), _safe_question(state)) except Exception as e: print(f"start_game error: {e}") return (state, gr.update(visible=True), gr.update(visible=False), "⚠️ Something went wrong, try again.", "", "") # --------------------------------------------------------------------------- # Step 3 β€” Answer checking (no reward yet, just feedback) # --------------------------------------------------------------------------- def check_answer(user_input: str, state: dict): try: if not state or not state.get("question"): return (state, _status_text(state), _safe_question(state), "", "", gr.update(visible=False), gr.update(visible=False), None, "") try: user_answer = int(user_input.strip()) except (ValueError, AttributeError): return (state, _status_text(state), _safe_question(state), "", "⚠️ Numbers only!", gr.update(visible=False), gr.update(visible=False), None, "") correct = (user_answer == state["answer"]) if correct: state["score"] += 1 state["streak"] += 1 state["correct_since_reward"] += 1 state["level_correct"] += 1 # Level-up? threshold = LEVEL_THRESHOLDS.get(state["level"], 999) level_msg = "" if state["level_correct"] >= threshold and state["level"] < 4: state["level"] += 1 state["level_correct"] = 0 level_msg = level_up_message(state["level"]) streak_fire = "πŸ”₯" * min(state["streak"], 5) feedback = f"βœ… {level_msg}" if level_msg else f"βœ… Great! {streak_fire}" # Reward due? β†’ show loading panel, trigger generate step if state["correct_since_reward"] >= REWARD_EVERY: state["correct_since_reward"] = 0 state["generate_now"] = True # signal .then() chain to generate question, answer = generate_question(state["level"]) state["question"] = question state["answer"] = answer return (state, _status_text(state), _safe_question(state), "", feedback, gr.update(visible=True), # reward_panel visible gr.update(visible=True), # loader visible None, # no image yet "") # no error else: state["streak"] = 0 feedback = f"❌ Answer: **{state['answer']}**" state["generate_now"] = False question, answer = generate_question(state["level"]) state["question"] = question state["answer"] = answer return (state, _status_text(state), _safe_question(state), "", feedback, gr.update(visible=False), # reward_panel gr.update(visible=False), # loader None, "") except Exception as e: print(f"check_answer error: {e}") state["generate_now"] = False question, answer = generate_question(state.get("level", 1)) state["question"] = question state["answer"] = answer return (state, _status_text(state), _safe_question(state), "", "⚠️ Something went wrong!", gr.update(visible=False), gr.update(visible=False), None, "") # --------------------------------------------------------------------------- # Step 4 β€” Actually generate the image (called by gr.Timer after reward shown) # --------------------------------------------------------------------------- def generate_image(state: dict): """Chained generation β€” only runs if check_answer flagged generate_now.""" if not state.get("generate_now"): return gr.update(), gr.update(), gr.update() # no-op state["generate_now"] = False try: animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)]) places = state.get("selected_places", [random.choice(PLACE_EMOJIS)]) streak = state.get("streak", 0) print(f"[generate_image] calling generate_reward_image | streak={streak} | animals={animals} | places={places}") result, prompt = generate_reward_image(streak, animals, places) print(f"[generate_image] result={result} | prompt={prompt!r}") if result is None: print("[generate_image] ⚠️ result is None β€” generation failed silently inside image_generator") return (gr.update(visible=False), None, "⚠️ Could not generate image β€” try again later!") print("[generate_image] βœ… image generated successfully") return gr.update(visible=False), result, "" except Exception as e: import traceback print(f"[generate_image] ❌ exception: {e}") print(traceback.format_exc()) return gr.update(visible=False), None, f"⚠️ Error: {e}" # --------------------------------------------------------------------------- # Restart # --------------------------------------------------------------------------- def restart(state: dict): return ({}, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(), "") # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- CSS = """ .gradio-container { max-width: 680px !important; margin: 0 auto !important; } .gradio-group > .form { padding: 16px 20px !important; } .gradio-group > .form > .gap > .prose h3, .gradio-group > .form > .gap > p { padding-top: 8px !important; padding-bottom: 4px !important; } #title { text-align: center; font-size: 2.2em; margin-bottom: 0.1em; } #subtitle { text-align: center; color: #888; margin-bottom: 1em; font-style: italic; } #question-box { text-align: center; font-size: 2.6em; font-weight: bold; padding: 0.5em; } #feedback-box { text-align: center; font-size: 1.3em; min-height: 2em; } #status-box { text-align: center; padding: 0.4em; border-radius: 8px; } #reward-img { border-radius: 16px; } #loader { text-align: center; font-size: 1.4em; padding: 2em; } .answer-input input { font-size: 2em !important; text-align: center !important; } .emoji-group .wrap { gap: 6px !important; flex-wrap: wrap !important; } .emoji-group label { font-size: 1.8em !important; cursor: pointer !important; padding: 6px 10px !important; border-radius: 12px !important; border: 2px solid transparent !important; transition: all 0.15s !important; user-select: none !important; } .emoji-group label:has(input:checked) { background: #e9d5ff !important; border-color: #7c3aed !important; } .emoji-group input[type="checkbox"] { display: none !important; } """ LOADER_HTML = """
🎨
Generating your reward…
""" with gr.Blocks(title="🦁 NumZoo") as demo: state = gr.State({}) gr.Markdown("# 🦁 NumZoo", elem_id="title") gr.Markdown("*Do maths. Win cute animals!*", elem_id="subtitle") gr.HTML(""" """) # ── Welcome ──────────────────────────────────────────────────────────── with gr.Group(visible=True) as welcome_panel: gr.Markdown("### Who are you?") player_name = gr.Textbox(placeholder="Your name…", label=" ", show_label=False, max_lines=1) next_btn = gr.Button("Next ➑️", variant="primary", size="lg") # ── Emoji picker ─────────────────────────────────────────────────────── with gr.Group(visible=False) as emoji_panel: gr.Markdown("### Pick your animals 🐾") animal_picker = gr.CheckboxGroup(choices=ANIMAL_EMOJIS, label="", show_label=False, elem_classes=["emoji-group"]) gr.Markdown("### Pick your places 🌍") place_picker = gr.CheckboxGroup(choices=PLACE_EMOJIS, label="", show_label=False, elem_classes=["emoji-group"]) picker_error = gr.Markdown("") go_btn = gr.Button("Let's go! πŸš€", variant="primary", size="lg") # ── Game ─────────────────────────────────────────────────────────────── with gr.Group(visible=False) as game_panel: status_md = gr.Markdown("", elem_id="status-box", container=True) question_md = gr.Markdown("", elem_id="question-box") answer_input = gr.Textbox(placeholder="?", label="Your answer", max_lines=1, elem_classes=["answer-input"]) check_btn = gr.Button("βœ”οΈ Check!", variant="primary") feedback_md = gr.Markdown("", elem_id="feedback-box") with gr.Group(visible=False) as reward_panel: gr.Markdown("### 🎁 Your reward!") loader_html = gr.HTML(LOADER_HTML, visible=False) reward_image = gr.Image(label="", show_label=False, elem_id="reward-img", height=400) reward_error = gr.Markdown("", visible=True) restart_btn = gr.Button("πŸ”„ Restart", variant="secondary", size="sm") # ── Wiring ───────────────────────────────────────────────────────────── next_btn.click(enter_name, [player_name, state], [state, welcome_panel, emoji_panel, game_panel, animal_picker, place_picker]) player_name.submit(enter_name, [player_name, state], [state, welcome_panel, emoji_panel, game_panel, animal_picker, place_picker]) go_btn.click(start_game, [animal_picker, place_picker, state], [state, emoji_panel, game_panel, picker_error, status_md, question_md]) # check_answer β†’ then β†’ generate_image (chained, runs immediately after) check_outputs = [state, status_md, question_md, answer_input, feedback_md, reward_panel, loader_html, reward_image, reward_error] gen_outputs = [loader_html, reward_image, reward_error] check_btn.click(check_answer, [answer_input, state], check_outputs).then( generate_image, inputs=[state], outputs=gen_outputs ) answer_input.submit(check_answer, [answer_input, state], check_outputs).then( generate_image, inputs=[state], outputs=gen_outputs ) restart_btn.click(restart, [state], [state, welcome_panel, emoji_panel, game_panel, player_name, picker_error]) if __name__ == "__main__": demo.launch( css=CSS, theme=gr.themes.Soft(primary_hue="purple", secondary_hue="pink"), )