File size: 7,217 Bytes
c674c63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d69ca6
c674c63
 
 
 
 
 
 
 
 
5d69ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c674c63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d69ca6
c674c63
5d69ca6
 
c674c63
 
 
 
 
 
 
 
 
5d69ca6
c674c63
 
5d69ca6
c674c63
 
 
 
 
 
5d69ca6
 
c674c63
 
5d69ca6
 
c674c63
5d69ca6
 
c674c63
5d69ca6
c674c63
 
 
 
 
5d69ca6
c674c63
5d69ca6
c674c63
5d69ca6
c674c63
5d69ca6
 
 
 
 
 
 
 
 
 
c674c63
 
 
 
 
5d69ca6
c674c63
 
5d69ca6
 
 
 
 
c674c63
 
 
5d69ca6
c674c63
 
 
 
 
 
 
 
 
5d69ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c674c63
 
 
 
5d69ca6
 
c674c63
 
 
 
 
 
 
5d69ca6
c674c63
 
 
 
 
 
 
5d69ca6
c674c63
 
 
5d69ca6
c674c63
5d69ca6
 
 
 
c674c63
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""
================================================================================
                    INTERACTIVE FACTORIZATION v1.4 EN
================================================================================
Main Developer: Marcos Autuori | Author: Autuori
================================================================================
"""

import gradio as gr
import time
import wave
import struct
import math
import os

# --- CONFIGURATION & DATA ---
GRID_NUMBERS = [
    10, 12, 14, 15, 16, 18, 20, 21,
    24, 25, 27, 28, 30, 32, 35, 36,
    40, 42, 45, 48, 50, 54, 56, 60,
    63, 64, 70, 72, 80, 81, 90, 100
]
KEYBOARD_KEYS = [2, 3, 4, 5, 6, 7, 8, 9, 10]
MAX_TIME = 150

# --- AUDIO GENERATION ENGINE ---
def ensure_sounds():
    if not os.path.exists("sounds"):
        os.makedirs("sounds")
    
    # Frequency mapping (Hz)
    sound_map = {
        "sounds/click.wav": 440,    # A4
        "sounds/correct.wav": 880,  # A5
        "sounds/wrong.wav": 220     # A3
    }
    
    for filename, freq in sound_map.items():
        if not os.path.exists(filename):
            sample_rate = 44100
            duration = 0.15 if "click" not in filename else 0.05
            volume = 0.3
            n_samples = int(sample_rate * duration)
            with wave.open(filename, 'w') as f:
                f.setnchannels(1)
                f.setsampwidth(2)
                f.setframerate(sample_rate)
                for i in range(n_samples):
                    val = int(volume * 32767.0 * math.sin(2.0 * math.pi * freq * i / sample_rate))
                    f.writeframesraw(struct.pack('<h', val))

ensure_sounds()

# --- GAME LOGIC ---
def get_empty_state():
    return {
        "solved": [False] * 32,
        "selected_idx": None,
        "current_factors": [],
        "start_time": time.time(),
        "game_over": False,
        "score": MAX_TIME
    }

def format_grid_button(idx, solved_list, selected_idx):
    label = str(GRID_NUMBERS[idx])
    if solved_list[idx]:
        return gr.update(value=f"✅ {label}", variant="success", interactive=False)
    if selected_idx == idx:
        return gr.update(variant="primary", interactive=True)
    return gr.update(value=label, variant="secondary", interactive=True)

def handle_game_logic(state, action_type, value=None):
    if state["game_over"]:
        return state, "Game Over! Please restart.", None

    feedback = "Select a target number..."
    sound_to_play = "sounds/click.wav"
    
    if action_type == "select_target":
        state["selected_idx"] = value
        state["current_factors"] = []
        feedback = f"Factoring: {GRID_NUMBERS[value]}"
        
    elif action_type == "select_factor":
        if state["selected_idx"] is None:
            feedback = "⚠️ Select a grid number first!"
            sound_to_play = "sounds/wrong.wav"
        else:
            state["current_factors"].append(int(value))
            
            if len(state["current_factors"]) == 2:
                a, b = state["current_factors"]
                target = GRID_NUMBERS[state["selected_idx"]]
                
                if a * b == target:
                    state["solved"][state["selected_idx"]] = True
                    feedback = f"✔️ {a} x {b} = {target}! Correct!"
                    sound_to_play = "sounds/correct.wav"
                    state["selected_idx"] = None
                else:
                    feedback = f"❌ {a} x {b} = {a*b} (Incorrect)."
                    sound_to_play = "sounds/wrong.wav"
                state["current_factors"] = []
            else:
                feedback = f"First factor: {value}. Pick the second one!"

    # Score calculation
    elapsed = int(time.time() - state["start_time"])
    state["score"] = max(0, MAX_TIME - elapsed)
    
    if all(state["solved"]):
        state["game_over"] = True
        feedback = f"🏆 VICTORY! Final Score: {state['score']}"

    return state, feedback, sound_to_play

# --- INTERFACE DESIGN ---
custom_css = """
.grid-btn { 
    min-width: 80px !important; 
    height: 80px !important; 
    font-size: 20px !important; 
    margin: 2px !important;
}
.kb-btn { 
    min-width: 60px !important; 
    height: 50px !important; 
}
"""

with gr.Blocks(title="Autuori - Factorization Game", css=custom_css) as demo:
    state = gr.State(get_empty_state())
    
    # Hidden audio component (The core of the sound system)
    audio_player = gr.Audio(interactive=False, visible=False, autoplay=True)
    
    gr.Markdown(f"""
    # 🎮 Interactive Factorization v1.4 EN
    **Developer:** Marcos Autuori
    *Instructions: Select a blue number from the grid, then pick two factors from the keyboard.*
    """)
    
    with gr.Row():
        with gr.Column(scale=4):
            # 4x8 Grid Layout
            grid_buttons = []
            for r in range(4):
                with gr.Row():
                    for c in range(8):
                        idx = r * 8 + c
                        btn = gr.Button(str(GRID_NUMBERS[idx]), elem_classes="grid-btn")
                        grid_buttons.append(btn)
        
        with gr.Column(scale=1):
            # Status & Score Panel
            status_box = gr.Textbox(label="Status", value="Select a blue target", interactive=False)
            score_display = gr.Number(label="Score (Time)", value=MAX_TIME, interactive=False)
            
            gr.Markdown("### Factors (2-10)")
            keyboard_btns = []
            # Keyboard arranged in rows
            for r_kb in range(3):
                with gr.Row():
                    for c_kb in range(3):
                        val = KEYBOARD_KEYS[r_kb * 3 + c_kb]
                        kb_btn = gr.Button(str(val), variant="secondary", elem_classes="kb-btn")
                        keyboard_btns.append(kb_btn)
            
            reset_btn = gr.Button("Restart Game", variant="stop")

    # Helper function to sync UI
    def update_ui(s):
        updates = [format_grid_button(i, s["solved"], s["selected_idx"]) for i in range(32)]
        return updates + [s["score"]]

    # --- EVENT MAPPING ---
    # Grid Clicks
    for i, btn in enumerate(grid_buttons):
        btn.click(
            fn=lambda s, idx=i: handle_game_logic(s, "select_target", idx),
            inputs=[state],
            outputs=[state, status_box, audio_player]
        ).then(fn=update_ui, inputs=[state], outputs=grid_buttons + [score_display])

    # Keyboard Clicks
    for kb_btn in keyboard_btns:
        kb_btn.click(
            fn=lambda s, v=kb_btn.value: handle_game_logic(s, "select_factor", v),
            inputs=[state],
            outputs=[state, status_box, audio_player]
        ).then(fn=update_ui, inputs=[state], outputs=grid_buttons + [score_display])

    # Reset Action
    def reset_all():
        new_state = get_empty_state()
        updates = [gr.update(value=str(num), variant="secondary", interactive=True) for num in GRID_NUMBERS]
        return [new_state, "New Game Started!", None] + updates + [MAX_TIME]

    reset_btn.click(
        fn=reset_all, 
        outputs=[state, status_box, audio_player] + grid_buttons + [score_display]
    )

if __name__ == "__main__":
    demo.launch()