Rename to LoveMath + major UX improvements
Browse files- Rename app MathAnimaux β LoveMath
- Translate all UI text to minimal English
- Add emoji selector (animals + places) before quiz
- Background pre-generation of reward images via ThreadPoolExecutor
- Emojiβtext mapping injected into FLUX prompt
- Comprehensive error handling throughout
- Update README with local run instructions
- README.md +44 -18
- app.py +248 -156
- image_generator.py +75 -49
- math_engine.py +9 -9
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: pink
|
| 6 |
sdk: gradio
|
|
@@ -16,26 +16,52 @@ tags:
|
|
| 16 |
- math
|
| 17 |
---
|
| 18 |
|
| 19 |
-
#
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
##
|
| 24 |
|
| 25 |
-
1.
|
| 26 |
-
2.
|
| 27 |
-
3.
|
| 28 |
-
4.
|
| 29 |
|
| 30 |
-
##
|
| 31 |
|
| 32 |
-
|
|
| 33 |
-
|-------
|
| 34 |
-
| 1 | β Additions | 5
|
| 35 |
-
| 2 | β
|
| 36 |
-
| 3 | βοΈ Multiplications | 5
|
| 37 |
-
| 4 | π²
|
| 38 |
|
| 39 |
-
##
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: LoveMath
|
| 3 |
+
emoji: π
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: pink
|
| 6 |
sdk: gradio
|
|
|
|
| 16 |
- math
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# π LoveMath
|
| 20 |
|
| 21 |
+
A mental math app for kids β answer questions, earn cute AI-generated animal images as rewards!
|
| 22 |
|
| 23 |
+
## How it works
|
| 24 |
|
| 25 |
+
1. Enter your name
|
| 26 |
+
2. Pick your favourite animals πΎ and places π (used to personalise reward images)
|
| 27 |
+
3. Answer math questions β every **3 correct answers** earns a reward image
|
| 28 |
+
4. Level up: **Additions β Subtractions β Multiplications β Mix**
|
| 29 |
|
| 30 |
+
## Levels
|
| 31 |
|
| 32 |
+
| Level | Operation | Goal |
|
| 33 |
+
|-------|-----------|------|
|
| 34 |
+
| 1 | β Additions | 5 correct |
|
| 35 |
+
| 2 | β Subtractions | 5 correct |
|
| 36 |
+
| 3 | βοΈ Multiplications | 5 correct |
|
| 37 |
+
| 4 | π² Mix | Endless |
|
| 38 |
|
| 39 |
+
## Image model
|
| 40 |
|
| 41 |
+
Rewards generated with **FLUX.1-schnell** (12B params, Apache 2.0) via π€ Diffusers.
|
| 42 |
+
Images start generating in the background as soon as the quiz begins.
|
| 43 |
+
|
| 44 |
+
## Run locally
|
| 45 |
+
|
| 46 |
+
> Requires Python on Apple Silicon (arm64). Recommended: [Miniforge](https://github.com/conda-forge/miniforge).
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
# 1. Install dependencies
|
| 50 |
+
~/miniforge3/bin/pip install -r requirements.txt
|
| 51 |
+
|
| 52 |
+
# 2. Accept FLUX.1-schnell license on HuggingFace
|
| 53 |
+
# β https://huggingface.co/black-forest-labs/FLUX.1-schnell
|
| 54 |
+
# Then log in:
|
| 55 |
+
hf auth login
|
| 56 |
+
|
| 57 |
+
# 3. Run
|
| 58 |
+
~/miniforge3/bin/python3 app.py
|
| 59 |
+
# Opens at http://localhost:7860
|
| 60 |
+
# First run downloads ~23GB of model weights (cached after that)
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
> On standard Python/pip (non-Apple Silicon):
|
| 64 |
+
> ```bash
|
| 65 |
+
> pip install -r requirements.txt
|
| 66 |
+
> python app.py
|
| 67 |
+
> ```
|
app.py
CHANGED
|
@@ -1,242 +1,334 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
Pour LΓ©a et ZoΓ© π
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
import
|
|
|
|
| 7 |
from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message
|
| 8 |
from image_generator import generate_reward_image
|
| 9 |
|
| 10 |
# ---------------------------------------------------------------------------
|
| 11 |
-
#
|
| 12 |
# ---------------------------------------------------------------------------
|
| 13 |
-
REWARD_EVERY = 3
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# ---------------------------------------------------------------------------
|
| 17 |
-
#
|
| 18 |
# ---------------------------------------------------------------------------
|
| 19 |
|
| 20 |
-
def
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
state["
|
| 36 |
-
state["
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
return (
|
| 40 |
state,
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
"", # clear feedback
|
| 48 |
-
None, # clear image
|
| 49 |
-
gr.update(visible=False), # hide reward panel
|
| 50 |
)
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
"""
|
| 55 |
-
if not state or state.get("waiting_next"):
|
| 56 |
-
return (state,) + _no_change_outputs(state)
|
| 57 |
-
|
| 58 |
try:
|
| 59 |
-
|
| 60 |
-
|
|
|
|
| 61 |
return (
|
| 62 |
state,
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
gr.update(visible=False),
|
| 69 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
if correct:
|
| 74 |
-
state["score"] += 1
|
| 75 |
-
state["streak"] += 1
|
| 76 |
-
state["correct_since_reward"] += 1
|
| 77 |
-
state["level_correct"] += 1
|
| 78 |
-
|
| 79 |
-
# Check level-up
|
| 80 |
-
level_msg = ""
|
| 81 |
-
leveled_up = False
|
| 82 |
-
threshold = LEVEL_THRESHOLDS.get(state["level"], 999)
|
| 83 |
-
if state["level_correct"] >= threshold and state["level"] < 4:
|
| 84 |
-
state["level"] += 1
|
| 85 |
-
state["level_correct"] = 0
|
| 86 |
-
level_msg = level_up_message(state["level"])
|
| 87 |
-
leveled_up = True
|
| 88 |
-
|
| 89 |
-
# Check reward
|
| 90 |
reward_image = None
|
| 91 |
reward_visible = False
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
# Next question
|
| 98 |
question, answer = generate_question(state["level"])
|
| 99 |
state["question"] = question
|
| 100 |
-
state["answer"]
|
| 101 |
-
|
| 102 |
-
streak_emoji = "π₯" * min(state["streak"], 5)
|
| 103 |
-
if leveled_up:
|
| 104 |
-
feedback = f"β
Correct ! {level_msg}"
|
| 105 |
-
else:
|
| 106 |
-
feedback = f"β
Bravo ! {streak_emoji}" if state["streak"] > 1 else "β
Correct !"
|
| 107 |
|
| 108 |
return (
|
| 109 |
state,
|
| 110 |
_status_text(state),
|
| 111 |
f"## {question} = ?",
|
| 112 |
-
"",
|
| 113 |
feedback,
|
| 114 |
reward_image,
|
| 115 |
gr.update(visible=reward_visible),
|
| 116 |
)
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
# New question after wrong answer
|
| 122 |
-
question, answer = generate_question(state["level"])
|
| 123 |
state["question"] = question
|
| 124 |
-
state["answer"]
|
| 125 |
return (
|
| 126 |
state,
|
| 127 |
_status_text(state),
|
| 128 |
f"## {question} = ?",
|
| 129 |
"",
|
| 130 |
-
|
| 131 |
None,
|
| 132 |
gr.update(visible=False),
|
| 133 |
)
|
| 134 |
|
| 135 |
|
| 136 |
-
def
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def _no_change_outputs(state):
|
| 148 |
-
return (
|
| 149 |
-
_status_text(state),
|
| 150 |
-
f"## {state.get('question', '')} = ?",
|
| 151 |
-
"",
|
| 152 |
-
"",
|
| 153 |
-
None,
|
| 154 |
-
gr.update(visible=False),
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
|
| 158 |
# ---------------------------------------------------------------------------
|
| 159 |
# Gradio UI
|
| 160 |
# ---------------------------------------------------------------------------
|
| 161 |
|
|
|
|
|
|
|
| 162 |
CSS = """
|
| 163 |
-
#title
|
| 164 |
-
#subtitle
|
| 165 |
-
#question-box
|
| 166 |
-
#feedback-box
|
| 167 |
-
#status-box
|
| 168 |
-
#reward-img
|
| 169 |
.answer-input input { font-size: 2em !important; text-align: center !important; }
|
|
|
|
| 170 |
"""
|
| 171 |
|
| 172 |
-
with gr.Blocks(title="
|
| 173 |
|
| 174 |
state = gr.State({})
|
| 175 |
|
| 176 |
-
#
|
| 177 |
-
gr.Markdown("
|
| 178 |
-
gr.Markdown("*Calcule bien et gagne des images d'animaux trop mignons !*", elem_id="subtitle")
|
| 179 |
|
| 180 |
-
#
|
| 181 |
with gr.Group(visible=True) as welcome_panel:
|
| 182 |
-
gr.Markdown("###
|
| 183 |
-
player_name = gr.Textbox(
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
)
|
| 188 |
-
|
|
|
|
| 189 |
|
| 190 |
-
#
|
| 191 |
with gr.Group(visible=False) as game_panel:
|
| 192 |
-
|
| 193 |
-
status_md = gr.Markdown("", elem_id="status-box", container=True)
|
| 194 |
-
|
| 195 |
question_md = gr.Markdown("", elem_id="question-box")
|
| 196 |
|
| 197 |
answer_input = gr.Textbox(
|
| 198 |
-
placeholder="
|
| 199 |
-
label="
|
| 200 |
max_lines=1,
|
| 201 |
elem_classes=["answer-input"],
|
| 202 |
)
|
| 203 |
-
|
| 204 |
-
|
| 205 |
feedback_md = gr.Markdown("", elem_id="feedback-box")
|
| 206 |
|
| 207 |
-
# ---- Reward panel ----
|
| 208 |
with gr.Group(visible=False) as reward_panel:
|
| 209 |
-
gr.Markdown("### π
|
| 210 |
reward_image = gr.Image(
|
| 211 |
-
label="",
|
| 212 |
-
|
| 213 |
-
elem_id="reward-img",
|
| 214 |
-
height=400,
|
| 215 |
)
|
| 216 |
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
]
|
| 232 |
-
|
| 233 |
answer_input.submit(check_answer, inputs=[answer_input, state], outputs=check_outputs)
|
| 234 |
|
|
|
|
| 235 |
restart_btn.click(
|
| 236 |
-
|
| 237 |
-
|
|
|
|
| 238 |
)
|
| 239 |
|
| 240 |
|
| 241 |
if __name__ == "__main__":
|
| 242 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
π LoveMath β Math practice with cute AI-generated animal rewards!
|
|
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
import random
|
| 6 |
+
import concurrent.futures
|
| 7 |
from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message
|
| 8 |
from image_generator import generate_reward_image
|
| 9 |
|
| 10 |
# ---------------------------------------------------------------------------
|
| 11 |
+
# Constants
|
| 12 |
# ---------------------------------------------------------------------------
|
|
|
|
| 13 |
|
| 14 |
+
REWARD_EVERY = 3 # correct answers needed to trigger a reward
|
| 15 |
+
|
| 16 |
+
ANIMAL_EMOJIS = ["πΆ", "π±", "π°", "π¦", "πΌ", "π¨", "π¦", "π―", "πΈ", "π§", "π¦", "π¦"]
|
| 17 |
+
PLACE_EMOJIS = ["π", "ποΈ", "πΈ", "π", "π", "β", "π΄", "π‘", "πΊ", "π"]
|
| 18 |
+
|
| 19 |
+
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
| 20 |
|
| 21 |
# ---------------------------------------------------------------------------
|
| 22 |
+
# State helpers
|
| 23 |
# ---------------------------------------------------------------------------
|
| 24 |
|
| 25 |
+
def _empty_state() -> dict:
|
| 26 |
+
return {}
|
| 27 |
+
|
| 28 |
+
def _status_text(state: dict) -> str:
|
| 29 |
+
if not state:
|
| 30 |
+
return ""
|
| 31 |
+
level_name = LEVEL_NAMES.get(state["level"], "")
|
| 32 |
+
return f"π€ {state['name']} | β {state['score']} | π {level_name}"
|
| 33 |
+
|
| 34 |
+
def _submit_generation(state: dict):
|
| 35 |
+
"""Fire off a background image generation job and store Future in state."""
|
| 36 |
+
animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)])
|
| 37 |
+
places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
|
| 38 |
+
streak = state.get("streak", 0)
|
| 39 |
+
future = _executor.submit(generate_reward_image, streak, animals, places)
|
| 40 |
+
state["future"] = future
|
| 41 |
+
state["awaiting_reward"] = False
|
| 42 |
+
|
| 43 |
+
def _no_change_outputs(state: dict):
|
| 44 |
+
import gradio as gr
|
| 45 |
return (
|
| 46 |
state,
|
| 47 |
+
_status_text(state),
|
| 48 |
+
f"## {state.get('question', '')} = ?",
|
| 49 |
+
"",
|
| 50 |
+
"",
|
| 51 |
+
None,
|
| 52 |
+
gr.update(visible=False),
|
|
|
|
|
|
|
|
|
|
| 53 |
)
|
| 54 |
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
# Game flow
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
|
| 59 |
+
def enter_name(player_name: str, state: dict):
|
| 60 |
+
"""Step 1: validate name, move to emoji picker."""
|
|
|
|
|
|
|
|
|
|
| 61 |
try:
|
| 62 |
+
import gradio as gr
|
| 63 |
+
name = player_name.strip() or "Player"
|
| 64 |
+
state = {"name": name}
|
| 65 |
return (
|
| 66 |
state,
|
| 67 |
+
gr.update(visible=False), # hide welcome
|
| 68 |
+
gr.update(visible=True), # show emoji picker
|
| 69 |
+
gr.update(visible=False), # hide game
|
| 70 |
+
random.sample(ANIMAL_EMOJIS, 1), # pre-select 1 animal
|
| 71 |
+
random.sample(PLACE_EMOJIS, 1), # pre-select 1 place
|
|
|
|
| 72 |
)
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"enter_name error: {e}")
|
| 75 |
+
import gradio as gr
|
| 76 |
+
return state, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), [], []
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def start_game(animal_sel: list, place_sel: list, state: dict):
|
| 80 |
+
"""Step 2: emoji confirmed β start quiz + fire background image generation."""
|
| 81 |
+
import gradio as gr
|
| 82 |
+
try:
|
| 83 |
+
# Validate selections (1β3 each required)
|
| 84 |
+
if not animal_sel or not place_sel:
|
| 85 |
+
return (state, gr.update(visible=True), gr.update(visible=False), "β οΈ Pick at least one animal and one place!", "", "")
|
| 86 |
+
|
| 87 |
+
state["selected_animals"] = animal_sel[:3]
|
| 88 |
+
state["selected_places"] = place_sel[:3]
|
| 89 |
+
state["level"] = 1
|
| 90 |
+
state["score"] = 0
|
| 91 |
+
state["streak"] = 0
|
| 92 |
+
state["correct_since_reward"] = 0
|
| 93 |
+
state["level_correct"] = 0
|
| 94 |
+
state["awaiting_reward"] = False
|
| 95 |
+
state["future"] = None
|
| 96 |
+
|
| 97 |
+
question, answer = generate_question(state["level"])
|
| 98 |
+
state["question"] = question
|
| 99 |
+
state["answer"] = answer
|
| 100 |
+
|
| 101 |
+
# Pre-generate first reward image immediately
|
| 102 |
+
_submit_generation(state)
|
| 103 |
+
|
| 104 |
+
return (state, gr.update(visible=False), gr.update(visible=True), "", _status_text(state), f"## {question} = ?")
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(f"start_game error: {e}")
|
| 108 |
+
return (state, gr.update(visible=True), gr.update(visible=False), f"β οΈ Something went wrong, try again.", "", "")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def check_answer(user_input: str, state: dict):
|
| 112 |
+
"""Validate answer, update state, potentially show reward."""
|
| 113 |
+
import gradio as gr
|
| 114 |
+
try:
|
| 115 |
+
if not state or not state.get("question"):
|
| 116 |
+
return _no_change_outputs(state)
|
| 117 |
+
|
| 118 |
+
# Parse input
|
| 119 |
+
try:
|
| 120 |
+
user_answer = int(user_input.strip())
|
| 121 |
+
except (ValueError, AttributeError):
|
| 122 |
+
return (
|
| 123 |
+
state,
|
| 124 |
+
_status_text(state),
|
| 125 |
+
f"## {state['question']} = ?",
|
| 126 |
+
"",
|
| 127 |
+
"β οΈ Numbers only!",
|
| 128 |
+
None,
|
| 129 |
+
gr.update(visible=False),
|
| 130 |
+
)
|
| 131 |
|
| 132 |
+
correct = (user_answer == state["answer"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
reward_image = None
|
| 134 |
reward_visible = False
|
| 135 |
+
feedback = ""
|
| 136 |
+
|
| 137 |
+
if correct:
|
| 138 |
+
state["score"] += 1
|
| 139 |
+
state["streak"] += 1
|
| 140 |
+
state["correct_since_reward"] += 1
|
| 141 |
+
state["level_correct"] += 1
|
| 142 |
+
|
| 143 |
+
# Check for level-up
|
| 144 |
+
threshold = LEVEL_THRESHOLDS.get(state["level"], 999)
|
| 145 |
+
level_msg = ""
|
| 146 |
+
if state["level_correct"] >= threshold and state["level"] < 4:
|
| 147 |
+
state["level"] += 1
|
| 148 |
+
state["level_correct"] = 0
|
| 149 |
+
level_msg = level_up_message(state["level"])
|
| 150 |
+
|
| 151 |
+
streak_fire = "π₯" * min(state["streak"], 5)
|
| 152 |
+
feedback = f"β
Great! {streak_fire}" if not level_msg else f"β
{level_msg}"
|
| 153 |
+
|
| 154 |
+
# Check if reward is due
|
| 155 |
+
if state["correct_since_reward"] >= REWARD_EVERY:
|
| 156 |
+
state["correct_since_reward"] = 0
|
| 157 |
+
future = state.get("future")
|
| 158 |
+
|
| 159 |
+
if future is not None and future.done():
|
| 160 |
+
# Image ready β grab it
|
| 161 |
+
try:
|
| 162 |
+
result = future.result()
|
| 163 |
+
reward_image = result[0] if result else None
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f"Image retrieval error: {e}")
|
| 166 |
+
reward_image = None
|
| 167 |
+
|
| 168 |
+
# Kick off next generation
|
| 169 |
+
_submit_generation(state)
|
| 170 |
+
else:
|
| 171 |
+
# Still generating β mark as awaiting
|
| 172 |
+
state["awaiting_reward"] = True
|
| 173 |
+
|
| 174 |
+
reward_visible = True
|
| 175 |
+
|
| 176 |
+
else:
|
| 177 |
+
state["streak"] = 0
|
| 178 |
+
feedback = f"β Answer: **{state['answer']}**"
|
| 179 |
+
|
| 180 |
+
# If previously awaiting reward and image is now ready, grab it
|
| 181 |
+
if state.get("awaiting_reward") and not reward_visible:
|
| 182 |
+
future = state.get("future")
|
| 183 |
+
if future is not None and future.done():
|
| 184 |
+
try:
|
| 185 |
+
result = future.result()
|
| 186 |
+
reward_image = result[0] if result else None
|
| 187 |
+
except Exception as e:
|
| 188 |
+
print(f"Image retrieval error: {e}")
|
| 189 |
+
reward_image = None
|
| 190 |
+
state["awaiting_reward"] = False
|
| 191 |
+
_submit_generation(state)
|
| 192 |
+
reward_visible = True
|
| 193 |
|
| 194 |
# Next question
|
| 195 |
question, answer = generate_question(state["level"])
|
| 196 |
state["question"] = question
|
| 197 |
+
state["answer"] = answer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
return (
|
| 200 |
state,
|
| 201 |
_status_text(state),
|
| 202 |
f"## {question} = ?",
|
| 203 |
+
"", # clear input
|
| 204 |
feedback,
|
| 205 |
reward_image,
|
| 206 |
gr.update(visible=reward_visible),
|
| 207 |
)
|
| 208 |
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f"check_answer error: {e}")
|
| 211 |
+
question, answer = generate_question(state.get("level", 1))
|
|
|
|
|
|
|
| 212 |
state["question"] = question
|
| 213 |
+
state["answer"] = answer
|
| 214 |
return (
|
| 215 |
state,
|
| 216 |
_status_text(state),
|
| 217 |
f"## {question} = ?",
|
| 218 |
"",
|
| 219 |
+
"β οΈ Something went wrong!",
|
| 220 |
None,
|
| 221 |
gr.update(visible=False),
|
| 222 |
)
|
| 223 |
|
| 224 |
|
| 225 |
+
def restart(state: dict):
|
| 226 |
+
"""Cancel any running future and go back to welcome screen."""
|
| 227 |
+
import gradio as gr
|
| 228 |
+
try:
|
| 229 |
+
future = state.get("future")
|
| 230 |
+
if future is not None:
|
| 231 |
+
future.cancel()
|
| 232 |
+
except Exception:
|
| 233 |
+
pass
|
| 234 |
+
return {}, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), "", ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
# ---------------------------------------------------------------------------
|
| 237 |
# Gradio UI
|
| 238 |
# ---------------------------------------------------------------------------
|
| 239 |
|
| 240 |
+
import gradio as gr
|
| 241 |
+
|
| 242 |
CSS = """
|
| 243 |
+
#title { text-align: center; font-size: 2.2em; margin-bottom: 0.1em; }
|
| 244 |
+
#subtitle { text-align: center; color: #888; margin-bottom: 1em; font-style: italic; }
|
| 245 |
+
#question-box{ text-align: center; font-size: 2.6em; font-weight: bold; padding: 0.5em; }
|
| 246 |
+
#feedback-box{ text-align: center; font-size: 1.3em; min-height: 2em; }
|
| 247 |
+
#status-box { text-align: center; padding: 0.4em; border-radius: 8px; }
|
| 248 |
+
#reward-img { border-radius: 16px; }
|
| 249 |
.answer-input input { font-size: 2em !important; text-align: center !important; }
|
| 250 |
+
.emoji-group label { font-size: 1.6em !important; }
|
| 251 |
"""
|
| 252 |
|
| 253 |
+
with gr.Blocks(title="π LoveMath") as demo:
|
| 254 |
|
| 255 |
state = gr.State({})
|
| 256 |
|
| 257 |
+
gr.Markdown("# π LoveMath", elem_id="title")
|
| 258 |
+
gr.Markdown("*Do maths. Win cute animals!*", elem_id="subtitle")
|
|
|
|
| 259 |
|
| 260 |
+
# ββ Welcome panel ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 261 |
with gr.Group(visible=True) as welcome_panel:
|
| 262 |
+
gr.Markdown("### Who are you?")
|
| 263 |
+
player_name = gr.Textbox(placeholder="Your nameβ¦", label="", max_lines=1)
|
| 264 |
+
next_btn = gr.Button("Next β‘οΈ", variant="primary", size="lg")
|
| 265 |
+
|
| 266 |
+
# ββ Emoji picker panel βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 267 |
+
with gr.Group(visible=False) as emoji_panel:
|
| 268 |
+
gr.Markdown("### Pick your animals πΎ")
|
| 269 |
+
animal_picker = gr.CheckboxGroup(
|
| 270 |
+
choices=ANIMAL_EMOJIS,
|
| 271 |
+
label="",
|
| 272 |
+
elem_classes=["emoji-group"],
|
| 273 |
+
)
|
| 274 |
+
gr.Markdown("### Pick your places π")
|
| 275 |
+
place_picker = gr.CheckboxGroup(
|
| 276 |
+
choices=PLACE_EMOJIS,
|
| 277 |
+
label="",
|
| 278 |
+
elem_classes=["emoji-group"],
|
| 279 |
)
|
| 280 |
+
picker_error = gr.Markdown("")
|
| 281 |
+
go_btn = gr.Button("Let's go! π", variant="primary", size="lg")
|
| 282 |
|
| 283 |
+
# ββ Game panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 284 |
with gr.Group(visible=False) as game_panel:
|
| 285 |
+
status_md = gr.Markdown("", elem_id="status-box", container=True)
|
|
|
|
|
|
|
| 286 |
question_md = gr.Markdown("", elem_id="question-box")
|
| 287 |
|
| 288 |
answer_input = gr.Textbox(
|
| 289 |
+
placeholder="?",
|
| 290 |
+
label="Your answer",
|
| 291 |
max_lines=1,
|
| 292 |
elem_classes=["answer-input"],
|
| 293 |
)
|
| 294 |
+
check_btn = gr.Button("βοΈ Check!", variant="primary")
|
|
|
|
| 295 |
feedback_md = gr.Markdown("", elem_id="feedback-box")
|
| 296 |
|
|
|
|
| 297 |
with gr.Group(visible=False) as reward_panel:
|
| 298 |
+
gr.Markdown("### π Your reward!")
|
| 299 |
reward_image = gr.Image(
|
| 300 |
+
label="", show_label=False,
|
| 301 |
+
elem_id="reward-img", height=400,
|
|
|
|
|
|
|
| 302 |
)
|
| 303 |
|
| 304 |
+
restart_btn = gr.Button("π Restart", variant="secondary", size="sm")
|
| 305 |
+
|
| 306 |
+
# ββ Event wiring βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 307 |
+
|
| 308 |
+
# Welcome β Emoji picker
|
| 309 |
+
enter_outputs = [state, welcome_panel, emoji_panel, game_panel, animal_picker, place_picker]
|
| 310 |
+
next_btn.click(enter_name, inputs=[player_name, state], outputs=enter_outputs)
|
| 311 |
+
player_name.submit(enter_name, inputs=[player_name, state], outputs=enter_outputs)
|
| 312 |
+
|
| 313 |
+
# Emoji picker β Game
|
| 314 |
+
start_outputs = [state, emoji_panel, game_panel, picker_error, status_md, question_md]
|
| 315 |
+
go_btn.click(start_game, inputs=[animal_picker, place_picker, state], outputs=start_outputs)
|
| 316 |
+
|
| 317 |
+
# Answer checking
|
| 318 |
+
check_outputs = [state, status_md, question_md, answer_input, feedback_md, reward_image, reward_panel]
|
| 319 |
+
check_btn.click(check_answer, inputs=[answer_input, state], outputs=check_outputs)
|
| 320 |
answer_input.submit(check_answer, inputs=[answer_input, state], outputs=check_outputs)
|
| 321 |
|
| 322 |
+
# Restart
|
| 323 |
restart_btn.click(
|
| 324 |
+
restart,
|
| 325 |
+
inputs=[state],
|
| 326 |
+
outputs=[state, welcome_panel, emoji_panel, game_panel, player_name, picker_error],
|
| 327 |
)
|
| 328 |
|
| 329 |
|
| 330 |
if __name__ == "__main__":
|
| 331 |
+
demo.launch(
|
| 332 |
+
css=CSS,
|
| 333 |
+
theme=gr.themes.Soft(primary_hue="purple", secondary_hue="pink"),
|
| 334 |
+
)
|
image_generator.py
CHANGED
|
@@ -5,33 +5,46 @@ import torch
|
|
| 5 |
# Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
|
| 6 |
if not hasattr(torch, "xpu"):
|
| 7 |
class _MockXPU:
|
| 8 |
-
is_available
|
| 9 |
-
device_count
|
| 10 |
-
empty_cache
|
| 11 |
-
manual_seed
|
| 12 |
reset_peak_memory_stats = staticmethod(lambda: None)
|
| 13 |
-
max_memory_allocated
|
| 14 |
-
synchronize
|
| 15 |
torch.xpu = _MockXPU()
|
| 16 |
|
| 17 |
# ---------------------------------------------------------------------------
|
| 18 |
-
# Detect
|
| 19 |
# ---------------------------------------------------------------------------
|
| 20 |
|
| 21 |
IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
|
| 22 |
-
|
| 23 |
if IS_HF_SPACE:
|
| 24 |
import spaces
|
| 25 |
|
| 26 |
# ---------------------------------------------------------------------------
|
| 27 |
-
#
|
| 28 |
# ---------------------------------------------------------------------------
|
| 29 |
|
| 30 |
-
|
| 31 |
-
"
|
| 32 |
-
"baby fox",
|
| 33 |
-
"baby
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
STYLES = [
|
| 37 |
"kawaii digital art",
|
|
@@ -40,33 +53,40 @@ STYLES = [
|
|
| 40 |
"pastel chibi illustration",
|
| 41 |
]
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
if streak <= 2:
|
| 48 |
-
mood
|
| 49 |
extras = "with big sparkling eyes, soft pastel colors"
|
| 50 |
elif streak <= 5:
|
| 51 |
-
mood
|
| 52 |
extras = "with big sparkling eyes, glitter, pastel rainbow colors, smiling"
|
| 53 |
else:
|
| 54 |
-
mood
|
| 55 |
extras = (
|
| 56 |
"with big sparkling eyes, magical sparkles, rainbow aura, "
|
| 57 |
"tiny crown, pastel colors, looking amazed"
|
| 58 |
)
|
| 59 |
|
| 60 |
prompt = (
|
| 61 |
-
f"A {mood} {
|
| 62 |
-
"white background, children's book illustration style, "
|
| 63 |
-
"high quality, detailed"
|
| 64 |
)
|
| 65 |
return prompt
|
| 66 |
|
| 67 |
-
|
| 68 |
# ---------------------------------------------------------------------------
|
| 69 |
-
# Pipeline loader (cached
|
| 70 |
# ---------------------------------------------------------------------------
|
| 71 |
|
| 72 |
_pipe = None
|
|
@@ -78,50 +98,56 @@ def get_pipeline():
|
|
| 78 |
|
| 79 |
from diffusers import FluxPipeline
|
| 80 |
|
| 81 |
-
print("Loading FLUX.1-schnell pipeline
|
| 82 |
_pipe = FluxPipeline.from_pretrained(
|
| 83 |
"black-forest-labs/FLUX.1-schnell",
|
| 84 |
torch_dtype=torch.bfloat16,
|
| 85 |
)
|
| 86 |
|
| 87 |
-
# Device selection: CUDA > MPS (Apple Silicon) > CPU
|
| 88 |
if torch.cuda.is_available():
|
| 89 |
_pipe = _pipe.to("cuda")
|
| 90 |
elif torch.backends.mps.is_available():
|
| 91 |
_pipe = _pipe.to("mps")
|
| 92 |
else:
|
| 93 |
-
# CPU fallback β slow but works
|
| 94 |
_pipe.enable_model_cpu_offload()
|
| 95 |
|
| 96 |
print("Pipeline ready.")
|
| 97 |
return _pipe
|
| 98 |
|
| 99 |
-
|
| 100 |
# ---------------------------------------------------------------------------
|
| 101 |
-
#
|
| 102 |
-
# Two versions: one with @spaces.GPU for HF, one plain for local
|
| 103 |
# ---------------------------------------------------------------------------
|
| 104 |
|
| 105 |
-
def _generate(streak: int):
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
if IS_HF_SPACE:
|
| 120 |
@spaces.GPU(duration=60)
|
| 121 |
-
def generate_reward_image(streak: int):
|
| 122 |
-
"""Generate
|
| 123 |
-
return _generate(streak)
|
| 124 |
else:
|
| 125 |
-
def generate_reward_image(streak: int):
|
| 126 |
-
"""Generate
|
| 127 |
-
return _generate(streak)
|
|
|
|
| 5 |
# Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
|
| 6 |
if not hasattr(torch, "xpu"):
|
| 7 |
class _MockXPU:
|
| 8 |
+
is_available = staticmethod(lambda: False)
|
| 9 |
+
device_count = staticmethod(lambda: 0)
|
| 10 |
+
empty_cache = staticmethod(lambda: None)
|
| 11 |
+
manual_seed = staticmethod(lambda seed: None)
|
| 12 |
reset_peak_memory_stats = staticmethod(lambda: None)
|
| 13 |
+
max_memory_allocated = staticmethod(lambda: 0)
|
| 14 |
+
synchronize = staticmethod(lambda: None)
|
| 15 |
torch.xpu = _MockXPU()
|
| 16 |
|
| 17 |
# ---------------------------------------------------------------------------
|
| 18 |
+
# Detect HuggingFace Spaces ZeroGPU
|
| 19 |
# ---------------------------------------------------------------------------
|
| 20 |
|
| 21 |
IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
|
|
|
|
| 22 |
if IS_HF_SPACE:
|
| 23 |
import spaces
|
| 24 |
|
| 25 |
# ---------------------------------------------------------------------------
|
| 26 |
+
# Emoji β descriptive text maps (fed into FLUX prompt)
|
| 27 |
# ---------------------------------------------------------------------------
|
| 28 |
|
| 29 |
+
ANIMAL_MAP: dict[str, str] = {
|
| 30 |
+
"πΆ": "puppy", "π±": "kitten", "π°": "bunny",
|
| 31 |
+
"π¦": "baby fox", "πΌ": "baby panda", "π¨": "baby koala",
|
| 32 |
+
"π¦": "baby lion", "π―": "baby tiger", "πΈ": "baby frog",
|
| 33 |
+
"π§": "baby penguin", "π¦": "butterfly", "π¦": "unicorn",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
PLACE_MAP: dict[str, str] = {
|
| 37 |
+
"π": "on a sunny beach with ocean waves",
|
| 38 |
+
"ποΈ": "on a snowy mountain top",
|
| 39 |
+
"πΈ": "in a cherry blossom garden",
|
| 40 |
+
"π": "under a rainbow",
|
| 41 |
+
"π": "on a glowing crescent moon",
|
| 42 |
+
"β": "surrounded by sparkling stars",
|
| 43 |
+
"π΄": "on a tropical island",
|
| 44 |
+
"π‘": "in a cosy cottage garden",
|
| 45 |
+
"πΊ": "in a field of tropical flowers",
|
| 46 |
+
"π": "in an enchanted mushroom forest",
|
| 47 |
+
}
|
| 48 |
|
| 49 |
STYLES = [
|
| 50 |
"kawaii digital art",
|
|
|
|
| 53 |
"pastel chibi illustration",
|
| 54 |
]
|
| 55 |
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
# Prompt builder
|
| 58 |
+
# ---------------------------------------------------------------------------
|
| 59 |
+
|
| 60 |
+
def build_prompt(streak: int, animals: list[str], places: list[str]) -> str:
|
| 61 |
+
# Pick one animal and one place from user selection
|
| 62 |
+
animal_emoji = random.choice(animals) if animals else random.choice(list(ANIMAL_MAP))
|
| 63 |
+
place_emoji = random.choice(places) if places else random.choice(list(PLACE_MAP))
|
| 64 |
+
|
| 65 |
+
animal_text = ANIMAL_MAP.get(animal_emoji, "bunny")
|
| 66 |
+
place_text = PLACE_MAP.get(place_emoji, "in a magical garden")
|
| 67 |
+
style = random.choice(STYLES)
|
| 68 |
|
| 69 |
if streak <= 2:
|
| 70 |
+
mood = "cute"
|
| 71 |
extras = "with big sparkling eyes, soft pastel colors"
|
| 72 |
elif streak <= 5:
|
| 73 |
+
mood = "super cute and happy"
|
| 74 |
extras = "with big sparkling eyes, glitter, pastel rainbow colors, smiling"
|
| 75 |
else:
|
| 76 |
+
mood = "magically adorable, ultra fluffy"
|
| 77 |
extras = (
|
| 78 |
"with big sparkling eyes, magical sparkles, rainbow aura, "
|
| 79 |
"tiny crown, pastel colors, looking amazed"
|
| 80 |
)
|
| 81 |
|
| 82 |
prompt = (
|
| 83 |
+
f"A {mood} {animal_text} {place_text}, {style}, {extras}, "
|
| 84 |
+
"white background, children's book illustration style, high quality, detailed"
|
|
|
|
| 85 |
)
|
| 86 |
return prompt
|
| 87 |
|
|
|
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
+
# Pipeline loader (cached)
|
| 90 |
# ---------------------------------------------------------------------------
|
| 91 |
|
| 92 |
_pipe = None
|
|
|
|
| 98 |
|
| 99 |
from diffusers import FluxPipeline
|
| 100 |
|
| 101 |
+
print("Loading FLUX.1-schnell pipelineβ¦")
|
| 102 |
_pipe = FluxPipeline.from_pretrained(
|
| 103 |
"black-forest-labs/FLUX.1-schnell",
|
| 104 |
torch_dtype=torch.bfloat16,
|
| 105 |
)
|
| 106 |
|
|
|
|
| 107 |
if torch.cuda.is_available():
|
| 108 |
_pipe = _pipe.to("cuda")
|
| 109 |
elif torch.backends.mps.is_available():
|
| 110 |
_pipe = _pipe.to("mps")
|
| 111 |
else:
|
|
|
|
| 112 |
_pipe.enable_model_cpu_offload()
|
| 113 |
|
| 114 |
print("Pipeline ready.")
|
| 115 |
return _pipe
|
| 116 |
|
|
|
|
| 117 |
# ---------------------------------------------------------------------------
|
| 118 |
+
# Core generation (always wrapped in try/except)
|
|
|
|
| 119 |
# ---------------------------------------------------------------------------
|
| 120 |
|
| 121 |
+
def _generate(streak: int, animals: list[str], places: list[str]):
|
| 122 |
+
try:
|
| 123 |
+
pipe = get_pipeline()
|
| 124 |
+
prompt = build_prompt(streak, animals, places)
|
| 125 |
+
print(f"Generating | streak={streak} | prompt: {prompt}")
|
| 126 |
+
|
| 127 |
+
result = pipe(
|
| 128 |
+
prompt=prompt,
|
| 129 |
+
num_inference_steps=4,
|
| 130 |
+
guidance_scale=0.0,
|
| 131 |
+
height=512,
|
| 132 |
+
width=512,
|
| 133 |
+
)
|
| 134 |
+
return result.images[0], prompt
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
print(f"Image generation failed: {e}")
|
| 138 |
+
return None, ""
|
| 139 |
|
| 140 |
|
| 141 |
+
# ---------------------------------------------------------------------------
|
| 142 |
+
# Public API β two versions depending on environment
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
|
| 145 |
if IS_HF_SPACE:
|
| 146 |
@spaces.GPU(duration=60)
|
| 147 |
+
def generate_reward_image(streak: int, animals: list[str], places: list[str]):
|
| 148 |
+
"""Generate reward image on HF Spaces ZeroGPU."""
|
| 149 |
+
return _generate(streak, animals, places)
|
| 150 |
else:
|
| 151 |
+
def generate_reward_image(streak: int, animals: list[str], places: list[str]):
|
| 152 |
+
"""Generate reward image locally."""
|
| 153 |
+
return _generate(streak, animals, places)
|
math_engine.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import random
|
| 2 |
|
| 3 |
-
# Levels definition
|
| 4 |
-
# Targeting ages 9-10
|
| 5 |
|
| 6 |
def generate_question(level: int) -> tuple[str, int]:
|
| 7 |
"""
|
|
@@ -19,7 +19,7 @@ def generate_question(level: int) -> tuple[str, int]:
|
|
| 19 |
|
| 20 |
elif level == 2:
|
| 21 |
a = random.randint(1, 30)
|
| 22 |
-
b = random.randint(1, a)
|
| 23 |
return f"{a} - {b}", a - b
|
| 24 |
|
| 25 |
elif level == 3:
|
|
@@ -45,17 +45,17 @@ def generate_question(level: int) -> tuple[str, int]:
|
|
| 45 |
|
| 46 |
LEVEL_NAMES = {
|
| 47 |
1: "β Additions",
|
| 48 |
-
2: "β
|
| 49 |
3: "βοΈ Multiplications",
|
| 50 |
-
4: "π²
|
| 51 |
}
|
| 52 |
|
| 53 |
-
LEVEL_THRESHOLDS = {1: 5, 2: 5, 3: 5, 4: 999}
|
| 54 |
|
| 55 |
def level_up_message(level: int) -> str:
|
| 56 |
messages = {
|
| 57 |
-
2: "π
|
| 58 |
-
3: "π
|
| 59 |
-
4: "π
|
| 60 |
}
|
| 61 |
return messages.get(level, "")
|
|
|
|
| 1 |
import random
|
| 2 |
|
| 3 |
+
# Levels definition
|
| 4 |
+
# Targeting ages 9-10
|
| 5 |
|
| 6 |
def generate_question(level: int) -> tuple[str, int]:
|
| 7 |
"""
|
|
|
|
| 19 |
|
| 20 |
elif level == 2:
|
| 21 |
a = random.randint(1, 30)
|
| 22 |
+
b = random.randint(1, a)
|
| 23 |
return f"{a} - {b}", a - b
|
| 24 |
|
| 25 |
elif level == 3:
|
|
|
|
| 45 |
|
| 46 |
LEVEL_NAMES = {
|
| 47 |
1: "β Additions",
|
| 48 |
+
2: "β Subtractions",
|
| 49 |
3: "βοΈ Multiplications",
|
| 50 |
+
4: "π² Mix",
|
| 51 |
}
|
| 52 |
|
| 53 |
+
LEVEL_THRESHOLDS = {1: 5, 2: 5, 3: 5, 4: 999}
|
| 54 |
|
| 55 |
def level_up_message(level: int) -> str:
|
| 56 |
messages = {
|
| 57 |
+
2: "π Next level!",
|
| 58 |
+
3: "π Multiplications!",
|
| 59 |
+
4: "π Expert mode!",
|
| 60 |
}
|
| 61 |
return messages.get(level, "")
|