""" π¦ 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 = """