Update app.py
Browse files
app.py
CHANGED
|
@@ -11,7 +11,7 @@ import time
|
|
| 11 |
import os
|
| 12 |
import base64
|
| 13 |
|
| 14 |
-
# ---
|
| 15 |
GRID_NUMBERS = [
|
| 16 |
10, 12, 14, 15, 16, 18, 20, 21,
|
| 17 |
24, 25, 27, 28, 30, 32, 35, 36,
|
|
@@ -19,9 +19,9 @@ GRID_NUMBERS = [
|
|
| 19 |
63, 64, 70, 72, 80, 81, 90, 100
|
| 20 |
]
|
| 21 |
KEYBOARD_KEYS = [2, 3, 4, 5, 6, 7, 8, 9, 10]
|
| 22 |
-
MAX_TIME = 200
|
| 23 |
|
| 24 |
-
# ---
|
| 25 |
def get_audio_base64(filename):
|
| 26 |
path = os.path.join("sounds", filename)
|
| 27 |
if os.path.exists(path):
|
|
@@ -30,7 +30,7 @@ def get_audio_base64(filename):
|
|
| 30 |
return "data:audio/wav;base64," + base64.b64encode(data).decode()
|
| 31 |
return ""
|
| 32 |
|
| 33 |
-
#
|
| 34 |
SOUNDS = {
|
| 35 |
"click": get_audio_base64("click.wav"),
|
| 36 |
"correct": get_audio_base64("correct.wav"),
|
|
@@ -38,20 +38,20 @@ SOUNDS = {
|
|
| 38 |
"complete": get_audio_base64("complete.wav")
|
| 39 |
}
|
| 40 |
|
| 41 |
-
# --- JAVASCRIPT
|
| 42 |
js_sound_trigger = """
|
| 43 |
(tag) => {
|
| 44 |
if (!tag) return;
|
| 45 |
const soundData = """ + str(SOUNDS) + """;
|
| 46 |
if (soundData[tag]) {
|
| 47 |
const audio = new Audio(soundData[tag]);
|
| 48 |
-
audio.play().catch(e => console.log("Audio
|
| 49 |
}
|
| 50 |
return tag;
|
| 51 |
}
|
| 52 |
"""
|
| 53 |
|
| 54 |
-
# ---
|
| 55 |
def get_empty_state():
|
| 56 |
return {
|
| 57 |
"solved": [False] * 32,
|
|
@@ -64,7 +64,7 @@ def get_empty_state():
|
|
| 64 |
|
| 65 |
def handle_game_logic(state, action_type, value=None):
|
| 66 |
if state["game_over"]:
|
| 67 |
-
return state, "Game Over!", ""
|
| 68 |
|
| 69 |
feedback = "Select a target number..."
|
| 70 |
sound_key = "click"
|
|
@@ -94,34 +94,34 @@ def handle_game_logic(state, action_type, value=None):
|
|
| 94 |
sound_key = "wrong"
|
| 95 |
state["current_factors"] = []
|
| 96 |
|
| 97 |
-
# Cálculo do Score
|
| 98 |
elapsed = int(time.time() - state["start_time"])
|
| 99 |
state["score"] = max(0, MAX_TIME - elapsed)
|
| 100 |
|
| 101 |
if all(state["solved"]):
|
| 102 |
state["game_over"] = True
|
| 103 |
-
feedback =
|
| 104 |
-
sound_key = "complete"
|
| 105 |
|
| 106 |
return state, feedback, sound_key
|
| 107 |
|
| 108 |
-
# ---
|
| 109 |
custom_css = """
|
| 110 |
.grid-btn { min-width: 80px !important; height: 80px !important; font-size: 20px !important; margin: 2px !important; }
|
| 111 |
-
.kb-btn { min-width: 60px !important; height:
|
| 112 |
"""
|
| 113 |
|
| 114 |
with gr.Blocks(title="Autuori - Factorization Game", css=custom_css) as demo:
|
| 115 |
state = gr.State(get_empty_state())
|
| 116 |
|
| 117 |
-
#
|
| 118 |
sound_trigger = gr.Textbox(visible=False)
|
| 119 |
sound_trigger.change(None, inputs=sound_trigger, js=js_sound_trigger)
|
| 120 |
|
| 121 |
-
gr.Markdown("# 🎮 Interactive Factorization v1.5 EN\n**
|
| 122 |
|
| 123 |
with gr.Row():
|
| 124 |
with gr.Column(scale=4):
|
|
|
|
| 125 |
grid_buttons = []
|
| 126 |
for r in range(4):
|
| 127 |
with gr.Row():
|
|
@@ -131,14 +131,19 @@ with gr.Blocks(title="Autuori - Factorization Game", css=custom_css) as demo:
|
|
| 131 |
grid_buttons.append(btn)
|
| 132 |
|
| 133 |
with gr.Column(scale=1):
|
| 134 |
-
|
|
|
|
| 135 |
score_display = gr.Number(label="Score", value=MAX_TIME, interactive=False)
|
| 136 |
|
| 137 |
-
gr.Markdown("###
|
| 138 |
keyboard_btns = []
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
reset_btn = gr.Button("Restart Game", variant="stop")
|
| 144 |
|
|
@@ -150,7 +155,7 @@ with gr.Blocks(title="Autuori - Factorization Game", css=custom_css) as demo:
|
|
| 150 |
updates.append(gr.update(value=label, variant=variant, interactive=not s["solved"][i]))
|
| 151 |
return updates + [s["score"]]
|
| 152 |
|
| 153 |
-
#
|
| 154 |
for i, btn in enumerate(grid_buttons):
|
| 155 |
btn.click(
|
| 156 |
fn=lambda s, idx=i: handle_game_logic(s, "select_target", idx),
|
|
|
|
| 11 |
import os
|
| 12 |
import base64
|
| 13 |
|
| 14 |
+
# --- CONFIGURATION ---
|
| 15 |
GRID_NUMBERS = [
|
| 16 |
10, 12, 14, 15, 16, 18, 20, 21,
|
| 17 |
24, 25, 27, 28, 30, 32, 35, 36,
|
|
|
|
| 19 |
63, 64, 70, 72, 80, 81, 90, 100
|
| 20 |
]
|
| 21 |
KEYBOARD_KEYS = [2, 3, 4, 5, 6, 7, 8, 9, 10]
|
| 22 |
+
MAX_TIME = 200
|
| 23 |
|
| 24 |
+
# --- AUDIO LOADING (BASE64 for Zero Latency) ---
|
| 25 |
def get_audio_base64(filename):
|
| 26 |
path = os.path.join("sounds", filename)
|
| 27 |
if os.path.exists(path):
|
|
|
|
| 30 |
return "data:audio/wav;base64," + base64.b64encode(data).decode()
|
| 31 |
return ""
|
| 32 |
|
| 33 |
+
# Pre-load sounds from the /sounds folder
|
| 34 |
SOUNDS = {
|
| 35 |
"click": get_audio_base64("click.wav"),
|
| 36 |
"correct": get_audio_base64("correct.wav"),
|
|
|
|
| 38 |
"complete": get_audio_base64("complete.wav")
|
| 39 |
}
|
| 40 |
|
| 41 |
+
# --- JAVASCRIPT FOR INSTANT PLAYBACK ---
|
| 42 |
js_sound_trigger = """
|
| 43 |
(tag) => {
|
| 44 |
if (!tag) return;
|
| 45 |
const soundData = """ + str(SOUNDS) + """;
|
| 46 |
if (soundData[tag]) {
|
| 47 |
const audio = new Audio(soundData[tag]);
|
| 48 |
+
audio.play().catch(e => console.log("Audio blocked: Interact with the page first."));
|
| 49 |
}
|
| 50 |
return tag;
|
| 51 |
}
|
| 52 |
"""
|
| 53 |
|
| 54 |
+
# --- GAME LOGIC ---
|
| 55 |
def get_empty_state():
|
| 56 |
return {
|
| 57 |
"solved": [False] * 32,
|
|
|
|
| 64 |
|
| 65 |
def handle_game_logic(state, action_type, value=None):
|
| 66 |
if state["game_over"]:
|
| 67 |
+
return state, "Game Over! Restart to play.", ""
|
| 68 |
|
| 69 |
feedback = "Select a target number..."
|
| 70 |
sound_key = "click"
|
|
|
|
| 94 |
sound_key = "wrong"
|
| 95 |
state["current_factors"] = []
|
| 96 |
|
|
|
|
| 97 |
elapsed = int(time.time() - state["start_time"])
|
| 98 |
state["score"] = max(0, MAX_TIME - elapsed)
|
| 99 |
|
| 100 |
if all(state["solved"]):
|
| 101 |
state["game_over"] = True
|
| 102 |
+
feedback = "🏆 CONGRATULATIONS! ALL SOLVED!"
|
| 103 |
+
sound_key = "complete"
|
| 104 |
|
| 105 |
return state, feedback, sound_key
|
| 106 |
|
| 107 |
+
# --- STYLING ---
|
| 108 |
custom_css = """
|
| 109 |
.grid-btn { min-width: 80px !important; height: 80px !important; font-size: 20px !important; margin: 2px !important; }
|
| 110 |
+
.kb-btn { min-width: 60px !important; height: 60px !important; font-size: 18px !important; }
|
| 111 |
"""
|
| 112 |
|
| 113 |
with gr.Blocks(title="Autuori - Factorization Game", css=custom_css) as demo:
|
| 114 |
state = gr.State(get_empty_state())
|
| 115 |
|
| 116 |
+
# Hidden field to trigger JS sound
|
| 117 |
sound_trigger = gr.Textbox(visible=False)
|
| 118 |
sound_trigger.change(None, inputs=sound_trigger, js=js_sound_trigger)
|
| 119 |
|
| 120 |
+
gr.Markdown("# 🎮 Interactive Factorization v1.5 EN\n**Developed by Marcos Autuori**")
|
| 121 |
|
| 122 |
with gr.Row():
|
| 123 |
with gr.Column(scale=4):
|
| 124 |
+
# 4x8 Target Grid
|
| 125 |
grid_buttons = []
|
| 126 |
for r in range(4):
|
| 127 |
with gr.Row():
|
|
|
|
| 131 |
grid_buttons.append(btn)
|
| 132 |
|
| 133 |
with gr.Column(scale=1):
|
| 134 |
+
# Status & Info
|
| 135 |
+
status_box = gr.Textbox(label="Status", value="Select a target", interactive=False)
|
| 136 |
score_display = gr.Number(label="Score", value=MAX_TIME, interactive=False)
|
| 137 |
|
| 138 |
+
gr.Markdown("### Factor Keyboard")
|
| 139 |
keyboard_btns = []
|
| 140 |
+
# Keyboard 3x3 (Correct Layout)
|
| 141 |
+
for r_kb in range(3):
|
| 142 |
+
with gr.Row():
|
| 143 |
+
for c_kb in range(3):
|
| 144 |
+
val = KEYBOARD_KEYS[r_kb * 3 + c_kb]
|
| 145 |
+
kb_btn = gr.Button(str(val), variant="secondary", elem_classes="kb-btn")
|
| 146 |
+
keyboard_btns.append(kb_btn)
|
| 147 |
|
| 148 |
reset_btn = gr.Button("Restart Game", variant="stop")
|
| 149 |
|
|
|
|
| 155 |
updates.append(gr.update(value=label, variant=variant, interactive=not s["solved"][i]))
|
| 156 |
return updates + [s["score"]]
|
| 157 |
|
| 158 |
+
# Events
|
| 159 |
for i, btn in enumerate(grid_buttons):
|
| 160 |
btn.click(
|
| 161 |
fn=lambda s, idx=i: handle_game_logic(s, "select_target", idx),
|